@web-ts-toolkit/express-request-attestation 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/signer.d.ts CHANGED
@@ -1,16 +1,14 @@
1
- import { H as RequestSignerOptions, F as RequestSigner } from './time-policy-DlER-7rf.js';
2
- export { A as ATTESTATION_ERROR_RESPONSE_HEADER, a as AttestationEnvelope, b as AttestationMacInput, c as AttestationProtocolError, D as DEFAULT_ATTESTATION_HEADER, d as DEFAULT_CLOCK_SKEW_MS, e as DEFAULT_CLUSTER_CLOCK_GUARD_MS, f as DEFAULT_MAX_AGE_MS, g as DEFAULT_MAX_BODY_BYTES, E as EMPTY_BODY_SHA256_HEX, G as GLOBAL_MAX_RETENTION_MS, K as KEY_BYTES_LENGTH, M as MAC_BYTES_LENGTH, h as MAC_INPUT_LABEL, i as MAX_BODY_BYTES_HARD_CAP, j as MAX_CLOCK_SKEW_MS, k as MAX_CLUSTER_CLOCK_GUARD_MS, l as MAX_CONTENT_TYPE_BYTES, m as MAX_ENCODED_TRANSACTION_ID_BYTES, n as MAX_HEADER_NAME_BYTES, o as MAX_KEY_ID_LENGTH, p as MAX_MAX_AGE_MS, q as MAX_PUBLIC_ORIGIN_BYTES, r as MAX_REPLAY_KEY_BYTES, s as MAX_REPLAY_NAMESPACE_LENGTH, t as MAX_REQUEST_TARGET_BYTES, u as MIN_CLOCK_SKEW_MS, v as MIN_CLUSTER_CLOCK_GUARD_MS, w as MIN_MAX_AGE_MS, x as MIN_REPLAY_KEY_BYTES, y as MacInputFields, N as NONCE_BYTES_LENGTH, z as PROTOCOL_VERSION, P as ProofTiming, B as ProofTimingDecision, C as REPLAY_KEY_PREFIX, R as ResolvedTimePolicy, I as RetentionAdmission, S as SignInput, T as TimePolicyOptions, J as TransactionIdFields, L as assertTimestampMs, O as base64urlDecode, Q as base64urlEncode, U as buildMacInputBytes, V as buildReplayKeyPreimageBytes, W as canonicalJsonBytes, X as canonicalizeRequestTarget, Y as checkRetentionAdmissible, Z as decodeTransactionId, _ as encodeTransactionId, $ as evaluateProofTiming, a0 as extractSingleHeaderValue, a1 as maxStoreAdmissionMs, a2 as maxVerifierRemainingMs, a3 as normalizeContentType, a4 as normalizeMethod, a5 as parseCanonicalJsonArray, a6 as resolveTimePolicy, a7 as utf8ByteLength, a8 as utf8Decode, a9 as utf8Encode, aa as validateBodyHashHex, ab as validateHeaderName, ac as validateKeyBytes, ad as validateKeyId, ae as validateMac, af as validateNonceHex, ag as validatePublicOrigin, ah as validateReplayKey, ai as validateReplayNamespace } from './time-policy-DlER-7rf.js';
1
+ import { H as RequestSignerOptions, F as RequestSigner } from './time-policy-CquS0Zgm.js';
2
+ export { A as ATTESTATION_ERROR_RESPONSE_HEADER, a as AttestationEnvelope, b as AttestationMacInput, c as AttestationProtocolError, D as DEFAULT_ATTESTATION_HEADER, d as DEFAULT_CLOCK_SKEW_MS, e as DEFAULT_CLUSTER_CLOCK_GUARD_MS, f as DEFAULT_MAX_AGE_MS, g as DEFAULT_MAX_BODY_BYTES, E as EMPTY_BODY_SHA256_HEX, G as GLOBAL_MAX_RETENTION_MS, K as KEY_BYTES_LENGTH, M as MAC_BYTES_LENGTH, h as MAC_INPUT_LABEL, i as MAX_BODY_BYTES_HARD_CAP, j as MAX_CLOCK_SKEW_MS, k as MAX_CLUSTER_CLOCK_GUARD_MS, l as MAX_CONTENT_TYPE_BYTES, m as MAX_ENCODED_TRANSACTION_ID_BYTES, n as MAX_HEADER_NAME_BYTES, o as MAX_KEY_ID_LENGTH, p as MAX_MAX_AGE_MS, q as MAX_PUBLIC_ORIGIN_BYTES, r as MAX_REPLAY_KEY_BYTES, s as MAX_REPLAY_NAMESPACE_LENGTH, t as MAX_REQUEST_TARGET_BYTES, u as MIN_CLOCK_SKEW_MS, v as MIN_CLUSTER_CLOCK_GUARD_MS, w as MIN_MAX_AGE_MS, x as MIN_REPLAY_KEY_BYTES, y as MacInputFields, N as NONCE_BYTES_LENGTH, z as PROTOCOL_VERSION, P as ProofTiming, B as ProofTimingDecision, C as REPLAY_KEY_PREFIX, R as ResolvedTimePolicy, I as RetentionAdmission, S as SignInput, T as TimePolicyOptions, J as TransactionIdFields, L as assertTimestampMs, O as base64urlDecode, Q as base64urlEncode, U as buildMacInputBytes, V as buildReplayKeyPreimageBytes, W as canonicalJsonBytes, X as canonicalizeRequestTarget, Y as checkRetentionAdmissible, Z as decodeTransactionId, _ as encodeTransactionId, $ as evaluateProofTiming, a0 as extractSingleHeaderValue, a1 as maxStoreAdmissionMs, a2 as maxVerifierRemainingMs, a3 as normalizeContentType, a4 as normalizeMethod, a5 as parseCanonicalJsonArray, a6 as resolveTimePolicy, a7 as utf8ByteLength, a8 as utf8Decode, a9 as utf8Encode, aa as validateBodyHashHex, ab as validateHeaderName, ac as validateKeyBytes, ad as validateKeyId, ae as validateMac, af as validateNonceHex, ag as validatePublicOrigin, ah as validateReplayKey, ai as validateReplayNamespace } from './time-policy-CquS0Zgm.js';
3
3
 
