Commit Graph
80 Commits
Author SHA1 Message Date
implementer b6c3fd9a28 docs(adr): record React SSR for public rendering decision as ADR-007
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m14s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m37s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m3s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m14s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m20s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 51s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s
2026-08-31 00:51:41 +00:00
implementer b8f0abdb0d docs(adr): record Fastify 5 HTTP runtime decision as ADR-004
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m13s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m40s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m11s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m12s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 44s
2026-08-31 00:34:08 +00:00
implementer 10ea1ef3db test: lock in the action SHA pins and workflow-level permissions with the ci-stages suite (E00-S05-T01)
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 4m11s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m24s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 51s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m49s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 4m2s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m15s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m43s
- assertHardening: every uses: ref is a full 40-char commit SHA equal to the
  committed PINNED_ACTIONS table (no floating tags) and the workflow declares
  a top-level permissions: contents: read block
- mutation probes: reverting a pin to @v4, swapping a pinned SHA or removing
  the permissions block all fail
2026-08-30 06:29:43 +00:00
implementer d0c3b6a83d ci: pin third-party actions to full commit SHAs and declare minimal workflow permissions (E00-S05-T01)
- actions/checkout pinned to 11bd71901bbe5b1630ceea73d27597364c9af683 (v4.2.2)
- actions/setup-node pinned to 1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a (v4.2.0)
- workflow-level permissions: contents: read (the pipeline only reads the repo)
- no floating @v4 tags remain anywhere in the workflow
2026-08-30 06:29:43 +00:00
implementer 13b1548df8 ci: build the config and database-postgres packages before the architecture suites (E00-S05-T01)
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 53s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m33s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 1m20s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 2m46s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 4m55s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m22s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m22s
The strict-tsconfig suite typechecks apps/server with tsc --noEmit, which
resolves @personal-blog/config and @personal-blog/database-postgres through
their compiled dist/ type declarations. On a fresh checkout dist/ does not
exist, so the architecture stage must build those two packages first (the
same prerequisite the unit and postgres-integration stages already declare).
2026-08-30 06:22:26 +00:00
implementer f3cd70e45d docs: document the lint command and the CI quality baseline stages (E00-S05-T01)
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 46s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m15s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m44s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Failing after 56s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Skipped
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Skipped
The non-container guide now covers pnpm lint (the formatting/lint policy) in
the Test and clean-clone smoke sections and describes the ordered PR CI
stages locked in by tests/ci-stages.test.mjs.
2026-08-30 06:12:25 +00:00
implementer 89fe0b52e6 ci: run the required PR quality stages in order (E00-S05-T01)
Restructure .gitea/workflows/ci.yml from a flat list of per-suite jobs into
the ordered stage baseline: frozen install -> typecheck -> formatting/lint ->
unit -> architecture -> PostgreSQL integration -> build of the admin and
server applications. Each stage gates on its predecessor through needs, so
the frozen install runs before every later stage and the pipeline halts on
the first failing stage. The docker-gated real-stack probes in the
postgres-integration (and container) suites keep running where a Docker
daemon is available and skipping cleanly otherwise.
2026-08-30 06:12:25 +00:00
implementer d9b493498e test: lock in the CI quality baseline stage order with the ci-stages suite (E00-S05-T01)
tests/ci-stages.test.mjs asserts that .gitea/workflows/ci.yml declares the
seven required PR stages (frozen-install, typecheck, formatting-lint, unit,
architecture, postgres-integration, build-apps) in order, that every later
stage gates on its predecessor through needs, that each stage runs its
expected command (frozen install, pnpm typecheck, pnpm lint, the unit /
architecture / postgres-integration node --test runs, the apps-group build
with the server artifact check), and that every committed test suite is
wired into exactly one stage. Mutation probes prove the assertions are
non-vacuous.
2026-08-30 06:12:25 +00:00
implementer e6d28f94f0 feat: add dependency-free formatting/lint policy suite and root lint script (E00-S05-T01)
The formatting-policy suite (tests/formatting-policy.test.mjs) locks in the
workspace formatting and lint policy: LF line endings, no BOM, no trailing
whitespace, no tab indentation, exactly one final newline, and valid JSON
with 2-space indentation and no duplicate keys. Every rule has a mutation
probe. The root `lint` script runs the suite; the CI formatting-lint stage
executes it.
2026-08-30 06:12:25 +00:00
implementer ffda249617 docs: document HOST validation and bind control (E00-S04-T04)
CI / Frozen lockfile install (pull_request) Successful in 54s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 34s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 30s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 1m16s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 54s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 46s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m6s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m2s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m1s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 50s
CI / Env adapter owns process.env (E00-S04-T04) (pull_request) Successful in 1m5s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 26s
The non-container guide now notes that HOST is validated at the adapter
boundary as a hostname/IP (invalid values fail startup naming the field) and
that the server passes config.host to server.listen, so a configured HOST
binds exactly that interface and the startup log reflects the actual bind.
The config-env-adapter CI job comment is refreshed to describe the extended
suite (HOST validation + loopback-only boot probe).
2026-08-30 04:42:06 +00:00
implementer 408f33e4e9 test: lock in HOST validation and bind-interface control (E00-S04-T04)
Extend the env-adapter suite to the issue's reworked acceptance criteria:
- static assertions: the adapter resolves HOST via resolveHost (hostname/IP at
  the adapter boundary) and the server passes config.host to server.listen
