Skip to content

Commit f684f23

Browse files
authored
Merge pull request #759 from Matrixthechoppedboy/security/issue-643-prewave-be-049-authorize-and-make-dead-letter
fix: authorize and make dead-letter replay idempotent
2 parents fb33274 + 498d3dd commit f684f23

8 files changed

Lines changed: 1596 additions & 263 deletions

File tree

‎docs/OPERATIONS_API.md‎

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,40 @@ All routes are admin-plane protected (`AdminGuard` + `RolesGuard`).
1515
| Method | Path | Capability | Roles |
1616
| --- | --- | --- | --- |
1717
| `GET` | `/v1/operations/queue` | `OPERATIONS_QUEUE_READ` | super_admin, administrator, security_analyst, auditor |
18-
| `GET` | `/v1/operations/queue/:id` | `OPERATIONS_QUEUE_READ` | super_admin, administrator, security_analyst, auditor |
19-
| `GET` | `/v1/operations/queue/:id/retry-eligibility` | `OPERATIONS_RETRY_READ` | super_admin, administrator, security_analyst, auditor |
18+
| `GET` | `/v1/operations/queue/:id` | `OPERATIONS_QUEUE_READ@ | super_admin, administrator, security_analyst, auditor |
19+
| `GET`| `/v1/operations/queue/:id/retry-eligibility` | `OPERATIONS_RETRY_READ@ | super_admin, administrator, security_analyst, auditor |
2020
| `POST` | `/v1/operations/queue/:id/retry` | `OPERATIONS_RETRY_SUBMIT` | super_admin, administrator |
2121
| `GET` | `/v1/operations/audit/:auditId` | `OPERATIONS_AUDIT_READ` | super_admin, administrator, security_analyst, auditor |
22+
| `POST` | `/v1/operations/queue/:id/replay` | `OPERATIONS_REPLAY_SUBMIT` | super_admin, administrator |
23+
| `POST` | `/v1/operations/queue/:id/replay/pause` | `OPERATIONS_REPLAY_CONTROL` | super_admin, administrator |
24+
| `POST` | `/v1/operations/queue/:id/replay/resume` | `OPERATIONS_REPLAY_CONTROL` | super_admin, administrator |
2225

2326
### Queue item shape
2427

2528
Every item carries immutable identifiers and the metadata the boards need to act
2629
without guessing:
2730

31+
The replay surface adds the dead-letter linkage fields below.
32+
2833
`id` (immutable work id), `releaseId`, `capability`, `authoritySource`,
2934
`authorityExpiresAt`, `status`, `impact`, `duplicationRisk`, `freshness`,
3035
`attempts`, `maxAttempts`, `retryable`, `version`, `updatedAt`, `summary`.
3136

3237
### Retry eligibility
3338

34-
`GET /v1/operations/queue/:id/retry-eligibility` returns:
39+
GET /v1/operations/queue/:id/retry-eligibility returns:
3540

3641
```json
3742
{ "id": "queue-001", "eligible": true, "reason": null, "attemptsRemaining": 4,
3843
"preconditionToken": "a1b2c3d4e5f60718293a4b5c6d7e8f90" }
3944
```
4045

41-
The frontend determines retryability directly from `eligible`/`reason` — it
42-
never has to infer it from other fields.
46+
The frontend determines retryability directly from `eligible`/`reason` — the
47+
nver has to infer it from other fields.
4348

4449
### Retry submission
4550

46-
`POST /v1/operations/queue/:id/retry` requires:
51+
POST /v1/operations/queue/:id/retry requires:
4752

4853
| Input | Where | Why |
4954
| --- | --- | --- |
@@ -56,11 +61,86 @@ never has to infer it from other fields.
5661
Authentication is not authorization: capability and expiry are revalidated on
5762
every request.
5863

