[E00-S04-T01] TypeBox/Ajv schema #396
No Reviewers
Labels
Clear labels
agent/analyst-drafted
agent/analyst-drafted
needs/human-decision
needs/human-decision
needs/security-review
needs/security-review
tier/t0
tier/t1
tier/t2
tier/t3
kind
bug
kind
bug
kind
epic
kind
epic
kind
initiative
EPPP programme initiative
kind
story
kind
story
kind
task
EPPP engineering card/task decomposed from a story
kind
toil
kind
toil
loop
1
loop
1
loop
2
loop
2
loop
3
loop
3
risk
agent-full
risk
agent-full
risk
human-gated
risk
human-gated
risk
human-only
risk
human-only
size
l
size
l
size
m
size
m
size
s
size
s
status
blocked
status
blocked
status
done
Workflow: Done
status
in-progress
status
in-progress
status
proposed
status
proposed
status
ready
status
ready
status
review
status
review
stream
checkout
stream
checkout
stream
onboarding
stream
onboarding
stream
platform
stream
platform
trivial — implementer only, auto-merge
standard — implementer + reviewer + tester
complex — security if triggered, human merge
critical — full chain + security, human merge
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Fabrika/PersonalBlog#396
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What changed
Implements [E00-S04-T01] TypeBox/Ajv schema (#182): the configuration schema for the EPPP configuration service is now defined with TypeBox and validated with Ajv, in a new
packages/configworkspace package (@personal-blog/config— theconfigpackage of the planned repository architecture, Architecture wiki §9).packages/config— new configuration-service package (@personal-blog/config):src/schema.tsdefinesconfigSchemawith TypeBox (Type.Object, golden-tuple pin@sinclair/typebox@0.34.52) covering the validated config fields:host(optional string, default0.0.0.0— the bind address),port(optional integer 1–65535, default3000— the existingPORT),databaseUrl(optional non-empty string — the existingDATABASE_URL; absent = the local non-container no-database path),sessionSecret(required, ≥ 32 chars — the story's secret field,EPPP_SESSION_SECRETper Security-and-Operations §32/§26), withadditionalProperties: falseso an unexpected setting is rejected loudly.src/validate.tscompiles the schema with Ajv (ajv, golden-tuple pin8.20.0,allErrors: true) and exportsvalidateConfig(value): { valid, errors }— the generic schema-validation entry point;src/index.tsis the package boundary re-exporting both plus theConfig(Static<typeof configSchema>) andConfigValidationResulttypes. Nothing readsprocess.envand nothing is wired into startup yet — the environment adapter (E00-S04-T04), field-specific startup errors (E00-S04-T02) and secret redaction (E00-S04-T03) build on this boundary in later tasks, as the brief requires.pnpm-lock.yaml— importer + resolved tree:packages/configimporter plus the exact-pinned@sinclair/typebox@0.34.52/ajv@8.20.0dependency graph (ajv's transitive tree: fast-deep-equal, fast-uri, json-schema-traverse, require-from-string), generated by pnpm 11.23.0;pnpm install --frozen-lockfilepasses.apps/server/Dockerfile: the build stage now copiespackages/config/package.jsonso the frozen in-image install matches the lockfile importers exactly (the server itself does not depend on config yet — no source/dist copy needed)..gitea/workflows/ci.yml— newconfig-schemajob runsnode --test tests/config-schema.test.mjson every PR (installs the frozen workspace and builds the config package, since the probe also exercises the compiled package boundary). Additive only (no existing job modified, same action majors, nosecrets:context, no untrusted interpolation).workspace-layout,workspace-config,strict-tsconfig,typescript-pin(layout ↔ lockfile parity, strict-base compile, pinned-TS resolution).tests/config-schema.test.mjs— new suite locking in both acceptance criteria (details in the criterion → test table): static assertions on the committed package (golden-tuple exact pins, TypeBox schema shape, Ajv compile, boundary re-exports, CI wiring), each backed by mutation probes proving non-vacuity (removing a field, making the secret optional, relaxing the minLength, opening the object, replacing TypeBox/Ajv with hand-rolled code all fail); a deterministic probe executes the issue's test plan — "validate a full config against the TypeBox/Ajv schema" — against the committedsrc/schema.tsthrough Ajv via Node type stripping (no build step), plus the negative cases (missing required field namingsessionSecret, secret too short, unknown property, port out of range / not an integer, emptydatabaseUrl); when the package is built (as in the CI job) the same probe exercises the compiledvalidateConfigboundary exactly as the later configuration adapter will consume it.docs/development/non-container.md: workspace package table and build expectations updated for the new package..gitignore:.config-schema-probe-*.mjstransient probe files (same pattern as the database-postgres probe files).Explicitly out of scope per the brief, not touched: field-specific startup error (E00-S04-T02), secret redaction (E00-S04-T03),
process.envaccess rule (E00-S04-T04). The server (apps/server/src/index.ts) is unchanged — it still readsPORT/DATABASE_URLdirectly until the T04 adapter lands.Criterion → test table
tests/config-schema.test.mjs— "the config package exists, pins the golden-tuple runtime deps exactly, and builds with tsc" (manifest name@personal-blog/config,dependencies['@sinclair/typebox'] === '0.34.52'anddependencies.ajv === '8.20.0', exactMAJOR.MINOR.PATCH— no ranges); "the configuration schema is defined with TypeBox …" (import { Type, type Static } from '@sinclair/typebox'+export const configSchema = Type.Object(); "the schema is validated with Ajv …" (import { Ajv } from 'ajv',new Ajv({ allErrors: true }),.compile(configSchema),export function validateConfig); "the package boundary re-exports the schema and the validator"; mutation probes "replacing TypeBox with a hand-rolled object …" and "dropping the Ajv compile …" prove the TypeBox/Ajv assertions are non-vacuoustests/config-schema.test.mjs— "the configuration schema is defined with TypeBox and covers the validated config fields" (host optional string default0.0.0.0; port optional integer 1–65535 default3000; databaseUrl optional non-empty string; sessionSecret requiredminLength: 32, neverType.Optional;additionalProperties: false); mutation probes "removing a validated config field …", "making the secret optional …", "relaxing the secret length …", "opening the object …"; deterministic probe — a full config (host, port, databaseUrl, sessionSecret) validates against the committed schema through Ajv; host/port/databaseUrl omitted → valid (defaults / no-database path); missingsessionSecret→ invalid naming the field; short secret, unknown property,port: 65536,port: '3000', emptydatabaseUrl→ all rejectedtests/config-schema.test.mjs— "a full config validates against the committed TypeBox/Ajv schema, and violations are rejected (deterministic probe)": the probe imports the committedpackages/config/src/schema.ts(Node type stripping, no build step) and validates a full config through Ajv; when the package is built (the CI job builds it first) the same probe additionally asserts the compiledvalidateConfigboundary accepts the full config and rejects the missing-secret / unknown-property cases identicallyconfig-schemajobtests/config-schema.test.mjs— "the config-schema criterion is enforced in CI" (root test glob covers the suite;.gitea/workflows/ci.ymlrunsnode --test tests/config-schema.test.mjsand builds@personal-blog/configfirst);.gitea/workflows/ci.yml—config-schemajob (additive, matching the security-reviewed #390/#391/#392/#393/#394/#395 precedent)Test plan executed
node --test tests/config-schema.test.mjs→ 12 tests, 12 pass / 0 fail / 0 skip on Node 22.23.2. The deterministic probe ran for real: a full config validates; missingsessionSecretis rejected naming the field; short secret / unknown property / out-of-range port / non-integer port / emptydatabaseUrlare all rejected; and the compiledvalidateConfigboundary (package built) behaves identically.node --test "tests/**/*.test.mjs"): 210 tests — 186 pass / 9 fail / 15 skip; the 9 failures are the pre-existing Node-22 environment artifacts identical to the base-commit baseline (this sandbox has Node 22 — the workspace engines gate requires Node ≥ 24):tests/frozen-install.test.mjs×5 andtests/root-commands.test.mjs×3 fail onERR_PNPM_UNSUPPORTED_ENGINE(verified:pnpm install --frozen-lockfilepasses under the engine override, and every package compiles undertsc),tests/node-engine.test.mjs×1 asserts the runtime is Node 24.x. CI runs Node 24 where these pass.pnpm install --frozen-lockfilesucceeds against the regeneratedpnpm-lock.yaml(packages/configimporter + typebox/ajv tree).tsc -p … --noEmitexit 0, including the newpackages/config(strict base config, NodeNext, verbatimModuleSyntax — no TS errors under TypeScript 6.0.3).workspace-layout(8),workspace-config(6),strict-tsconfig(5),typescript-pin(3),app-readiness(18 pass / 1 docker-skip),health-endpoint(7),architecture-import(10),no-core-extension-imports(3).Risks / notes
verbatimModuleSyntax,import { Ajv } from 'ajv'is used instead of the default import — ajv 8.20.0 ships CJS with adist/ajv.d.tsdeclaring both a namedAjvclass andexport default; the named import types and runs correctly under Node's ESM-CJS interop (verified behaviorally).src/schema.tsvia Node type stripping (no build needed); the compiled-validateConfigassertions run when the package is built — the CI job builds it first, so they are deterministic in CI and skip gracefully on a clean local clone without a build step.port/databaseUrlare the settings the server already consumes (PORT/DATABASE_URL),sessionSecretis the story's required secret (EPPP_SESSION_SECRET, Security-and-Operations §32), andhostcarries the current hardcoded bind address (0.0.0.0) as its default. T04's adapter mapsprocess.envonto this shape.packages/configand the lockfile importer, revert the Dockerfile manifest copy, theconfig-schemaCI job and the fixture updates (no data migration involved).Refs #182