feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,54 @@
# DESIGN — F-003 HTTP foundation and request context
## Architecture
All cross-cutting HTTP behavior lives in explicit, readable code:
- `src/infrastructure/logging/logger.ts` — pino factory. JSON only, level from
`LOG_LEVEL` (default `info`), base field `service`. Accepts an optional writable
destination so tests can capture output without transports (no magic).
- `src/shared/errors.ts``AppError` class (statusCode, code, message) and the
single `errorEnvelope(statusCode, code, message, requestId)` builder. Envelope v2:
`{ error: { statusCode, code, message }, requestId }`.
- `src/shared/http-input.ts``parseJson(schema, input)` using zod. On failure
throws `AppError(400, VALIDATION_ERROR)` carrying field-level issues (field paths
only, never internal state).
- `src/app/build-app.ts` — composition root wires everything explicitly:
- Fastify `genReqId`: trust incoming `x-request-id` only if it matches
`^[A-Za-z0-9._-]{1,128}$`; otherwise generate `crypto.randomUUID()`.
- `onRequest` hook: set `x-request-id` response header, start timer.
- `onResponse` hook: one JSON log line per request
`{ requestId, method, url, statusCode, durationMs }`.
- `setErrorHandler`: map `AppError` and Fastify errors to the envelope. 4xx expose
their message; >=500 always returns generic `Internal Server Error`. Errors are
logged with stack server-side, tagged with requestId.
- `setNotFoundHandler`: envelope with code `NOT_FOUND` + requestId.
- `src/infrastructure/http/server.ts` — startup log lines through the logger.
## Request id policy
- Propagation is opt-in and sanitized; anything suspicious is replaced by a fresh UUID.
- Response header name: `x-request-id`.
## Validation policy
- Validation is explicit per route: call `parseJson(schema, body)` inside the handler.
- No schema decorators, no magic body binding. The F-003 deliverable is the hook
(helper + envelope integration), exercised by composition tests with a test-only
route registered on the built app.
## Backward compatibility
- `GET /health` response body unchanged; envelope gains fields (additive).
## Files
| File | Role |
|---|---|
| src/infrastructure/logging/logger.ts | pino factory |
| src/shared/errors.ts | AppError + envelope v2 |
| src/shared/http-input.ts | zod parse helper |
| src/app/build-app.ts | hooks, genReqId, handlers |
| src/infrastructure/http/server.ts | startup logging |
| src/app/tests/http-foundation.test.ts | composition tests (request id, logs, envelope, no leak) |
| src/shared/tests/http-input.test.ts | helper unit tests |
## Risks / mitigations
- Double logging (fastify internal + hooks): keep `logger: false` on Fastify, log only via explicit hooks.
- Header injection through propagated id: strict regex + length cap.
- 5xx leaking internals: handler returns fixed generic message for anything >=500; tests assert absence of thrown message/stack.

View File

@@ -0,0 +1,37 @@
# SPEC — F-003 HTTP foundation and request context
## Problem
Every request needs identity, structured logs and predictable errors. Today the app
answers with ad-hoc console logging potential, no request correlation id, and an
error envelope without request context.
## Goal
request_id everywhere, JSON logs, one error envelope, no magic.
## Scope IN
- request_id generated or propagated on every request
- Structured JSON logging with request_id
- Single error response envelope (v2: adds stable `code` + `requestId`)
- Input validation hook on API layer (foundation helper, explicit usage)
## Scope OUT
- No tracing backend
- No metrics backend
- No production endpoints that consume validation yet (first consumer: F-005 identity)
## Acceptance criteria
1. Every response carries `request_id` (via `x-request-id` response header; error bodies also embed it).
2. Every log line for a request carries the same request_id.
3. Given an invalid request When handled Then the error envelope shape is stable.
4. Internal stack traces never leak to API responses (5xx message is generic; stack stays server-side in logs).
5. `./scripts/verify.sh` green.
## Non-functional
- JSON-only logs (no pretty printers in runtime).
- Incoming `x-request-id` must be sanitized before reuse (length/charset limits).
- No new framework magic: logging hooks and error handler are explicit code in the composition root.
## Dependencies added
- pino (JSON structured logging; Fastify ecosystem standard)
- zod (explicit input validation; no decorators/annotations magic)
Justification goes to `spec/tech.md`.

View File

@@ -0,0 +1,11 @@
# TASKS — F-003 HTTP foundation and request context
- [ ] TASK-001 deps: add pino + zod; justify both in spec/tech.md
- [ ] TASK-002 src/infrastructure/logging/logger.ts: createLogger({ level?, destination? })
- [ ] TASK-003 src/shared/errors.ts: AppError + errorEnvelope v2 (code + requestId)
- [ ] TASK-004 src/shared/http-input.ts: parseJson(schema, input) -> AppError(400, VALIDATION_ERROR)
- [ ] TASK-005 src/app/build-app.ts: genReqId sanitization, onRequest/onResponse hooks, error + notFound handlers with requestId; buildApp(deps) injectable logger
- [ ] TASK-006 src/infrastructure/http/server.ts: logger-based startup lines
- [ ] TASK-007 tests: src/shared/tests/http-input.test.ts
- [ ] TASK-008 tests: src/app/tests/http-foundation.test.ts (id propagation/sanitization, log correlation, envelope stability incl. malformed JSON, no stack leak)
- [ ] TASK-009 run lint + boundaries + typecheck + build + test + verify.sh

View File

@@ -0,0 +1,21 @@
# TESTS — F-003 HTTP foundation and request context
## Unit — src/shared/tests/http-input.test.ts
1. parseJson returns parsed value for valid input
2. parseJson throws AppError 400 VALIDATION_ERROR with field issues for invalid input
3. parseJson rejects non-object input when schema expects object
## Composition — src/app/tests/http-foundation.test.ts
(app built with logger writing to an in-memory stream)
1. GET /health responds 200 with x-request-id header (UUID shape)
2. Valid incoming x-request-id is propagated: same value in response header and in the request log line
3. Malicious x-request-id (bad charset / >128 chars) is replaced by generated UUID; log line carries the generated id
4. Unknown route -> 404 envelope { error: { statusCode: 404, code: NOT_FOUND, message }, requestId } + x-request-id header
5. Invalid JSON body on test route -> 400, envelope shape stable, requestId present
6. Schema-invalid body on test route -> 400 code VALIDATION_ERROR, issues mention field path only
7. Handler throwing Error('secret internal detail') -> 500 generic message, response body contains neither the message nor a stack; error log line contains requestId and stack
8. Every request produces exactly one 'request completed' log line carrying the requestId
## Manual / QA
- verify.sh green; live curl shows x-request-id header and JSON envelope on unknown route.