Commit Graph
20 Commits
Author SHA1 Message Date
implementer ec4719a8b4 docs: point at the committed .env.example template (E00-S04-T05)
CI / Frozen lockfile install (pull_request) Successful in 45s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 25s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 43s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 42s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 49s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m3s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m12s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m6s
CI / Env adapter owns process.env (E00-S04-T04) (pull_request) Successful in 1m8s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 52s
CI / .env.example placeholders only (E00-S04-T05) (pull_request) Successful in 25s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 28s
2026-08-30 05:12:06 +00:00
implementer 345ceccfad feat: HOST validated at the adapter boundary and controls the actual bind interface (E00-S04-T04)
The environment adapter now resolves HOST through resolveHost, validating it
at the adapter boundary as a hostname (RFC 1123) or IP address (IPv4/IPv6,
node:net isIP); an invalid HOST throws a field-specific ConfigStartupError
naming host, so arbitrary env content is never used for binding or echoed
verbatim into the startup log (issue acceptance criterion, resolving security
review finding SEC-3).

The server passes config.host to server.listen(config.port, config.host, ...),
so a configured HOST binds exactly that interface and the startup log never
claims a bind the process does not enforce (resolving SEC-2).
2026-08-30 04:41:53 +00:00
implementer 7aeeeaaa97 feat: environment adapter is the single owner of process.env reads (E00-S04-T04) 2026-08-30 04:17:19 +00:00
implementer ae900589c3 docs: document the redacted resolved-configuration log in the non-container guide (E00-S04-T03)
CI / Frozen lockfile install (pull_request) Successful in 45s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 25s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 23s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 48s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 43s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 46s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m11s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m9s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m18s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 57s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 26s
2026-08-30 03:54:36 +00:00
implementer 6458013306 feat: secrets redact from logs via config redaction layer and server redacting logger (E00-S04-T03) 2026-08-30 03:52:52 +00:00
implementer 0ce790fca3 feat: missing required setting fails startup with a field-specific error (E00-S04-T02)
- packages/config: add src/startup.ts exposing assertValidConfig (builds on
  the T01 TypeBox/Ajv schema) and the field-specific startup errors
  (MissingRequiredSettingError names the missing field; ConfigStartupError
  names each violating field); re-export from the package boundary
- apps/server: validate the startup configuration (including the required
  EPPP_SESSION_SECRET) before the server binds, so a missing required
  setting crashes the process at startup naming the field; depends on
  @personal-blog/config
- compose.yaml: provide EPPP_SESSION_SECRET for the app service (dev-only
  >= 32 char default; override via .env / shell)
- Dockerfile: ship the compiled packages/config in the image (build source +
  runtime dist), matching the server's new workspace dependency
- pnpm-lock.yaml: apps/server importer gains @personal-blog/config
2026-08-30 03:00:40 +00:00
implementer bcea489391 feat: add TypeBox/Ajv config schema package to the workspace (E00-S04-T01)
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.
2026-08-30 02:29:52 +00:00
implementer ebdee5daa5 feat: app reports ready only after the startup migration run (E00-S03-T06) 2026-08-30 01:56:19 +00:00
implementer bb3a68648a fix: inject the ledger into MigrationRunner (type-only import) so probes load the committed module under type stripping
CI / Frozen lockfile install (pull_request) Successful in 45s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 29s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 28s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 42s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 44s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 41s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
Node's type stripping does not rewrite './ledger.js' to './ledger.ts', so the
runner's runtime import of the ledger could not resolve when the behavioral
probes execute the committed runner.ts directly (CI failure on Node 24).
The ledger is now imported type-only and the caller passes the instance
(new MigrationLedger(pool)) — the probes already do. runner.ts has no
runtime imports left, so type stripping erases them and the committed
module loads as-is.
2026-08-30 01:24:14 +00:00
implementer f6d407394d feat: add migration runner with structured failure diagnostic (E00-S03-T05)
MigrationRunner applies pending migrations through the migration ledger
exactly once; when a migration fails it throws a MigrationFailedError
whose diagnostic is a structured object identifying the failing migration
(version), the failure phase (apply/record), the underlying cause, and the
applied/pending ledger state, serializable via toJSON. Re-exported from
the driver boundary so no other package needs the pg driver to run
migrations. Advisory lock (T04) and ready gate (T06) remain out of scope.
2026-08-30 01:17:23 +00:00
implementer cb1b161ce9 fix: destroy the connection on unlock failure so a still-locked session is never reused (E00-S03-T04)
CI / Frozen lockfile install (pull_request) Successful in 47s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 25s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 41s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 44s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
release() previously returned the connection to the pool in a finally even
when the pg_advisory_unlock statement failed, so a pooled connection could be
reused while its session still held the migration advisory lock - the next
borrower would block every other runner (reviewer finding F4). On unlock
failure the connection is now destroyed (client.release(error) removes the
client from the pool, ending the session and its lock); the plain
client.release() is kept only on the success path. Locked in by a static
assertion, a mutation probe, and a deterministic stub-pool behavioral probe
of the committed release() control flow.
2026-08-30 00:49:56 +00:00
implementer dac3679e33 fix: memoize in-flight acquire so concurrent acquire() is re-entrant-safe (E00-S03-T04)
CI / Frozen lockfile install (pull_request) Successful in 49s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 45s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 50s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
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
2026-08-30 00:29:20 +00:00
implementer 3f80063303 feat: add migration advisory lock to database-postgres (E00-S03-T04) 2026-08-30 00:08:57 +00:00
implementer 7f6450410e feat: add migration ledger to database-postgres (E00-S03-T03)
MigrationLedger over the package-owned pg Pool: ensure() creates the
schema_migrations table (version text PRIMARY KEY, applied_at timestamptz
NOT NULL DEFAULT now()) with idempotent DDL; record() inserts an applied
migration with a parameterized, idempotent statement (ON CONFLICT DO
NOTHING — a rerun never double-applies); has()/applied() read the ledger
back in apply order. Re-exported from the driver boundary (src/index.ts)
so no other package needs the pg driver to touch migration state.
2026-08-29 23:19:56 +00:00
implementer 09b7a40d00 fix: pin database-postgres driver to the golden tuple (pg 8.22.0, Kysely 0.29.4)
CI / Frozen lockfile install (pull_request) Successful in 46s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 35s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 25s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 31s
Review finding on PR #391: packages/database-postgres pinned pg@8.23.0 and
kysely@0.29.5, but the architecture doc's golden tuple (Technology-Stack
section 5.2 / section 7) pins pg@8.22.0 and Kysely@0.29.4. Reproducibility
requires the exact documented versions.