- deterministic boundary probe: valid HOST forms (IPv4/IPv6/hostname) pass,
  invalid HOST forms throw ConfigStartupError naming host
- boot probes: HOST=127.0.0.1 binds loopback only (no answer on a
  non-loopback interface) with the startup log reflecting the actual bind;
  an invalid HOST exits non-zero naming the field without echoing the raw
  value
- mutation probes: bypassing resolveHost or dropping config.host from
  server.listen both fail
- config-startup-error: update the order-asserion mutation probe for the new
  server.listen(config.port, config.host, ...) signature
2026-08-30 04:42:03 +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 73a1ae88cd chore: drop unused mkdirSync import in the env-adapter suite (E00-S04-T04)
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 55s
CI / Frozen lockfile install (pull_request) Successful in 46s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 28s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 44s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 1m5s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m32s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m41s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m10s
CI / Env adapter owns process.env (E00-S04-T04) (pull_request) Successful in 1m12s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 1m6s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 35s
2026-08-30 04:18:11 +00:00
implementer b9345e205c docs: document the environment adapter in the non-container guide (E00-S04-T04)
CI / Frozen lockfile install (pull_request) Successful in 46s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 24s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 32s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 50s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 48s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 45s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m20s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m20s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m11s
CI / Env adapter owns process.env (E00-S04-T04) (pull_request) Successful in 1m48s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 1m21s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 31s
2026-08-30 04:17:22 +00:00
implementer 3214807c9d test: update suites locked to the old direct process.env wiring for the adapter (E00-S04-T04) 2026-08-30 04:17:22 +00:00
implementer 20173a8241 test: lock in the env adapter as the single owner of process.env with static scan, mutation and deterministic probes (E00-S04-T04) 2026-08-30 04:17:22 +00:00
implementer 1214a33adb feat: server loads all settings through the config environment adapter (E00-S04-T04) 2026-08-30 04:17:19 +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 60bd097b70 test: lock in secret redaction from logs with static, mutation and deterministic probes (E00-S04-T03) 2026-08-30 03:52:55 +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 873264004a docs: document the required EPPP_SESSION_SECRET and the startup error in the non-container guide (E00-S04-T02)
CI / Frozen lockfile install (pull_request) Successful in 45s
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 advisory lock (E00-S03-T04) (pull_request) Successful in 43s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 42s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m1s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 46s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m6s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 48s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 27s
2026-08-30 03:00:47 +00:00
implementer 1a9fd592d9 test: lock in the field-specific startup error with static, mutation and deterministic probes (E00-S04-T02)
- 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
2026-08-30 03:00:45 +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 5eff1580ac docs: document the config package in the non-container guide (E00-S04-T01)
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 31s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 41s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 42s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 46s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 57s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 49s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 26s
2026-08-30 02:29:52 +00:00
implementer ce0d3c0d44 test: lock in the config schema with static, mutation and deterministic probes (E00-S04-T01)
tests/config-schema.test.mjs covers both acceptance criteria: the schema
is defined with TypeBox/Ajv (static assertions on the committed package —
golden-tuple exact pins, Type.Object schema, Ajv compile, boundary
re-exports — each backed by a mutation probe proving non-vacuity) and the
schema covers the validated config fields (host, port, databaseUrl,
sessionSecret with their constraints). The deterministic probe executes
the issue's test plan — 'validate a full config against the TypeBox/Ajv
schema' — against the committed schema through Ajv via Node type
stripping (no build step), plus the negative cases (missing required field
naming sessionSecret, secret too short, unknown property, port bounds, and
empty databaseUrl); when the package is built (as in the CI job) it also
exercises the compiled validateConfig boundary exactly as the later
adapter will consume it.
2026-08-30 02:29:52 +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 5b0bded4b4 docs: document the readiness gate in the non-container guide (E00-S03-T06)
CI / Frozen lockfile install (pull_request) Successful in 49s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 23s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 40s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 56s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 41s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 55s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
2026-08-30 01:56:22 +00:00
implementer c20110be15 test: lock in the app readiness gate with static, deterministic and real-stack probes (E00-S03-T06) 2026-08-30 01:56:22 +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 52190d082c test: lock in the migration failure diagnostic (E00-S03-T05)
CI / Frozen lockfile install (pull_request) Successful in 44s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 28s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 44s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 51s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Failing after 51s
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.
2026-08-30 01:17:23 +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 a2b98439e9 test: lock in the migration advisory lock with static + real-stack probes (E00-S03-T04)
CI / Frozen lockfile install (pull_request) Successful in 56s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 25s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 24s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 49s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 53s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
2026-08-30 00:13:46 +00:00
implementer 3f80063303 feat: add migration advisory lock to database-postgres (E00-S03-T04) 2026-08-30 00:08:57 +00:00
implementer e52b19b1e7 test: lock in the migration ledger with static + real-stack probes (E00-S03-T03)
CI / Frozen lockfile install (pull_request) Successful in 56s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 25s
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 / Compose config (E00-S03-T01) (pull_request) Successful in 29s
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.
2026-08-29 23:20:00 +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 97c5306768 test: lock in pg/Kysely import isolation to database-postgres (E00-S03-T02)
CI / Frozen lockfile install (pull_request) Successful in 43s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 30s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 31s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 24s
- database-postgres-imports.test.mjs: static scan of every workspace package
  source proves pg/kysely import specifiers resolve only to
  packages/database-postgres; owner manifest pins the driver and no other
  package declares it; mutation probes prove the scan catches a driver import
  injected into apps/server/src/index.ts; comment-stripping and specifier
  matcher unit probes; CI-enforcement assertion