4
4
  /**
5
- * ATT-06 typed browser client failures (task sections 4.1/4.6).
5
+ * Typed browser preparation and signer-loading failures.
6
6
  *
7
- * Browser-safe ONLY: no Node built-ins, Express/Redis types, `Buffer`,
8
- * `NodeJS`, or `node:*` imports. This module is part of the `./signer`
9
- * closure typechecked by `tsconfig.signer-browser.json` (`types: []`).
7
+ * Available from the browser-safe `/signer` entry without server dependencies.
10
8
  *
11
9
  * These are LOCAL preparation/loading errors. Server rejections arrive as
12
- * HTTP responses with section 4.5 codes; only a pre-handler stale-key marker
13
- * plus 403 triggers one reload/re-sign retry (ATT-07). Public HMAC material
10
+ * HTTP responses; only a pre-handler stale-key marker plus 403 triggers one
11
+ * reload/re-sign retry in `fetchWithAttestation`. Public HMAC material
14
12
  * delivered in JavaScript remains accessible to every caller despite
15
13
  * non-extractable `CryptoKey` wrapping; this error taxonomy does not imply
16
14
  * device or browser authenticity.
@@ -38,12 +36,10 @@ declare class SignerClientError extends Error {
38
36
  declare function isSignerClientError(error: unknown): error is SignerClientError;
39
37
 
40
38
  /**
41
- * ATT-06 browser WebCrypto request signer (task sections 4.1/4.2/4.6).
39
+ * Browser WebCrypto request signer.
42
40
  *
43
- * Browser-safe ONLY: this module and everything it imports (`src/shared/*`,
44
- * `./client-errors.js`) must never import Express, Redis, `Buffer`, `NodeJS`,
45
- * or `node:*` values or types. Typechecked by
46
- * `tsconfig.signer-browser.json` with `types: []`.
41
+ * Import from `@web-ts-toolkit/express-request-attestation/signer`; no Node,
42
+ * Express, Redis, or MongoDB dependencies/types are required.
47
43
  *
48
44
  * `createRequestSigner({ keyId, key, publicOrigin, replayNamespace })`
49
45
  * copies and validates 32-byte material, then returns the typed
@@ -52,7 +48,7 @@ declare function isSignerClientError(error: unknown): error is SignerClientError
52
48
  * (`buildMacInputBytes`) and returns the transaction ID
53
49
  * (`encodeTransactionId`). With explicit timestamp/nonce/body hash the
54
50
  * operation is deterministic; automatic nonce/timestamp selection belongs to
55
- * ATT-07's request wrapper, never to this low-level API.
51
+ * `fetchWithAttestation`, never to this low-level API.
56
52
  *
57
53
  * Key handling:
58
54
  *
@@ -63,6 +59,8 @@ declare function isSignerClientError(error: unknown): error is SignerClientError
63
59
  * { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])`. Non-extractable
64
60
  * wrapping does NOT make publicly delivered material confidential: any
65
61
  * caller that can fetch the module can sign fresh proofs.
62
+ * Concurrent calls share the import; a rejected import is cleared so a
63
+ * later explicit signing attempt can recover.
66
64
  * - Required capabilities (`TextEncoder`, `crypto.subtle`,
67
65
  * `crypto.getRandomValues`, secure context) are feature-detected ON USE,
68
66
  * never at import. Importing this module performs no network requests,
@@ -87,20 +85,26 @@ declare function generateNonceHex(): string;
87
85
  * material and returns immutable protection-space binding plus
88
86
  * `sign(input)`. The HMAC key is imported lazily (non-extractable) on first
89
87
  * `sign`; the factory itself performs no crypto, network, timer, or storage
90
- * work.
88
+ * work. Concurrent calls share the import. Import failure rejects with
89
+ * `SignerClientError`; a later `sign` call can retry the import. Callers supply
90
+ * timestamp, nonce, final Content-Type, and exact body digest themselves;
91
+ * prefer `fetchWithAttestation` for automatic preparation and fresh proofs.
92
+ * Public HMAC material remains accessible to every caller and does not
93
+ * authenticate a user/session. Invalid options throw `AttestationProtocolError`;
94
+ * missing WebCrypto capability and crypto failure use SIGNER_PREPARATION_FAILED.
91
95
  */