- packages/database-postgres/package.json: pg 8.23.0 -> 8.22.0,
  kysely 0.29.5 -> 0.29.4, @types/pg 8.23.1 -> 8.21.0 (no 8.22.x of
  @types/pg is published; 8.21.0 is the closest matching release, types
  for the immediately preceding pg minor)
- pnpm-lock.yaml: regenerated with pnpm 11.23.0 (Node 24); the resolved
  pg dependency tree is unchanged apart from the driver version itself
- tests/database-postgres-imports.test.mjs: exact-pin assertions updated
  to the corrected versions, with a comment noting the @types/pg choice
2026-08-29 11:27:29 +00:00
implementer 5c202ba21e feat: add database-postgres driver boundary package (E00-S03-T02)
- 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
2026-08-29 11:14:14 +00:00
implementer 0d1bd19093 feat: add root build/test/typecheck scripts (E00-S01-T05)
CI / Frozen lockfile install (pull_request) Successful in 50s
Adds root scripts so build, test and typecheck run from the workspace root:
- root package.json gains scripts: build (pnpm -r run build), test
  (node --test on tests/**/*.test.mjs), typecheck (pnpm -r run typecheck)
- every workspace package (apps/server, packages/core,
  extensions/example) gains build (tsc -p tsconfig.json) and typecheck
  (tsc -p tsconfig.json --noEmit) scripts
- typescript 6.0.3 pinned as an exact root devDependency so the commands
  run from a clean checkout; lockfile regenerated with pnpm 11.23.0

Verified from a clean state: pnpm install --frozen-lockfile passes,
pnpm build emits dist for all 3 packages, pnpm typecheck passes for all 3,
pnpm test runs tests/architecture-import.test.mjs 10/10 green, and CI's
pnpm -r list --depth -1 still lists all 4 workspace projects.

Closes #158
2026-08-28 08:39:13 +00:00
implementer b1fef8d592 feat: configure ESM package boundaries across the workspace (E00-S01-T03)
CI / Frozen lockfile install (pull_request) Successful in 39s
Adds ESM boundary declarations to every workspace package manifest
(apps/server, packages/core, extensions/example): "type": "module",
"main"/"types" entry points and an "exports" map (types + import
conditions) so each package exposes only its public root; adds
"type": "module" to the workspace root package.json so the workspace is
uniformly ESM. Placeholder src/index.ts files stay as empty ESM modules.

Verified: each package compiles under tsconfig.base.json (NodeNext) with
TypeScript 6.0.3 and emits ESM dist/index.js + dist/index.d.ts; Node
imports each package by name through its exports map and rejects deep
subpath imports (ERR_PACKAGE_PATH_NOT_EXPORTED); pnpm 11.23.0
install --frozen-lockfile passes with the lockfile unchanged.

Closes #156
2026-08-28 08:13:00 +00:00
implementer 4e527f9267 feat: add strict tsconfig.base.json shared by all packages (E00-S01-T02)
CI / Frozen lockfile install (pull_request) Successful in 39s
Adds a strict base TypeScript config at the workspace root (ES2023 / NodeNext
/ strict family incl. noUncheckedIndexedAccess, exactOptionalPropertyTypes,
noImplicitOverride, useUnknownInCatchVariables, verbatimModuleSyntax per
Engineering-Standards) and gives every workspace package
(apps/server, packages/core, extensions/example) a tsconfig.json that extends
it. Each package gets a minimal src/index.ts placeholder so it compiles under
the base config (CJS-safe `export {}` until ESM boundaries land in T03).

Verified: every package typechecks and emits (js + d.ts + sourcemap) with the
pinned TypeScript 6.0.3; a strictness probe confirms the strict family fires.

Closes #155
2026-08-28 00:00:51 +00:00
implementer a2fa1dee32 chore: bootstrap pnpm workspace and package manifests
CI / Frozen lockfile install (pull_request) Successful in 48s
E00-S01-T01: create workspace/package manifests
- root package.json pins packageManager pnpm@11.23.0
- pnpm-workspace.yaml separates apps/*, packages/*, extensions/* entries
- skeleton manifests for apps/server, packages/core, extensions/example
- committed frozen lockfile generated by pnpm 11.23.0
- minimal CI: pnpm install --frozen-lockfile + workspace verification
2026-08-27 23:07:58 +00:00