feat(F-003): HTTP foundation with request context and error envelope
- request_id generated or sanitized-propagated on every request (x-request-id)
- structured JSON logging (pino), one correlated line per request, injectable logger
- error envelope v2 { error: { statusCode, code, message, details? }, requestId }
- 5xx messages always generic; stack traces stay in server logs only
- explicit parseJson (zod) input validation hook at the API layer
- README HTTP contract section; deps justified in spec/tech.md
- all gates approved; verify.sh green
This commit is contained in:
22
work/artifacts/F-003/architect.md
Normal file
22
work/artifacts/F-003/architect.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Architect — F-003 HTTP foundation and request context
|
||||
|
||||
done -> work/artifacts/F-003/architect.md
|
||||
|
||||
## Deliverables
|
||||
- specs/F-003-http-foundation/SPEC.md
|
||||
- specs/F-003-http-foundation/DESIGN.md
|
||||
- specs/F-003-http-foundation/TASKS.md
|
||||
- specs/F-003-http-foundation/TESTS.md
|
||||
|
||||
## Key decisions
|
||||
1. **pino** for JSON structured logging: Fastify ecosystem standard, fast, boring. Fastify's own logger stays OFF (`logger: false`); logging happens through explicit onRequest/onResponse hooks so there is exactly one log line per request and zero hidden log paths.
|
||||
2. **zod** for the input validation hook: explicit `parseJson(schema, body)` calls inside handlers. No decorators, no magic binding. F-003 ships the hook; first real consumer is F-005 identity.
|
||||
3. **request_id policy**: propagate incoming `x-request-id` only when it matches `^[A-Za-z0-9._-]{1,128}$` (header-injection defense); otherwise `crypto.randomUUID()`. Response header `x-request-id` on every response; error envelopes also embed requestId.
|
||||
4. **Error envelope v2** (additive): `{ error: { statusCode, code, message }, requestId }`. Codes: NOT_FOUND, VALIDATION_ERROR, INTERNAL_ERROR, or upstream Fastify error codes for known 4xx. >=500 always answers the generic `Internal Server Error`; stack goes to logs tagged with requestId, never to the client.
|
||||
5. **Logger injection**: `buildApp({ logger? })` — tests capture logs through an in-memory destination. No transports, no env sniffing in tests.
|
||||
|
||||
## Boundaries
|
||||
- Cross-cutting code lives in `shared` and `infrastructure/logging`; composition wiring only in `src/app`. Modules (health) untouched.
|
||||
|
||||
## Open risks
|
||||
- Envelope change touches F-001 test expectations (additive; tests will be updated in build).
|
||||
18
work/artifacts/F-003/documenter.md
Normal file
18
work/artifacts/F-003/documenter.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Documenter — F-003 HTTP foundation and request context
|
||||
|
||||
done -> work/artifacts/F-003/documenter.md
|
||||
|
||||
## Why documentation applies
|
||||
F-003 defines the API-wide HTTP contract (request id, error envelope, validation hook) that every future module consumes.
|
||||
|
||||
## Documents reviewed/produced
|
||||
- `project/README.md`: new "HTTP contract" section — x-request-id policy, envelope shape + codes, no-stack-leak guarantee, parseJson validation pattern, LOG_LEVEL. (Applied via a build-stage bounce because `project/` writes require build/implementer/running.)
|
||||
- `spec/tech.md`: pino + zod justification rows added at build stage.
|
||||
- `specs/F-003-http-foundation/` SPEC/DESIGN/TASKS/TESTS consistent with implementation.
|
||||
|
||||
## Verification
|
||||
- README HTTP contract section matches tested behavior (cross-checked against qa.json live probes and test suite).
|
||||
- Envelope documented as v2 (additive over F-001), codes enumerated.
|
||||
|
||||
## Pending docs (not this ticket)
|
||||
- Per-module validation schemas documented as modules land (F-005+).
|
||||
43
work/artifacts/F-003/implementer.md
Normal file
43
work/artifacts/F-003/implementer.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Implementer — F-003 HTTP foundation and request context
|
||||
|
||||
done -> work/artifacts/F-003/implementer.md
|
||||
|
||||
## Files created
|
||||
- src/infrastructure/logging/logger.ts (pino factory; JSON only; injectable destination for tests)
|
||||
- src/shared/http-input.ts (parseJson zod helper -> AppError 400 VALIDATION_ERROR with field paths)
|
||||
- src/app/tests/http-foundation.test.ts (9 tests: id generation/propagation/sanitization, log correlation, envelope stability, no stack leak, one log line per request)
|
||||
- src/shared/tests/http-input.test.ts (3 tests)
|
||||
|
||||
## Files modified
|
||||
- src/shared/errors.ts: AppError class + error envelope v2 `{ error: { statusCode, code, message, details? }, requestId }` (additive over F-001 shape)
|
||||
- src/app/build-app.ts: genReqId sanitization (^[A-Za-z0-9._-]{1,128}$ else randomUUID), onRequest/onResponse hooks (x-request-id header + one JSON log line per request), explicit setErrorHandler/setNotFoundHandler with requestId; buildApp({ logger? }) injectable
|
||||
- src/infrastructure/http/server.ts: startup/shutdown logging via injected logger
|
||||
- spec/tech.md: pino + zod justification
|
||||
- package.json: deps pino, zod
|
||||
|
||||
## API changes
|
||||
- Every response now carries `x-request-id` header
|
||||
- Error body v2: `{ error: { statusCode, code, message, details? }, requestId }`
|
||||
- GET /health body unchanged
|
||||
|
||||
## Tests passed (evidence)
|
||||
```
|
||||
npm run lint:boundaries -> Boundary check OK: 16 file(s) checked
|
||||
npm run lint -> OK
|
||||
npm run typecheck -> exit 0
|
||||
npm run build -> exit 0
|
||||
npm test -> 6 files passed, 2 skipped (integration, no DB in unit run); 23 passed | 6 skipped
|
||||
live smoke (PORT=3998):
|
||||
GET /health -> 200, x-request-id: 65b38aee-..., body {"status":"ok"}
|
||||
GET /nope -> 404 envelope v2 with requestId matching log line
|
||||
POST /health bad json -> 400; log code FST_ERR_CTP_INVALID_JSON_BODY with same requestId as response
|
||||
server logs: one "request completed" JSON line per request carrying requestId
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
- Fastify 4xx parse-error messages are exposed as-is (e.g. "Unexpected token"); they carry no internals and are standard.
|
||||
- Validation hook is exercised via a test-only route (/__test/echo registered in composition test); first production consumer arrives with F-005.
|
||||
|
||||
## Follow-up work
|
||||
- F-004 config/flags will centralize LOG_LEVEL handling.
|
||||
- F-005 identity first real parseJson consumer.
|
||||
35
work/artifacts/F-003/leader-close.json
Normal file
35
work/artifacts/F-003/leader-close.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"feature_id": "F-003",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "F-003 closed. HTTP foundation in place: request_id everywhere (generated or sanitized-propagated), JSON structured logs correlated per request, stable error envelope v2, explicit validation hook. All gates APPROVED, verify.sh exit 0.",
|
||||
"gates": {
|
||||
"reviewer": "APPROVED (reviewer.json)",
|
||||
"security": "APPROVED (security.json)",
|
||||
"qa": "APPROVED (qa.json)",
|
||||
"verify_sh": "exit 0"
|
||||
},
|
||||
"deliverables": [
|
||||
"src/infrastructure/logging/logger.ts (pino factory, injectable destination)",
|
||||
"src/shared/errors.ts (AppError + error envelope v2)",
|
||||
"src/shared/http-input.ts (parseJson zod hook)",
|
||||
"src/app/build-app.ts (genReqId sanitization, onRequest/onResponse hooks, error+notFound handlers)",
|
||||
"src/infrastructure/http/server.ts (logger-based startup)",
|
||||
"12 new tests across shared + app composition; README HTTP contract section",
|
||||
"spec/tech.md pino+zod justification"
|
||||
],
|
||||
"process_notes": [
|
||||
"Doc stage bounced to build to write README (project/ gated to build/implementer/running). This is the correct guardrail behavior."
|
||||
],
|
||||
"next_feature_hint": "F-004 (config/flags) only depends on F-001; F-005 identity needs F-002+F-003, both now done -> F-005 unblocked",
|
||||
"evidence": [
|
||||
"work/artifacts/F-003/architect.md",
|
||||
"work/artifacts/F-003/implementer.md",
|
||||
"work/artifacts/F-003/reviewer.json",
|
||||
"work/artifacts/F-003/security.json",
|
||||
"work/artifacts/F-003/qa.json",
|
||||
"work/artifacts/F-003/documenter.md",
|
||||
"./scripts/verify.sh exit 0 at close"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:18:00Z"
|
||||
}
|
||||
40
work/artifacts/F-003/qa.json
Normal file
40
work/artifacts/F-003/qa.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"feature_id": "F-003",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "All 5 acceptance criteria verified with fresh live probes and full suites.",
|
||||
"traceability": [
|
||||
{
|
||||
"criterion": "AC1: every response carries request_id",
|
||||
"test": "live curl: GET /health (200) and GET /missing (404) both returned x-request-id UUID headers",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC2: every request log line carries the same request_id",
|
||||
"test": "grep of server JSON logs: each live request_id appears exactly once, in its 'request completed' line",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC3: invalid request -> stable error envelope shape",
|
||||
"test": "live 404 and malformed-JSON 400 bodies share { error: { statusCode, code, message }, requestId }; schema-invalid case covered by unit test (VALIDATION_ERROR + field details)",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC4: internal stack traces never leak",
|
||||
"test": "live bodies contain zero occurrences of 'stack'; unit test proves 500 handler hides thrown message and stack while logging them server-side",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC5: verify.sh green",
|
||||
"test": "./scripts/verify.sh",
|
||||
"result": "PASS (exit 0)"
|
||||
}
|
||||
],
|
||||
"regressions": "PASS - unit 23 passed | 6 skipped; F-002 integration 6 passed; health endpoint body unchanged",
|
||||
"evidence": [
|
||||
"live probes at PORT=3997 with fresh dist build",
|
||||
"npm test / test:integration green",
|
||||
"./scripts/verify.sh exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:16:00Z"
|
||||
}
|
||||
27
work/artifacts/F-003/reviewer.json
Normal file
27
work/artifacts/F-003/reviewer.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature_id": "F-003",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "HTTP foundation matches specs/F-003 DESIGN.md. Explicit hooks, sanitized id propagation, stable envelope v2, no stack leak. No regressions: F-001/F-002 suites fully green including database integration tests.",
|
||||
"checks": {
|
||||
"design_conformance": "PASS: logger factory, AppError/envelope v2, parseJson hook, genReqId + hooks + handlers all exactly as designed; logger: false on Fastify prevents double logging",
|
||||
"security_of_propagation": "PASS: strict regex ^[A-Za-z0-9._-]{1,128}$ before trusting incoming x-request-id; tested against newline injection and oversized values",
|
||||
"no_magic": "PASS: validation is an explicit parseJson call; no decorators/annotations; hooks are plain readable functions in the composition root",
|
||||
"test_coverage": "PASS: 12 new tests map 1:1 to specs/F-003 TESTS.md; test-only route /__test/echo exercises the hook end-to-end without polluting production API",
|
||||
"regression": "PASS: full unit suite 23 passed; F-002 integration suite 6 passed; envelope change verified additive"
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"severity": "info",
|
||||
"note": "Fastify 4xx error codes (e.g. FST_ERR_CTP_INVALID_JSON_BODY) surface in the envelope code field. Public codes, no internals; acceptable and useful for clients."
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
"npm run lint / typecheck / lint:boundaries (16 files) -> exit 0",
|
||||
"npm test -> 6 files passed, 23 passed | 6 skipped",
|
||||
"npm run test:integration -> 2 files, 6 passed (no F-002 regression)",
|
||||
"live smoke at build stage: x-request-id header on 200/404/400, requestId correlated in JSON logs",
|
||||
"files reviewed: src/app/build-app.ts, src/shared/errors.ts, src/shared/http-input.ts, src/infrastructure/logging/logger.ts, src/infrastructure/http/server.ts, both test suites"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:12:00Z"
|
||||
}
|
||||
27
work/artifacts/F-003/security.json
Normal file
27
work/artifacts/F-003/security.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature_id": "F-003",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Security gate passed. Header propagation sanitized against injection, no stack/internal leakage, log injection neutralized by JSON serialization, zero audit vulnerabilities with pino+zod.",
|
||||
"checks": {
|
||||
"dependencies": "PASS: npm audit -> 0 vulnerabilities; pino/zod justified in spec/tech.md",
|
||||
"header_injection": "PASS: incoming x-request-id accepted only under ^[A-Za-z0-9._-]{1,128}$; newline and 129-char payloads replaced by UUID (regression tests present)",
|
||||
"response_leakage": "PASS: 5xx path forces generic 'Internal Server Error'; body assertions confirm absence of thrown message and stack; 4xx exposes only safe messages/codes",
|
||||
"log_injection": "PASS: requestId is pre-sanitized; url/method serialized via pino JSON encoding which escapes control characters",
|
||||
"dangerous_patterns": "PASS: no eval / new Function / child_process in src"
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"severity": "info",
|
||||
"note": "Fastify 4xx codes exposed in envelope (e.g. FST_ERR_CTP_INVALID_JSON_BODY) are public framework codes; no internal state disclosed."
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
"npm audit -> found 0 vulnerabilities",
|
||||
"grep eval|new Function over src -> none",
|
||||
"grep x-request-id non-test usages -> only constant + sanitized generator path",
|
||||
"5xx generic guard verified at build-app.ts lines 68/75",
|
||||
"test 'internal errors never leak message or stack to the client' passes"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:14:00Z"
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
# Sesión actual
|
||||
|
||||
- Feature en curso: _ninguna_ (F-002 cerrada DONE el 2026-08-14)
|
||||
- Feature en curso: _ninguna_ (F-003 cerrada DONE el 2026-08-14)
|
||||
- Inicio: —
|
||||
- Orquestador: —
|
||||
|
||||
## Plan
|
||||
- Features ahora desbloqueadas: F-003 (HTTP foundation), F-004 (config/flags) — dependen solo de F-001. F-005 (identity) tiene sus dos dependencias (F-002, F-003): falta F-003.
|
||||
- Sugerencia de orden: F-003 → F-004 → F-005.
|
||||
- Hechas: F-001, F-002, F-003.
|
||||
- Desbloqueadas ahora: F-004 (config/flags, depende de F-001) y F-005 (identity, depende de F-002+F-003, ambas done).
|
||||
- Sugerencia de orden: F-004 → F-005 (identity necesita config/flags limpio para secrets).
|
||||
|
||||
## Bitácora
|
||||
- 2026-08-14: F-001 DONE y F-002 DONE, todos los gates APPROVED, verify.sh verde.
|
||||
- 2026-08-14: F-001, F-002 y F-003 DONE; todos los gates APPROVED; verify.sh verde.
|
||||
- Dev PostgreSQL + Redis siguen corriendo (mdv-dev-postgres, mdv-dev-redis) para los próximos tickets.
|
||||
|
||||
## Próximo paso
|
||||
- intake de F-003 (HTTP foundation and request context).
|
||||
- intake de F-004 (config and feature flags).
|
||||
|
||||
@@ -13,3 +13,9 @@
|
||||
- Entregable: migraciones node-pg-migrate (up/down/no-op probados contra PostgreSQL 16 real), pool fail-fast, docker-compose (Postgres + Redis), convención <module>_<table> documentada
|
||||
- Nota: security devolvió un hallazgo bajo (interpolación de identificador en DDL de tests); se mitigó con validación estricta + tests de regresión
|
||||
- Artefactos: work/artifacts/F-002/
|
||||
|
||||
## 2026-08-14 — F-003 HTTP foundation and request context — DONE
|
||||
- Gates: reviewer APPROVED, security APPROVED, qa APPROVED, verify.sh exit 0
|
||||
- Entregable: request_id (generado o propagado-sanitizado), logs JSON correlacionados, error envelope v2 con requestId, hook de validación parseJson (zod), server con logging inyectable
|
||||
- Nota: doc stage rebotó a build para escribir README (project/ está gateado a build/implementer/running); comportamiento correcto del guardrail
|
||||
- Artefactos: work/artifacts/F-003/
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"state": "waiting",
|
||||
"next_agent": "leader",
|
||||
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||
"updated_at": "2026-08-14T20:00:16Z",
|
||||
"updated_at": "2026-08-14T20:13:28Z",
|
||||
"timeline": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user