92
96
  declare function createRequestSigner(options: RequestSignerOptions): RequestSigner;
93
97
 
94
98
  /**
95
- * ATT-06 generation-aware ESM signer client (task sections 4.1/4.6).
99
+ * Generation-aware ESM signer discovery and loading.
96
100
  *
97
- * Browser-safe ONLY: no Node built-ins, Express/Redis types, `Buffer`,
98
- * `NodeJS`, or `node:*` imports. Part of the `./signer` closure typechecked
99
- * by `tsconfig.signer-browser.json` (`types: []`).
101
+ * Browser-safe: import from `@web-ts-toolkit/express-request-attestation/signer`.
102
+ * Requires standard Fetch/byte-stream APIs; no server dependencies or Node types.
100
103
  *
101
104
  * - `fetchSignerBundle({ metadataUrl, apiOrigin, replayNamespace, fetch?,
102
105
  * loadModule?, signal? })` fetches bounded discovery metadata
103
- * (`4096`-byte cap) with noncached, credential-omitting, redirect-rejecting
106
+ * (`4096`-byte incremental cap, 5000 ms response-body read deadline) with
107
+ * noncached, credential-omitting, redirect-rejecting
104
108
  * defaults, validates origin/namespace/version, resolves the key-specific
105
109
  * asset URL against the metadata location, and loads it through native
106
110
  * dynamic `import()` (default) or an injectable typed loader. No `eval`,
@@ -112,20 +116,22 @@ declare function createRequestSigner(options: RequestSignerOptions): RequestSign
112
116
  * loadModule?, loadTimeoutMs? })` owns at most one active signer and one
113
117
  * in-flight load (single-flight within this context, no cross-tab claim).
114
118
  * `invalidate(observedKeyId)` is generation-aware: a late rejection for an
115
- * old key never evicts a newer signer. `dispose()` is terminal. Failure
116
- * clears the rejected load so a later explicit call can recover. A late
119
+ * old key never evicts a newer signer or its pending replacement, including
120
+ * challenges from multiple rotations ago. `dispose()` is terminal.
121
+ * Failure clears the rejected load so a later explicit call can recover. A late
117
122
  * old-generation result never installs. One metadata-to-module retirement
118
- * race may refetch discovery once; repeated failure stops. Timeout/abort
119
- * stops waiting; native import evaluation is not cancellable, so late
120
- * results are discarded without claiming rollback of module execution.
123
+ * race may refetch discovery once; a changed validated module URL can recover
124
+ * even under the same key ID, while unchanged identity/repeated failure stops.
125
+ * Timeout stops waiting; disposal signals current metadata cancellation and
126
+ * prevents late installation. Native import evaluation is not cancellable,
127
+ * so discarding late results does not roll back module execution.
121
128
  * - Runtime module-cache retention is documented: invalidation cannot erase
122
129
  * the browser module cache or guarantee immediate key deletion from RAM.
123
130
  * Publicly distributed HMAC material remains accessible to every caller.
124
131
  *
125
132
  * No storage API, cookies, DOM mutation, global network patch, import-time
126
- * requests, or polling timers. One-shot load timeouts use a single
127
- * `setTimeout` per load with cleanup (`sideEffects: false` still holds:
128
- * timers exist only while a load is in flight).
133
+ * requests, or polling timers. Load/read deadlines and the reader's cooperative
134
+ * yield timer exist only during the operation and are cleaned up on settlement.
129
135
  */
130
136
 
131
137
  /** Bounded discovery metadata body: at most 4096 UTF-8 bytes. */
@@ -136,47 +142,105 @@ declare const DEFAULT_SIGNER_LOAD_TIMEOUT_MS: 5000;
136
142
  interface SignerModule {
137
143
  readonly signer: RequestSigner;
138
144
  }
139
- /** Typed native ESM loader. Tests/host bundlers inject a fixture loader. */
145
+ /** Loads a validated absolute module URL; defaults to native dynamic import, whose execution is not cancellable. */
140
146
  type SignerModuleLoader = (moduleUrl: string) => Promise<SignerModule>;
