@web-ts-toolkit/express-request-attestation 0.51.0 → 0.52.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/README.md +352 -56
- package/index.d.mts +212 -103
- package/index.d.ts +212 -103
- package/index.js +31 -56
- package/index.mjs +31 -56
- package/package.json +1 -1
- package/signer.d.mts +196 -93
- package/signer.d.ts +196 -93
- package/signer.js +454 -326
- package/signer.mjs +454 -326
- package/{time-policy-DlER-7rf.d.mts → time-policy-CquS0Zgm.d.mts} +46 -25
- package/{time-policy-DlER-7rf.d.ts → time-policy-CquS0Zgm.d.ts} +46 -25
package/signer.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { H as RequestSignerOptions, F as RequestSigner } from './time-policy-
|
|
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-
|
|
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
|
-
*
|
|
5
|
+
* Typed browser preparation and signer-loading failures.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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
|
-
*
|
|
39
|
+
* Browser WebCrypto request signer.
|
|
42
40
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
99
|
+
* Generation-aware ESM signer discovery and loading.
|
|
96
100
|
*
|
|
97
|
-
* Browser-safe
|
|
98
|
-
*
|
|
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
|
|
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
|
|
116
|
-
*
|
|
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;
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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.
|
|
127
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
241
|
+
* Bounded request-body materialization.
|
|
176
242
|
*
|
|
177
|
-
* Browser-safe
|
|
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
|
-
*
|
|
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
|
-
* `
|
|
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.
|
|
204
|
-
*
|
|
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
|
-
* -
|
|
231
|
-
*
|
|
232
|
-
* `
|
|
233
|
-
*
|
|
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
|
|
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
|
-
*
|
|
320
|
+
* Prepared signed fetch with one pre-handler stale-key retry.
|
|
244
321
|
*
|
|
245
|
-
* Browser-safe
|
|
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
|
|
258
|
-
* type
|
|
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
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
|
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
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
402
|
+
/** Compatibility alias for `fetch`; set at most one of the two options. */
|
|
333
403
|
readonly fetchFn?: typeof fetch;
|
|
334
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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
|
|