64+
## Dead-Letter Replay (PREWAVE-BE-049)
65+
66+
Dead-letter replay is a distinct, higher-risk surface from plain retry. It is
67+
**denied by default** and only proceeds when every guard below is satisfied.
68+
69+
### Replay request shape
70+
71+
`POST /v1/operations/queue/:id/replay` requires:
72+
73+
| Input | Where | Why |
74+
| --- | --- | --- |
75+
| `Idempotency-Key` | header | Required. Duplicate keys replay the stored outcome; they never re-execute. A duplicate cannot produce a second effect. |
76+
| `X-Precondition-Token` | header (or `preconditionToken` body) | Required. Must match the item's current version, or the request is rejected as stale. |
77+
| `X-Authority-Capability` | header | Required. Must equal `OPERATIONS_REPLAY_SUBMIT` and the item's required capability. |
78+
| `X-Authority-Expires-At` | header (ISO) | Required. Revalidated on every request; missing/expired/ambiguous fails closed. |
79+
| `reason` | body (required) | Non-empty operator rationale, audited with the replay record. |
80+
| `dryRun` | body (required) | When `true`, evaluates and returns the decision without enqueuing work. |
81+
| `originalPayloadHash` | body (required) | Sha-256 hex of the original dead-letter payload. Must match the stored hash or the replay is rejected as conflict. |
82+
| `batchLimit` | body (optional) | Bounded batch size; defaults to `1` and is capped at the configured maximum. Exceeding the cap fails closed. |
83+
84+
### Replay guards (fail closed)
85+
86+
1. **Authorization.** Requires `OPERATIONS_REPLAY_SUBMIT` capability and a
87+
valid, non-expired authority. Missing, stale, revoked, or ambiguous authority
88+
returns `DENIED ` or `STALE_AUTHORITY`.
89+
2. **Reason.** A replay without a non-empty reason is rejected. The reason is
90+
recorded in the audit record.
91+
3. **Bounds.** confirmed duplication risk, quarantined items, and exceeding the
92+
configured batch cap all fail closed.
93+
4. **Dry run.** `dryRun` is required and must be explicit; it never enqueues
94+
work and never advances the item version.
95+
5. **Original payload hash.** `originalPayloadHash` must match the stored
96+
dead-letter payload hash; a mismatch is `CONFLICT`.
97+
6. **Release/schema compatibility.** Replay is prevented across release or schema
98+
incompatibility. The item's `releaseId`, chain ID, deployment block, and
99+
artifact checksum must match the current authoritative release; a stale or mismatched
100+
release returns `STALE_RELEASE`.
101+
7. **Audit record.** Every accepted or rejected replay carries a stable `auditId`.
102+
The audit record links the original dead-letter reference to the replay
103+
reference immutably.
104+
105+
### Replay response
106+
107+
```json
108+
{ "id": "queue-001", "auditId": "audit-replay-001", "dryRun": false,
109+
"accepted": true, "duplicate": false, "processed": 1,
110+
"originalRef": "dl-001", "replayRef": "replay-001",
111+
"releaseId": "release-2025-01", "schemaVersion": "v1" }
112+
```
113+
114+
When a duplicate `Idempotency-Key` is seen, the stored outcome is returned with
115+
`duplicate: true` and the same `originalRef`/`replayRef`; no second effect is
116+
produced.
117+
118+
### Bounded batch control
119+
120+
`POST /v1/operations/queue/:id/replay/pause` and
121+
`POST /v1/operations/queue/:id/replay/resume` let operators stop and resume
122+
bounded replay batches. Both require the same authority, precondition, and audit
123+
guarantees as replay submission. Pausing is idempotent; resuming a batch that is
124+
already running is a no-op. Batches always respect the configured maximum and
125+
stop at the first failure or poison message.
126+
127+
### Replay error codes
128+
129+
In addition to the canonical error codes below, replay requests may return:
130+
131+
| `errorCode` | HTTP | Meaning |
132+
| --- | --- | --- |
133+
| `STALE_RELEASE` | 409 | Item release/schema is incompatible with the current authoritative release. |
134+
| `PAYLOAD_HASH_MISMATCH` | 409 | `originalPayloadHash` does not match the stored payload. |
135+
| `BATCH_LIMIT_EXCEEDED` | 422 | Requested batch size exceeds the configured maximum. |
136+
| `DRY_RUN_REQUIRED` | 400 | `dryRun` was not supplied. |
137+
| `REASON_REQUIRED` | 400 | `reason` was missing or empty. |
138+
59139
## Canonical results
60140

61141
Every accepted or rejected protected request carries a **stable audit
62-
identifier** (`auditId`). Rejections use the canonical envelope
63-
`{ statusCode, errorCode, message, auditId, timestamp }` with one of:
142+
identifien** (`auditId`). Rejections use the canonical envelope
143+
`{ statusCode, errorCode, message, auditId, timestamp } ` with one of:
64144

65145
| `errorCode` | HTTP | Meaning |
66146
| --- | --- | --- |
@@ -79,12 +159,34 @@ identifier** (`auditId`). Rejections use the canonical envelope
79159
1. **No duplicate protocol mutation.** A retry re-enqueues existing work through
80160
the queue source, which carries its own deterministic idempotency key; a
81161
duplicate `Idempotency-Key` replays the stored outcome instead of re-running.
162+
The same holds for dead-letter replay: one message cannot produce duplicate
163+
effects.
82164
2. **Fail closed.** Missing, stale, revoked, or ambiguous authority never
83-
defaults to allowed.
84-
3. **Sanitized audit.** Records exclude secrets, protected evidence,
165+
defaults to allowed. Replay is denied by default and requires authority,
166+
reason, bounds, dry run, original payload hash, and an audit record.
167+
3. **Immutable linkage.** Original and replay references are linked immutably in the
168+
audit record; neither reference is rewritten by a later replay.
169+
4. **Sanitized audit.** Records exclude secrets, protected evidence,
85170
credentials, raw authorization headers, and internal topology.
86-
4. **Read-only protocol outcomes.** No operational endpoint edits protocol
87-
state; the only write-shaped handler is the idempotent retry submission.
171+
5. **Read-only protocol outcomes.** No operational endpoint edits protocol
172+
state; the only write-shaped handlers are the idempotent retry submission and the
173+
bounded, audited replay controls.
174+
6. **Chain-bound data.** Replay decisions are bound to chain ID, release ID, deployment block, and artifact checksum; stale, ambiguous, missing, or unauthorized
175+
state fails closed.
176+
177+
## Required tests
178+
179+
- `Unauthorized:` replay without `OPERATIONS_REPLAY_SUBMIT` or with expired
180+
authority is denied.
181+
- `Stale release:` replay across incompatible release/schema is rejected with
182+
`STALE_RELEASE`.
183+
- `Duplicate replay:` a repeated `Idempotency-Key` returns the stored outcome
184+
without a second effect.
185+
- `Batch limit:` a batch exceeding the configured maximum fails closed.
186+
- `Poison message:` a poison message halts the batch and is quarantined.
187+
- `Partial failure:` a failure in a batch stops further replay and is audited.
188+
- `Audit:` every accepted/rejected replay produces a stable audit record linking
189+
original and replay references immutably.
88190

89191
## Fixtures
90192

0 commit comments

Comments
 (0)