141
- /** Options for `fetchSignerBundle` (single metadata + import attempt). */
147
+ /**
148
+ * Options for one discovery/import attempt. Metadata must have a readable byte
149
+ * stream (standard `Response`); whole-value-only fetch substitutes are rejected.
150
+ * Body consumption is capped at 4096 bytes and 5000 ms after the response arrives.
151
+ * This deadline excludes waiting for headers and module import/evaluation.
152
+ */
142
153
  interface FetchSignerBundleOptions {
154
+ /** Absolute metadata URL on `apiOrigin`, without userinfo; typically `/attestation/signer-meta` at that origin. */
143
155
  readonly metadataUrl: string;
156
+ /** Pinned canonical HTTPS origin (loopback HTTP allowed); metadata and module origins must match. */
144
157
  readonly apiOrigin: string;
158
+ /** Expected namespace, matching both verifier and imported signer. */
145
159
  readonly replayNamespace: string;
160
+ /** Default global fetch; called with no-store/omit/error and must return a readable byte-stream Response. */
146
161
  readonly fetch?: typeof fetch;
162
+ /** Default native dynamic import; receives a validated absolute `.mjs` URL. Host owns module CORS/CSP. */
147
163
  readonly loadModule?: SignerModuleLoader;
164
+ /** Signals cancellation to discovery fetch/body reading, not native module evaluation. */
148
165
  readonly signal?: AbortSignal;
149
166
  }
150
- /** Options for `createSignerClient`. */
167
+ /** Pinned discovery configuration for a caller-owned, single-flight signer client. */
151
168
  interface SignerClientOptions {
169
+ /** Absolute discovery URL on `apiOrigin`, without userinfo; usually `${apiOrigin}/attestation/signer-meta`. */
152
170
  readonly metadataUrl: string;
171
+ /** Canonical API origin, identical to the server's configured `publicOrigin`. */
153
172
  readonly apiOrigin: string;
173
+ /** Pinned protection-space namespace. Recreate/reconfigure clients when migrating namespaces. */
154
174
  readonly replayNamespace: string;
175
+ /** Default global fetch; discovery omits credentials, rejects redirects, and requires a byte-stream Response. */
155
176
  readonly fetch?: typeof fetch;
177
+ /** Default native dynamic import. Timeout/disposal stops waiting or installation, not module execution/cache. */
156
178
  readonly loadModule?: SignerModuleLoader;
179
+ /**
180
+ * Whole load: headers, body reads, import, and one retirement refetch.
181
+ * Default 5000 ms; integer range 1–60000. Expiry is SIGNER_LOAD_TIMEOUT.
182
+ * There is no additional standalone 5000 ms body deadline inside client loads.
183
+ */
157
184
  readonly loadTimeoutMs?: number;
158
185
  }
159
- /** Minimal owned signer-client surface. */
186
+ /** One active signer and one current pending load per client; no cross-tab sharing. */
160
187
  interface SignerClient {
188
+ /** Cached or shared current load. A later call can recover after failure; a disposed client throws SIGNER_LOAD_FAILED. */
161
189
  getSigner(): Promise<RequestSigner>;
190
+ /**
191
+ * Invalidate the observed installed key, or supersede an initial load. A
192
+ * different active key and malformed IDs are ignored. Pending replacements
193
+ * and their explicit recovery coalesce challenges from any older issued key,
194
+ * including multiple rotations behind. An installed same-key signer can be
195
+ * invalidated again. The next `getSigner()` starts/shares the replacement.
196
+ */
162
197
  invalidate(observedKeyId: string): void;
198
+ /** Terminal and idempotent: signals current metadata cancellation and prevents late installation; native import/cache persists. */
163
199
  dispose(): void;
164
200
  }
165
201
  /**
166
- * Bounded discovery fetch plus native dynamic import (single attempt, no
167
- * retirement refetch). Use `createSignerClient` when the one allowed
168
- * metadata-to-module retirement refetch is required.
202
+ * Incremental discovery (4096 bytes, fixed 5000 ms body-read deadline after
203
+ * the response arrives), then native dynamic import. Exceeding the cap/deadline
204
+ * or aborting discovery cancels its reader without awaiting producer cleanup
205
+ * and rejects with `SignerClientError` (`SIGNER_LOAD_FAILED`).
206
+ * Non-OK discovery responses discard their bodies without reading error payloads
207
+ * or awaiting cancellation, preserving the fixed metadata-fetch failure.
208
+ *
209
+ * The read deadline excludes the wait for response headers and module import.
210
+ * Use `signal` to abort native discovery fetch/read; it cannot cancel native
211
+ * module evaluation. Use `createSignerClient` for a configurable whole-load
212
+ * waiting bound and the one allowed metadata-to-module retirement refetch.
213
+ * Discovery defaults to global fetch with no-store/omit/error; module loading
214
+ * defaults to native dynamic import. Metadata and module must match the pinned
215
+ * API origin, namespace, version, and key. Native module CORS/CSP/cache rules
216
+ * apply independently of the discovery fetch options.
169
217
  */
