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.
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`.
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).
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
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.
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.
Covers the issue test plan: form renders and posts to the endpoint; token is
read from config, never hardcoded in source; a failed-token response shows a
user-safe error without leaking the secret; confirmation on success.
All reading list links point at external http(s) URLs, so each rendered
anchor now carries rel="noopener noreferrer" as a hardening best
practice. Renderer test updated for the new attribute and asserts every
rendered link carries it.
Covers: page wiring and nav reachability, data-file integrity, grouped
rendering, valid hrefs, optional notes, HTML escaping, invalid-entry
skipping, data-only extensibility, and a 500-entry fixture (counts,
grouping, and a generous render-time bound).