- tests/config-startup-error.test.mjs: static assertions on the committed
startup-error module, the package boundary, the server wiring (validation
before bind), the compose secret and the Dockerfile shipping, each backed
by mutation probes; the deterministic probes execute the issue's test plan
("start with a missing required field and confirm the error names it") —
the compiled boundary throws MissingRequiredSettingError naming
sessionSecret, and booting the committed server without EPPP_SESSION_SECRET
exits non-zero naming the field while a valid secret boots to /health 200
- health-endpoint/app-readiness boot probes: provide a valid
EPPP_SESSION_SECRET (the required setting is validated at startup)
- ci.yml: new config-startup-error job (builds config + database-postgres,
runs the suite); app-readiness job now builds the config package too
- .gitignore: transient .config-startup-probe-*.mjs files
Adds packages/config (@personal-blog/config) — the EPPP configuration
service foundation. The package defines the configuration schema with
TypeBox (configSchema: host, port, databaseUrl, sessionSecret — the
validated config fields, golden-tuple pins @sinclair/typebox@0.34.52 and
ajv@8.20.0) and compiles it with Ajv (validateConfig). The environment
adapter (T04), field-specific startup errors (T02) and secret redaction
(T03) build on this boundary in later tasks; nothing reads process.env yet.
Wiring for the new workspace package: lockfile importer + resolved
typebox/ajv tree, apps/server/Dockerfile manifest copy (frozen in-image
install must match the lockfile importers), config-schema CI job, package
set fixtures (workspace-layout, workspace-config, strict-tsconfig,
typescript-pin), probe-file gitignore entry.
Static assertions + mutation probes on the committed runner source, a
deterministic stub-pool behavioral probe (intentionally failing migration
fixture -> structured diagnostic naming the failing migration, apply and
record phases), a docker-gated real-stack probe against a real database
(the issue's test plan), and CI enforcement via the additive
database-postgres-diagnostic job.
Security-review finding F2: two concurrent acquire() calls on the same
MigrationLock instance could each check out a connection; the second
pg_advisory_lock would overwrite this.client, leaking the first locked
connection until session end.
acquire() now memoizes the in-flight acquire in acquireInFlight and
returns it on re-entry, so exactly one connection is checked out and no
locked connection leaks. The memo is cleared once the acquire settles.
tryAcquire()/release() paths unchanged.
Locked in by:
- static criterion test: acquireInFlight field, re-entry guard returns
the in-flight acquire, memo cleared on settle
- mutation probe: removing the re-entry guard fails the criterion
- real-stack probe: two concurrent acquire() calls on one instance leave
pool.totalCount at 1 (exactly one connection), the lock granted once,
nothing left after release; probe fails (hangs) on the pre-fix code
- CI job comment updated to reflect the re-entrancy criterion
Static assertions on the committed ledger source (idempotent table DDL,
parameterized idempotent record, has/applied queries, driver-boundary
re-export, CI enforcement) with mutation probes proving non-vacuousness;
docker-gated real-stack probe migrates an empty database (isolated compose
project + host port) and confirms the ledger exists, the applied migrations
are recorded, and a re-run records nothing twice. New additive
database-postgres-ledger CI job gates the criterion on every PR.
- packages/database-postgres (@personal-blog/database-postgres): the single
workspace package allowed to import the PostgreSQL driver — declares pg and
kysely as exact dependencies (@types/pg for types) and its src/index.ts
imports and re-exports the driver pieces (Pool, Kysely, PostgresDialect) so
the isolation is real, not a placeholder
- pnpm-lock.yaml: importer for packages/database-postgres plus the resolved
pg/kysely dependency tree (frozen-lockfile install keeps working)
- .gitea/workflows/ci.yml: new database-postgres-imports job runs
tests/database-postgres-imports.test.mjs on every PR so the isolation
criterion gates merges
- compose.yaml: db.image pinned to postgres:18.6-bookworm (exact 18.6 minor,
same bookworm flavor, no Alpine drift) so the database container is
reproducible and exposes the expected PostgreSQL version; document the
rollback (revert image to postgres:18-bookworm)
- .gitea/workflows/ci.yml: new compose-config job runs
tests/compose-config.test.mjs on every PR so the pinned-version criterion
gates merges (docker-gated real-stack probes skip cleanly without a daemon)
Resolve the security review of #389 (findings 1-4):
- .dockerignore: every env/credential pattern is now **/-prefixed
(**/.env, **/.env.*, **/node_modules, **/.npmrc, ..., **/secrets,
**/*.pem, **/*.key, ...) and the redundant 'secrets/' line is dropped.
Docker's matcher (moby/patternmatcher) anchors slash-less patterns to
the context root, so the bare forms excluded nothing under apps/server/;
**/ matches the root AND any nested depth. (finding 1, 3)
- tests/secrets-not-embedded.test.mjs: the dockerignore matcher is now a
faithful port of moby/patternmatcher (filepath.Clean + anchored full-path
match + parent-directory propagation), not gitignore basename semantics;
asserts nested example paths (apps/server/.npmrc, config/server.key,
apps/server/secrets/...) are excluded; requires no redundant equivalent
patterns verbatim; adds mutation probes for bare-pattern and
duplicate-pattern regressions. (finding 2, 3)
- apps/server/Dockerfile + compose.yaml: guarantee restated precisely
(credential files excluded at the context root AND at any depth).
- .gitea/workflows/ci.yml: new job runs
'node --test tests/secrets-not-embedded.test.mjs' on every PR; the
docker-gated layer-scan probe runs where a daemon exists, skips cleanly
otherwise. (finding 4)
- tests/compose-config.test.mjs: .dockerignore presence list updated to the
**/-prefixed forms (node_modules, .env).
Tested: secrets suite 16 tests -> 15 pass / 1 docker-gated skip / 0 fail;
full suite 101 pass / 12 fail / 8 skip, failures identical to clean main
(env-dependent pnpm/Node-24 suites); matcher port verified against the
moby/patternmatcher evidence table.
Rework PR #15 per security review SEC-14-R1: the client no longer carries
an API token. js/newsletter-config.js ships only the non-secret endpoint,
enforced https-only at config load time via validateEndpoint() (mirroring
the protocol allowlist in js/reading-list.js); js/newsletter.js POSTs
email-only with no Authorization header. Failure paths keep the single
user-safe message that never leaks token, endpoint, status, or raw body;
success still shows the confirmation. CI gains a gitleaks step that fails
on any secret hit; README documents the server-side token, the residual
signup-abuse risk, and the authoritative server-side validation follow-up.