170
218
  declare function fetchSignerBundle(options: FetchSignerBundleOptions): Promise<SignerModule>;
171
- /** Create the generation-aware single-flight signer client. */
219
+ /**
220
+ * Create a caller-owned, generation-aware single-flight signer client. No I/O
221
+ * starts until `getSigner()`. One active signer and one current pending load
222
+ * are retained; concurrent callers share that load. Replacement/recovery is
223
+ * preserved across stale challenges from any older issued key, including
224
+ * multiple rotations behind. No historical key map or cross-tab state is kept.
225
+ *
226
+ * `loadTimeoutMs` bounds the whole headers/body/import/refetch wait (default
227
+ * 5000 ms, integer range 1–60000); expiry rejects with SIGNER_LOAD_TIMEOUT.
228
+ * Module-phase failure may refetch discovery once and reload only when the
229
+ * validated `(keyId, absolute module URL)` changes, including a same-key URL
230
+ * change. Unchanged identity or a second module failure ends that load.
231
+ * A later explicit `getSigner()` can recover after failure.
232
+ *
233
+ * `dispose()` is terminal and signals the current metadata fetch. Obsolete
234
+ * settlements cannot install a signer or alter its replacement. Timeout and
235
+ * disposal cannot cancel native module execution or erase its cache. This
236
+ * client loads public material; it does not authenticate a user or session.
237
+ */
172
238
  declare function createSignerClient(options: SignerClientOptions): SignerClient;
173
239
 
174
240
  /**
175
- * ATT-07 bounded request-body materialization (task sections 4.3/4.6).
241
+ * Bounded request-body materialization.
176
242
  *
177
- * Browser-safe ONLY: no Node built-ins, Express/Redis types, `Buffer`,
178
- * `NodeJS`, or `node:*` imports. Part of the `./signer` closure typechecked
179
- * by `tsconfig.signer-browser.json` (`types: []`).
243
+ * Browser-safe, using standard same-realm Request and readable byte-stream APIs.
180
244
  *
181
245
  * A `Request` body is internally a stream. This module reads supported finite
182
246
  * requests with a byte-counting reader, cancellation, and a bounded deadline
@@ -187,7 +251,8 @@ declare function createSignerClient(options: SignerClientOptions): SignerClient;
187
251
  * - `materializeRequestBodyBytes(request, { maxBodyBytes, signal, timeoutMs })`
188
252
  * returns the exact serialized bytes for hashing and for both signed
189
253
  * attempts. Empty (`body === null`) yields zero bytes. The reader is
190
- * cancelled on limit/deadline/abort/error so no stream is left flowing.
254
+ * signaled to cancel on limit/deadline/abort/error without waiting for the
255
+ * producer's cancellation promise.
191
256
  * - `assertSupportedBodyInit(body)` rejects caller JSON objects (docs use
192
257
  * `JSON.stringify`) and other non-`BodyInit` values before a `Request` is
193
258
  * built. Supported finite values (`string`, `URLSearchParams`, `Blob`,
@@ -196,13 +261,12 @@ declare function createSignerClient(options: SignerClientOptions): SignerClient;
196
261
  * it through a `Request` first and this module reads that request's bytes,
197
262
  * preserving the generated multipart boundary.
198
263
  * - `FormData` boundary preservation is owned by the caller in
199
- * `attested-fetch.ts`: the bytes captured here are reused verbatim for the
264
+ * `fetchWithAttestation`: the bytes captured here are reused verbatim for the
200
265
  * original attempt and the single stale-key retry, never reconstructed.
201
266
  *
202
267
  * No storage API, DOM mutation, global network patch, import-time I/O, or
203
- * polling timers. One-shot read deadlines use a single `setTimeout` per
204
- * materialization with cleanup (`sideEffects: false` still holds: timers
205
- * exist only while a read is in flight).
268
+ * polling timers. The internal reader owns a one-shot deadline and at most
269
+ * one cooperative-yield timer, both cleaned up when the read settles.
206
270
  */
207
271
  /** Default body budget for prepared fetch: 1 MiB (matches server default). */
208
272
  declare const DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES: number;
@@ -210,9 +274,13 @@ declare const DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES: number;
210
274
  declare const ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP: number;
211
275
  /** Default bounded read deadline for one body materialization: 5000 ms. */
212
276
  declare const DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS$1: 5000;
277
+ /** Bounds for consuming one Request body; this helper does not clone its input. */
213
278
  interface MaterializeRequestBodyOptions {
279
+ /** Positive safe-integer byte cap; default 1 MiB, hard cap 16 MiB. Covers reader-owned bytes, not native tee queues. */
214
280
  readonly maxBodyBytes?: number;
281
+ /** Explicit caller signal for this read; abort preserves its exact reason. Supply `request.signal` if desired. */
215
282
  readonly signal?: AbortSignal | null | undefined;
283
+ /** Body-read waiting bound only; default 5000 ms, integer range 1–60000. Expiry uses SIGNER_PREPARATION_FAILED. */
216
284
  readonly timeoutMs?: number;
217
285
  }
