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.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@personal-blog/config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "EPPP configuration service. Owns the TypeBox/Ajv configuration schema (E00-S04-T01); the environment adapter (E00-S04-T04), field-specific startup errors (E00-S04-T02), secret redaction (E00-S04-T03) and the .env.example template (E00-S04-T05) land in later tasks.",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.34.52",
|
||||
"ajv": "8.20.0"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @personal-blog/config — EPPP configuration service.
|
||||
*
|
||||
* [E00-S04-T01] TypeBox/Ajv schema: the package boundary exposes the
|
||||
* configuration schema (`configSchema`, defined with TypeBox) and the Ajv
|
||||
* schema-validation entry point (`validateConfig`). 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.
|
||||
*/
|
||||
|
||||
export { configSchema } from './schema.js';
|
||||
export type { Config } from './schema.js';
|
||||
export { validateConfig } from './validate.js';
|
||||
export type { ConfigValidationResult } from './validate.js';
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* EPPP configuration schema — [E00-S04-T01] TypeBox/Ajv schema.
|
||||
*
|
||||
* The single source of truth for the **validated configuration fields** of
|
||||
* the EPPP server (the `@personal-blog/config` package). The schema is
|
||||
* defined with TypeBox (`@sinclair/typebox`, golden-tuple pin 0.34.52 —
|
||||
* Technology-Stack §5.2/§7) and validated with Ajv (`ajv`, golden-tuple pin
|
||||
* 8.20.0) via `validateConfig` (see `validate.ts`). E00-S04-T02
|
||||
* (field-specific startup errors), T03 (secret redaction), T04 (the
|
||||
* `process.env` adapter) and T05 (`.env.example` placeholders) build on this
|
||||
* schema; this module only defines it.
|
||||
*
|
||||
* The validated config fields:
|
||||
*
|
||||
* - `host` — the interface the HTTP server binds. Default `0.0.0.0` (the
|
||||
* committed server binds all interfaces today, E00-S02-T03). Environment
|
||||
* source: `HOST`.
|
||||
* - `port` — the port the HTTP server listens on. Integer in the valid TCP
|
||||
* port range (1–65535), default `3000` (the container default, matching
|
||||
* the Dockerfile `EXPOSE 3000` and the compose `:3000` container port;
|
||||
* `PORT` is read today, E00-S02-T03).
|
||||
* - `databaseUrl` — the PostgreSQL connection string (the `pg` `Pool`
|
||||
* `connectionString`, E00-S03-T02). Optional: when absent the app has no
|
||||
* startup migration run to wait for and reports ready immediately (the
|
||||
* local non-container developer path, E00-S01-T06/E00-S03-T06). Must be
|
||||
* non-empty when present. Environment source: `DATABASE_URL`.
|
||||
* - `sessionSecret` — the admin-session secret (`EPPP_SESSION_SECRET`, the
|
||||
* required secret per Security-and-Operations §32/§26). **Required** and at
|
||||
* least 32 characters: it is the story's secret field (E00-S04-T03 redacts
|
||||
* it from logs) and the schema's required field (E00-S04-T02 reports a
|
||||
* missing required setting). No default — a secret must never be invented
|
||||
* by the schema.
|
||||
*
|
||||
* The object is closed (`additionalProperties: false`) so a typo'd or
|
||||
* unexpected setting is rejected loudly instead of silently ignored.
|
||||
*/
|
||||
|
||||
import { Type, type Static } from '@sinclair/typebox';
|
||||
|
||||
/** The EPPP configuration schema — validates the parsed configuration object. */
|
||||
export const configSchema = Type.Object(
|
||||
{
|
||||
host: Type.Optional(Type.String({ default: '0.0.0.0' })),
|
||||
port: Type.Optional(Type.Integer({ minimum: 1, maximum: 65535, default: 3000 })),
|
||||
databaseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
||||
sessionSecret: Type.String({ minLength: 32 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** The validated configuration type — `Static` of `configSchema`. */
|
||||
export type Config = Static<typeof configSchema>;
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* EPPP configuration validation — [E00-S04-T01] TypeBox/Ajv schema.
|
||||
*
|
||||
* Compiles the TypeBox `configSchema` with Ajv (the golden-tuple validator,
|
||||
* Technology-Stack §5.2/§7) and exposes `validateConfig`, the generic
|
||||
* schema-validation entry point: given an unknown value it reports whether
|
||||
* the value is a valid configuration and the Ajv error messages otherwise.
|
||||
*
|
||||
* This is deliberately NOT the E00-S04-T02 field-specific startup error:
|
||||
* `validateConfig` returns the raw schema-validation outcome (valid or not,
|
||||
* with the Ajv messages) and performs no startup wiring — the adapter
|
||||
* (E00-S04-T04) and the startup error formatting (E00-S04-T02) build on it
|
||||
* in later tasks.
|
||||
*/
|
||||
|
||||
import { Ajv } from 'ajv';
|
||||
|
||||
import { configSchema } from './schema.js';
|
||||
|
||||
/** Ajv instance for the config schema — `allErrors` reports every violation. */
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
|
||||
/** The compiled validator — TypeBox schemas are JSON Schema, so Ajv compiles them directly. */
|
||||
const validateConfigValue = ajv.compile(configSchema);
|
||||
|
||||
/** The outcome of validating a value against the config schema. */
|
||||
export interface ConfigValidationResult {
|
||||
/** True when the value is a valid configuration. */
|
||||
valid: boolean;
|
||||
/** Ajv error messages, empty when `valid` is true. */
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an unknown value against the config schema.
|
||||
*
|
||||
* @param value - the value to validate (typically the parsed config object)
|
||||
* @returns `{ valid: true, errors: [] }` for a valid configuration, or
|
||||
* `{ valid: false, errors }` with the Ajv messages naming each violation
|
||||
* (e.g. `"must have required property 'sessionSecret'"`).
|
||||
*/
|
||||
export function validateConfig(value: unknown): ConfigValidationResult {
|
||||
const valid = validateConfigValue(value);
|
||||
if (valid) {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
const errors = (validateConfigValue.errors ?? []).map((error) => error.message ?? 'invalid');
|
||||
return { valid: false, errors };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user