- workspace-layout / workspace-config / strict-tsconfig / typescript-pin:
  package-set fixtures updated to include packages/database-postgres
- docs/development/non-container.md: workspace package table and build
  expectations updated for the new package
2026-08-29 11:14:15 +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 a78ffea4c5 test: lock in the PostgreSQL 18.6 container criteria (E00-S03-T01)
CI / Frozen lockfile install (pull_request) Successful in 43s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 29s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 24s
- compose-config.test.mjs: assertDbService requires the pinned
  postgres:18.6-bookworm image; new docker-gated real-stack probe starts the
  stack and asserts SHOW server_version exposes 18.6; mutation probe proves
  reverting to a floating major tag fails the criterion; parser probe updated
- build-targets.test.mjs: db-service mutation fixture updated to the pinned
  image tag
2026-08-29 10:45:00 +00:00
implementer 4cd68c9193 feat: pin the database container to PostgreSQL 18.6 (E00-S03-T01)
- 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)
2026-08-29 10:45:00 +00:00
implementer 719fb4380b test: plant nested marker files in the layer-scan probe (E00-S02-T08)
CI / Frozen lockfile install (pull_request) Successful in 52s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 38s
The Docker-gated probe only planted a root-level .env.t08-* marker, which no
Dockerfile COPY instruction ever copies — so it could not observe a nested
build-context leak in the image layers. Plant additional marker files at
nested paths the Dockerfile's COPY apps/server apps/server would sweep into
the build-stage image (apps/server/.env.t08-*, apps/server/secrets/t08-*.pem)
so the end-to-end scan actually verifies the 'any depth' exclusion
guarantee, not just the root form.
2026-08-29 01:53:57 +00:00
implementer f00c13d57a fix: make .dockerignore exclusions apply at any depth (E00-S02-T08)
CI / Frozen lockfile install (pull_request) Successful in 52s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 36s
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.
2026-08-29 01:49:07 +00:00
implementer b3ad55efe8 test: lock in no-secrets-in-image criteria (E00-S02-T08)
CI / Frozen lockfile install (pull_request) Successful in 50s
tests/secrets-not-embedded.test.mjs: static assertions that the Dockerfile
embeds no secrets (no secret-bearing ARG/ENV, no secret-path or blanket COPY)
and .dockerignore excludes env/credential files; non-vacuous mutation probes
for every assertion; Docker-gated probe that builds the image with a marker
env file in the context and scans every layer + image config for secret
values.
2026-08-29 01:28:26 +00:00
implementer 0938da8a73 feat: keep secrets out of the app image (E00-S02-T08)
- .dockerignore: exclude env + credential files (.npmrc, .netrc, .aws, .ssh,
  secrets/, *.pem, *.key, *.p12, *.pfx, *.jks, id_rsa, id_ed25519, ...) from
  the build context so a local secret file cannot be embedded in the image