218
286
  /**
@@ -225,26 +293,33 @@ declare function assertSupportedBodyInit(body: unknown): void;
225
293
  /**
226
294
  * Materialize the exact serialized bytes of a `Request` with a byte-counting
227
295
  * reader, caller-abort propagation, and a bounded deadline.
296
+ * Defaults: 1 MiB (16 MiB hard cap), 5000 ms read deadline (1–60000), and no
297
+ * implicit signal; pass `signal: request.signal` to follow that Request's abort.
228
298
  *
229
299
  * - `request.body === null` yields zero bytes (actually empty request).
230
- * - A used (`bodyUsed`) or locked body fails locally; the original `Request`
231
- * should be cloned before reading when reuse is required (see
232
- * `attested-fetch.ts`, which clones for reading so the caller's `Request`
233
- * stays reusable when feasible).
300
+ * - Consumes the supplied Request; it does not clone. A used (`bodyUsed`) or
301
+ * locked body fails locally. Clone before calling when reuse is required.
302
+ * `fetchWithAttestation` owns that clone for inherited bodies, leaving the
303
+ * caller's finite Request readable for concurrent/repeated calls. Cancelling
304
+ * the clone cannot cancel a producer still owned by the original tee branch;
305
+ * native tee queues are outside the reader-owned storage bound.
234
306
  * - Exceeding `maxBodyBytes` cancels the reader and throws
235
307
  * `SIGNER_PREPARATION_FAILED` (client-side 413 analogue: never sent).
236
308
  * - Deadline expiry cancels the reader and throws
237
- * `SIGNER_PREPARATION_FAILED` (stalled read). Caller abort cancels and
309
+ * `SIGNER_PREPARATION_FAILED`. Caller abort cancels and
238
310
  * throws the abort reason.
311
+ * - Empty chunks retain no storage; tiny chunks share a geometrically grown
312
+ * buffer capped at `maxBodyBytes`. Reads check elapsed time and yield
313
+ * periodically so productive streams allow deadline/abort tasks to run.
314
+ * - Cancellation is best-effort and never awaited; its rejection is observed.
315
+ * The reader lock, abort listener, and timers are released on settlement.
239
316
  */
240
317
  declare function materializeRequestBodyBytes(request: Request, options?: MaterializeRequestBodyOptions): Promise<Uint8Array>;
241
318
 
