# Findings Ledger Proxy Contract (Web V) ## Status - Final v1.0 (2025-12-01); validated with Findings Ledger Guild for Sprint 0216. ## Scope - Gateway → Findings Ledger forwarding for vuln workflow actions (`open`, `ack`, `close`, `reopen`, `export`). - Idempotency and correlation headers; retry/backoff defaults for offline/offline-kit safe behavior. ## Required Headers | Name | Requirement | Notes | | --- | --- | --- | | `Authorization: Bearer ` | Required | RS256/ES256 service token, `aud=stellaops-ledger`, scopes `ledger:write ledger:read`. | | `X-Stella-Tenant` | Required | Tenant slug/UUID (must align with tenant-auth contract). | | `X-Stella-Project` | Conditional | Required for project-scoped findings. | | `X-Idempotency-Key` | Required on POST/PUT | Deterministic `BLAKE3-256(base64url(tenant + route + canonical_body))`; 44 chars. TTL: 24h. | | `X-Correlation-Id` | Required | UUID/ULID stable across gateway → ledger → notifier; echoed by responses. | | `Content-Type` | Required | `application/json`. | | `If-Match` | Optional | When present, ledger enforces optimistic concurrency using the last `ETag` value. | ## Behavior - Delivery semantics: gateway is at-least-once; Findings Ledger guarantees exactly-once per `X-Idempotency-Key` within 24h TTL. - Retry/backoff (gateway): base delay 500 ms, factor 2, jitter ±20%, max 3 attempts, cap total wait 10 s. Offline kits persist NDJSON (headers+body) and replay on next sync window. - Timeout: 5 s per attempt; timeout → `ERR_LEDGER_TIMEOUT`. - Concurrency: ledger returns `ETag` for each workflow record; gateway includes `If-Match` on retries when available. Mismatch → 409 + `ERR_LEDGER_CONFLICT`. - Error mapping (deterministic envelope with `trace_id` + echoed `X-Correlation-Id`): - 400 → `ERR_LEDGER_BAD_REQUEST` (propagate `details`). - 404 → `ERR_LEDGER_NOT_FOUND`. - 409 → `ERR_LEDGER_CONFLICT`. - 429/503 → `ERR_LEDGER_RETRY`. - 500+ → `ERR_LEDGER_UPSTREAM`. ## Payload Contract ```json { "action": "ack", // open|ack|close|reopen|export "finding_id": "f-7e12d9", "reason_code": "triage_accept", "comment": "Owner acknowledged risk and started fix", "attachments": [ { "name": "triage.pdf", "digest": "sha256-..." } ], "actor": { "subject": "svc-console", "type": "service" }, "metadata": { "policy_version": "2025.11.0", "vex_statement_id": "vex-123" } } ``` - Body must be canonical JSON (sorted keys) before hashing for `X-Idempotency-Key`. - Maximum size: 64 KiB; larger bodies rejected with 413. ## Example Request ```bash curl -X POST https://gateway.stellaops.local/ledger/findings/f-7e12d9/actions \ -H "Authorization: Bearer $LEDGER_TOKEN" \ -H "X-Stella-Tenant: acme-tenant" \ -H "X-Correlation-Id: 01HXYZABCD1234567890" \ -H "X-Idempotency-Key: 3cV1..." \ -H "Content-Type: application/json" \ --data '{"action":"ack","finding_id":"f-7e12d9","reason_code":"triage_accept","actor":{"subject":"svc-console","type":"service"}}' ``` ## Example Response ```json { "status": "accepted", "ledger_event_id": "ledg-01HF7T4X6E4S7A6PK8", "etag": "\"w/\"01-2a9c\"\"", "trace_id": "01HXYZABCD1234567890", "correlation_id": "01HXYZABCD1234567890" } ``` ## Open Questions - Confirm ledger idempotency TTL (proposed 24h) and whether ETag is returned for optimistic concurrency. - Confirm expected payload schemas for each workflow action (open/ack/close/export). - Confirm whether ledger enforces ordering per `tenant_id`.