- Dockerfile: document the T08 guarantee (no secret ARG/ENV, fixed non-secret
  COPY paths, runtime credentials via Compose environment)
- compose.yaml: T08 in scope; runtime credentials stay in service environment,
  never in the image
2026-08-29 01:28:26 +00:00
implementer 41428b083e test: lock in read-only rootfs criteria (E00-S02-T06)
CI / Frozen lockfile install (pull_request) Successful in 56s
2026-08-29 00:53:40 +00:00
implementer 7ce3ba53cf feat: make the app root filesystem read-only (E00-S02-T06) 2026-08-29 00:53:40 +00:00
implementer 6e8ba388a6 test: lock in non-root execution criteria (E00-S02-T05)
CI / Frozen lockfile install (pull_request) Successful in 51s
tests/non-root-user.test.mjs locks in both acceptance criteria: a static
assertion that the Dockerfile runtime stage declares a non-root USER (not
root/uid 0, 'USER node' exactly), non-vacuous mutation probes, and a
Docker-gated real-stack probe that starts the stack and asserts 'id -u' and
'id -un' inside the running app container report a non-root user, with the
health endpoint still answering as a regression guard.
2026-08-29 00:41:16 +00:00
implementer a4cf365098 feat: run the app image as a non-root user (E00-S02-T05)
The runtime stage of apps/server/Dockerfile now drops root privileges with
'USER node' — the non-root user (uid/gid 1000) the official Node image ships
with — so the app container does not run with root privileges. The server
binds port 3000 (>= 1024) and only reads the root-owned files copied above,
so no extra user creation or ownership changes are required. compose.yaml
header updated: T05 is in scope; T06 (read-only rootfs) and T07 (multi-arch)
remain out of scope.
2026-08-29 00:41:10 +00:00
implementer b51af02d06 test: lock in DB volume persistence criteria (E00-S02-T04)
CI / Frozen lockfile install (pull_request) Successful in 45s
compose-config: assert the db service mounts the named db-data volume at
the PostgreSQL data directory and the top-level volumes map declares it;
non-vacuous mutation probes (missing mount, missing volume declaration,
wrong mount target all fail); Docker-gated real-stack probe writes a
fixture row and asserts it survives `docker compose restart` (restart)
and `docker compose down` + `up -d` (recreate), cleaned up with
`docker compose down -v`.
2026-08-29 00:32:15 +00:00
implementer 3dd80f91fe feat: persist the database volume across restart/recreate (E00-S02-T04)
Mount the named `db-data` volume at PostgreSQL's data directory
(/var/lib/postgresql/data) on the db service and declare it in the
top-level volumes map, so the database survives `docker compose
restart` and `docker compose down` + `up -d` (recreate). Reset with
`docker compose down -v` per the issue rollback note. Header comments
in compose.yaml and the server Dockerfile updated: T04 is no longer out
of scope; T05/T06 remain.
2026-08-29 00:32:15 +00:00
implementer dfb7b0e952 fix: handle quoted scoped keys in frozen-install probe (E00-S02-T03)
CI / Frozen lockfile install (pull_request) Successful in 50s
2026-08-29 00:23:27 +00:00
implementer f7257f9258 test: lock in app health endpoint criteria (E00-S02-T03) 2026-08-29 00:21:34 +00:00
implementer 9c049ce6b5 feat: app health endpoint succeeds (E00-S02-T03) 2026-08-29 00:21:34 +00:00
implementer 59a102bee3 feat: PostgreSQL health gates app start (E00-S02-T02)
CI / Frozen lockfile install (pull_request) Successful in 47s
2026-08-28 23:59:45 +00:00
implementer 3bbea68e6c fix: align app/db image bases with documented stack (E00-S02-T01)
CI / Frozen lockfile install (pull_request) Successful in 48s
Switch the app image from node:24-alpine to node:24.19.0-bookworm-slim in
both build and runtime stages (Technology-Stack 5.4: glibc Debian base
required because argon2 is a native dependency; musl/Alpine causes
native-module build surprises), and the db image from postgres:16-alpine
to postgres:18-bookworm (Technology-Stack 5.2/5.4/6.2 + golden tuple 7
pin PostgreSQL 18.6; 18-bookworm is the 18.x line on Debian bookworm).