242
319
  /**
243
- * ATT-07 prepared signed fetch with one stale-key retry (task sections 4.5/4.6).
320
+ * Prepared signed fetch with one pre-handler stale-key retry.
244
321
  *
245
- * Browser-safe ONLY: no Node built-ins, Express/Redis types, `Buffer`,
246
- * `NodeJS`, or `node:*` imports. Part of the `./signer` closure typechecked
247
- * by `tsconfig.signer-browser.json` (`types: []`).
322
+ * Browser-safe: use the `/signer` entry without Express or Node dependencies/types.
248
323
  *
249
324
  * - `fetchWithAttestation(input, init, options)` builds one immutable request
250
325
  * snapshot from `string | URL | Request` input plus `RequestInit`. Actual
@@ -254,14 +329,18 @@ declare function materializeRequestBodyBytes(request: Request, options?: Materia
254
329
  * `keepalive`) are preserved. Only the configured API origin is signed;
255
330
  * unrelated origins fail locally without sending auth or signature material.
256
331
  * - The serialized body is materialized ONCE within the byte cap/read
257
- * deadline (`request-body.ts`), then those same bytes plus the final content
258
- * type are hashed and reused for both attempts. Normal finite `BodyInit`
332
+ * deadline, then those same bytes plus the final content
333
+ * type and one body digest are reused for both attempts. Normal finite `BodyInit`
259
334
  * values and replayable `Request`s are supported; caller JSON objects must
260
335
  * be serialized with `JSON.stringify` first. `FormData` is serialized
261
336
  * through a `Request` first and its bytes/boundary are preserved verbatim
262
337
  * (never reconstructed on retry, never `.arrayBuffer()` on `FormData`
263
338
  * itself). A `Request` body uses a byte-counting reader with cancellation
264
- * and timeout rather than rejecting on stream type alone.
339
+ * and timeout rather than rejecting on stream type alone. Inherited bodies
340
+ * are cloned before reading so finite caller Requests remain reusable,
341
+ * including concurrent calls. Non-null `init.body` supplies a replacement;
342
+ * `null`/`undefined` inherit the body, as in Fetch. Explicit replacement
343
+ * streams transfer ownership, including aliases of the original's body.
265
344
  * - Each attempt mints one fresh 128-bit nonce (`generateNonceHex`) and one
266
345
  * validated `now()` timestamp. Loaded signer protection space must match
267
346
  * the allowed `apiOrigin`/`replayNamespace` before signing. The caller
@@ -269,6 +348,9 @@ declare function materializeRequestBodyBytes(request: Request, options?: Materia
269
348
  * - Signed attempts use `redirect: 'manual'`; redirect results are
270
349
  * exposed/returned without forwarding a signature. `Content-Encoding` other
271
350
  * than identity/absent fails locally (v1 identity-only profile).
351
+ * - `no-cors` and browser-forbidden proof fields fail before body/signer work
352
+ * when statically known. Each constructed Request must retain the exact proof
353
+ * before transport is invoked; unrestricted temporary Headers are insufficient.
272
354
  * - At most one retry, only on the pre-handler stale-key signal: expected
273
355
  * `403` plus `X-Attestation-Error: stale-key`. The error response is
274
356
  * inspected via headers only (never consumed), only the observed key is
@@ -276,41 +358,29 @@ declare function materializeRequestBodyBytes(request: Request, options?: Materia
276
358
  * timestamp/nonce. A second stale challenge stops. Expired/future/replay/
277
359
  * invalid proofs, operational `5xx`, auth failures, redirects, network
278
360
  * uncertainty, and handler uncertainty never retry.
361
+ * - The first stale response remains untouched until the retry Request is
362
+ * built. Reload/signing/construction failure or a repeated envelope returns
363
+ * that readable response. Abandoned bodies are cancelled without awaiting
364
+ * producer cleanup, with cancellation rejection observed. Caller abort stops
365
+ * this invocation's preparation wait without disposing the shared signer client.
279
366
  *
280
- * Composition (tested in `test/fetch-client.test.ts`, documented for ATT-08):
281
- *
282
- * ```ts
283
- * import express from 'express';
284
- * import { createOidcVaultMiddleware } from '@web-ts-toolkit/express-oidc-vault';
285
- * import {
286
- * createAttestationBodyCapture,
287
- * createRequestAttestationMiddleware,
288
- * } from '@web-ts-toolkit/express-request-attestation';
289
- *
290
- * const capture = createAttestationBodyCapture({ maxBodyBytes });
291
- * const guard = createRequestAttestationMiddleware({ publicOrigin, replayNamespace, keyProvider, store });
292
- * const app = express();
293
- * // Vault router owns its base path: mount once, never double-prefix.
294
- * app.use(createOidcVaultMiddleware({ basePath: '/auth/oidc', backendOrigin, storeProvider, config }));
295
- * // Capture JSON bytes before both validators on selected API routes only.
296
- * app.use(express.json({ limit: maxBodyBytes, verify: capture.verify, inflate: false }));
297
- * app.use(capture.errorHandler);
298
- * app.post('/api/submit', guard, requireBearerAuth, handler);
299
- * // Asset/OPTIONS/OIDC navigation/callback/backchannel routes stay reachable
300
- * // under their own policies; the guard never enrolls DPoP bindings or
301
- * // rewrites `req.auth`, and every auth refresh re-signs via a fresh wrapper call.
302
- * ```
367
+ * Application auth retries call this wrapper again for a fresh proof. The
368
+ * server guard never establishes or rewrites authentication. Auth-before-guard
369
+ * avoids replay allocation for rejected credentials; guard-first can reject
370
+ * bad proofs before auth work but reserves even if later auth fails. Durable
371
+ * business idempotency and retry decisions remain application-owned.
303
372
  *
304
- * Browser CORS/CSP (application-configured, verified in tests):
373
+ * Browser CORS/CSP (application-configured):
305
374
  *
306
375
  * - Allow the configured signature field plus actual `Content-Type`,
307
376
  * `Authorization`/DPoP headers as needed; expose `X-Attestation-Error` so
308
377
  * the wrapper can see the retry marker; allow credentialed origins only when
309
378
  * the application explicitly uses `credentials: 'include'`.
310
- * - Native module imports require `script-src` + CORS for the public module
311
- * URLs. Same-origin uses the native default; cross-origin deployments must
312
- * explicitly configure CORS for metadata/module fetches (which use
313
- * `cache: no-store`, `credentials: omit`, `redirect: error`).
379
+ * - Native module imports require `script-src` + CORS for key and runtime URLs;
380
+ * metadata/API fetches need suitable `connect-src`. Discovery uses
381
+ * no-store/omit/error, while dynamic imports follow native module rules.
382
+ * Asset modules must not redirect to other origins. Let OPTIONS reach the
383
+ * application's CORS handler before the server attestation guard.
314
384
  *
315
385
  * Public HMAC material stays accessible to every caller despite
316
386
  * non-extractable `CryptoKey` wrapping; this wrapper provides request-format
@@ -321,30 +391,63 @@ declare function materializeRequestBodyBytes(request: Request, options?: Materia
321
391
  declare const DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS: 5000;
322
392
  /** Options for `fetchWithAttestation` (all validation is strict). */
