fix: harden the legacy HTTP rollback path

N-06: export one idempotency-key authority from mutation-intent.ts and use it
in the V2 client. A caller-supplied key is validated before credentials, timers
and fetch, and an invalid value is rejected as VALIDATION_REJECTED /
IDEMPOTENCY_KEY_INVALID rather than trimmed, regenerated or dropped, so a keyed
command can no longer replay while sending no key.

N-07: bound the legacy credential wait by the existing attempt controller,
which already carries the total deadline and the caller signal, so a
non-cooperative owner cannot hold the request open and no extra timer is
introduced. The owner receives the operation context, and the failure follows
ownership: deadline to REQUEST_TIMEOUT, caller to REQUEST_ABORTED, and only a
genuine rejection to AUTH_INTEGRATION_FAILURE. None of these paths fetch.

N-08: readBoundedJson delegates to the common bounded reader, so cancel and
releaseLock throws stay isolated inside the closed result, and the V2
content-type mismatch now cancels the response body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-13 23:46:03 +09:00
co-authored by Claude Opus 5
parent 4fe924ee0f
commit c9e820aed5
7 changed files with 356 additions and 63 deletions
@@ -307,6 +307,9 @@ describe("HTTP operation execution contract", () => {
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
});
// The credential wait is bounded by the same attempt controller, so wait
// until the request is actually in flight before firing the deadline.
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1));
await vi.waitFor(() => expect(scheduler.callbacks).toHaveLength(1));
scheduler.callbacks[0]();
await expect(timeoutResult).resolves.toMatchObject({