Update tests/compose-config.test.mjs so the committed assertions lock in
the corrected image bases (db image, Dockerfile build/runtime stages,
parser probe).
2026-08-28 23:45:18 +00:00
implementer 3be575818d feat: docker compose up -d starts DB + app (E00-S02-T01)
CI / Frozen lockfile install (pull_request) Successful in 43s
2026-08-28 23:35:05 +00:00
implementer 27851fcaeb test: lock in no core package imports a concrete extension (E00-S01-T14)
CI / Frozen lockfile install (pull_request) Successful in 47s
2026-08-28 23:24:02 +00:00
implementer ca9e0ffaf6 test: lock in clean-clone frozen lockfile install (E00-S01-T13)
CI / Frozen lockfile install (pull_request) Successful in 45s
2026-08-28 23:10:26 +00:00
implementer 5c6ca444d9 test: lock in root build/test/typecheck commands (E00-S01-T12)
CI / Frozen lockfile install (pull_request) Successful in 44s
2026-08-28 22:56:48 +00:00
implementer 729a67b79d test: lock in apps/packages/extensions workspace layout separation (E00-S01-T11)
CI / Frozen lockfile install (pull_request) Successful in 44s
2026-08-28 22:43:01 +00:00
implementer 4b5519465f test: lock in strict base tsconfig for all packages (E00-S01-T10)
CI / Frozen lockfile install (pull_request) Successful in 45s
2026-08-28 21:46:57 +00:00
implementer 628885ae0f test: lock in TypeScript 6.0.3 exact dependency (E00-S01-T09)
CI / Frozen lockfile install (pull_request) Successful in 44s
2026-08-28 21:33:52 +00:00
implementer 6323e486bc docs: document enforced Node 24.x engine restriction (E00-S01-T08)
CI / Frozen lockfile install (pull_request) Successful in 43s
2026-08-28 09:15:09 +00:00
implementer 7c548ac43b test: lock in Node 24.x engine restriction (E00-S01-T08) 2026-08-28 09:15:09 +00:00
implementer d405ee5cfa feat: restrict Node engine to 24.x (E00-S01-T08) 2026-08-28 09:15:09 +00:00
implementer 23a574d6af test: lock in committed pnpm 11.23.0 workspace config (E00-S01-T07)
CI / Frozen lockfile install (pull_request) Successful in 48s
2026-08-28 09:03:50 +00:00
implementer 21485aed44 feat(server): add start script for compiled entrypoint (E00-S01-T06)
CI / Frozen lockfile install (pull_request) Successful in 48s
2026-08-28 08:52:03 +00:00
implementer 72c53494a1 docs: add local non-container developer path guide (E00-S01-T06) 2026-08-28 08:52:03 +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 4d0df92290 feat: add dependency-boundary rule and architecture import test (E00-S01-T04)
CI / Frozen lockfile install (pull_request) Successful in 38s
Adds a declarative dependency-boundary rule (dependency-boundaries.json)
classifying workspace packages into apps/packages/extensions groups and
forbidding packages/* (core) from importing extensions/* (concrete
extensions); core depends only on extension contracts, never concrete
extensions.

Adds tests/architecture-import.test.mjs (node:test, zero new dependencies,
lockfile untouched) that loads the rule, walks every workspace package's
source and resolves each import/export/require specifier (bare workspace
names, relative paths, dynamic import(), require(), export-from) to a
group, failing on any forbidden core -> extension edge. Comment-aware
scanning avoids false positives; line numbers in violation reports map to
the original source. Synthetic negative cases prove detection (bare name,
relative path, export-from, dynamic import, require), while the current
compliant graph passes with zero violations.

Verified: 10/10 tests pass; injecting a real core -> extension import makes
the integration test fail with a per-file/line violation report; pnpm 11.23.0
install --frozen-lockfile passes unchanged (CI frozen-install job stays green).

Closes #157
2026-08-28 08:30:34 +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
implementer e186faeb44 Add root .gitignore for local env files and node_modules
CI / Run tests (pull_request) Successful in 29s
CI / Secret scan (gitleaks) (pull_request) Successful in 57s
Ignores .env, .env.*, and node_modules/ at any depth so local secrets and dependency directories can never be committed accidentally. Single-file hygiene change; no tracked files affected. Closes #19.
2026-08-26 19:34:37 +00:00
implementer 32c81d8b91 test: add 3xx-redirect failure and no-credential-header/URL checks
CI / Run tests (pull_request) Successful in 16s
CI / Secret scan (gitleaks) (pull_request) Failing after 13s
Extends the newsletter suite with two boundary cases: redirects (301/302/307/
308) must be treated as failures with the user-safe error, and the POST must
carry no credential of any kind — no api-key/auth-token/cookie headers, and no
token/secret in the body or URL.
2026-08-26 11:31:54 +00:00
implementer 86aa3afbbf refactor: newsletter signup posts no credential (SEC-14-R1 option a)
CI / Secret scan (gitleaks) (pull_request) Failing after 12s
CI / Run tests (pull_request) Successful in 15s
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.
2026-08-26 11:27:11 +00:00