323
393
  interface FetchWithAttestationOptions {
324
- /** Generation-aware signer client (ATT-06). Required, retained by reference. */
394
+ /** Caller-owned shared signer client, retained by reference; abort never disposes it. */
325
395
  readonly signerClient: SignerClient;
326
396
  /** Pinned API origin; only this origin is signed. Required. */
327
397
  readonly apiOrigin: string;
328
398
  /** Protection-space namespace; must match the loaded signer. Required. */
329
399
  readonly replayNamespace: string;
330
- /** Injected fetch (preferred name, matches task text). Defaults to global fetch. */
400
+ /** API transport, default global fetch. Must honor the Request signal; no package transport deadline or uncertain retry. */
331
401
  readonly fetch?: typeof fetch;
332
- /** Alias for `fetch` (matches ATT-01 scaffold). At most one may be set. */
402
+ /** Compatibility alias for `fetch`; set at most one of the two options. */
333
403
  readonly fetchFn?: typeof fetch;
334
- /** Signature header field. Defaults to `x-client-transaction-id`. */
404
+ /**
405
+ * Signature field, default `x-client-transaction-id`. Shared HTTP validation
406
+ * rejects malformed/reserved names with `AttestationProtocolError`. Otherwise
407
+ * valid browser-forbidden fields (including `sec-*`, `proxy-*`, `Cookie2`, and
408
+ * `User-Agent`) fail locally with `SIGNER_PREPARATION_FAILED` before body
409
+ * preparation or signer work. The field must be writable by browser Fetch.
410
+ */
335
411
  readonly headerName?: string;
336
412
  /** Body budget, default 1 MiB, hard cap 16 MiB. Must match server/parser limits. */
337
413
  readonly maxBodyBytes?: number;
338
- /** Bounded body-read deadline, default 5000 ms, allowed 1–60000 ms. */
414
+ /** Body-read deadline only, default 5000 ms (integer 1–60000); distinct from signer load and API transport waits. */
339
415
  readonly bodyReadTimeoutMs?: number;
340
- /** Epoch-millisecond clock for per-attempt timestamps. Defaults to `Date.now`. */
416
+ /** Per-attempt clock, default `Date.now`; must return nonnegative safe-integer epoch milliseconds. */
341
417
  readonly now?: () => number;
342
418
  }
343
419
  /**
344
420
  * Prepare one immutable request snapshot, sign the exact bytes sent, and
345
- * retry at most once on the pre-handler stale-key marker.
421
+ * retry at most once on `403` plus `X-Attestation-Error: stale-key`. Only the
422
+ * pinned API origin is signed; Authorization/credentials follow native Fetch
423
+ * options, and signed attempts always use manual redirects. Body bytes and
424
+ * their digest are reused, with a fresh nonce/timestamp on each attempt.
425
+ *
426
+ * After general option validation, `no-cors` and browser-forbidden fields reject with
427
+ * `SIGNER_PREPARATION_FAILED` before body/signer work when statically known.
428
+ * `init.mode` is sampled once before `init.body`; an undefined mode inherits the
429
+ * Request's mode (or Fetch's default). Supported `cors`/`same-origin` settings
430
+ * are preserved. Every constructed attempt must retain the exact proof.
431
+ *
432
+ * Inherited finite Request bodies are cloned, leaving the original readable for
433
+ * concurrent/repeated calls. Non-null `init.body` overrides even used/locked
434
+ * originals; `null`/`undefined` inherit and require an unused, unlocked body.
435
+ * Explicit stream bodies (even aliases of the original's body) transfer reading/
436
+ * cancellation ownership after native construction succeeds. The byte cap covers
437
+ * reader-owned storage, not native tee queues retained for the caller's original
438
+ * Request. Reuse refers to the original Request's current body, not the identity
439
+ * of a raw stream reference saved before native cloning. Construction failure
440
+ * leaves an unaccepted explicit stream caller-owned.
346
441
  *
347
- * See the module header for the full preparation/retry/composition contract.
442
+ * Reload/signing/retry-construction failure or a repeated envelope returns the
443
+ * untouched first stale response. Only a prepared retry (or caller abort)
444
+ * abandons it; cancellation is nonblocking with rejection observed. Caller abort
445
+ * stops waiting for preparation, without disposing the shared signer client or
446
+ * cancelling native module/crypto execution, and rejects with the caller's exact
447
+ * reason. Dispatched readers belong to native Fetch/the injected transport,
448
+ * which must honor the forwarded signal. Transport errors never retry. Every
449
+ * separate application auth/business retry needs a fresh wrapper call; public
450
+ * HMAC is not authentication or durable business idempotency.
348
451
  */
349
452
  declare function fetchWithAttestation(input: RequestInfo | URL, init: RequestInit | undefined, options: FetchWithAttestationOptions): Promise<Response>;
350
453