@web-ts-toolkit/express-request-attestation 0.48.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/LICENSE +201 -0
- package/README.md +505 -0
- package/index.d.mts +712 -0
- package/index.d.ts +712 -0
- package/index.js +2881 -0
- package/index.mjs +2850 -0
- package/package.json +64 -0
- package/signer.d.mts +351 -0
- package/signer.d.ts +351 -0
- package/signer.js +2064 -0
- package/signer.mjs +2041 -0
- package/time-policy-DlER-7rf.d.mts +385 -0
- package/time-policy-DlER-7rf.d.ts +385 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@web-ts-toolkit/express-request-attestation",
|
|
3
|
+
"description": "Versioned per-request HMAC signing and replay detection for Express APIs with a browser-safe signer entry",
|
|
4
|
+
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-request-attestation",
|
|
5
|
+
"version": "0.48.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"keywords": [
|
|
8
|
+
"express",
|
|
9
|
+
"hmac",
|
|
10
|
+
"replay",
|
|
11
|
+
"attestation",
|
|
12
|
+
"webcrypto"
|
|
13
|
+
],
|
|
14
|
+
"main": "./index.js",
|
|
15
|
+
"module": "./index.mjs",
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": {
|
|
20
|
+
"import": "./index.d.mts",
|
|
21
|
+
"require": "./index.d.ts",
|
|
22
|
+
"default": "./index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"import": "./index.mjs",
|
|
25
|
+
"require": "./index.js",
|
|
26
|
+
"default": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"./signer": {
|
|
29
|
+
"types": {
|
|
30
|
+
"import": "./signer.d.mts",
|
|
31
|
+
"require": "./signer.d.ts",
|
|
32
|
+
"default": "./signer.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"import": "./signer.mjs",
|
|
35
|
+
"require": "./signer.js",
|
|
36
|
+
"default": "./signer.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"express": ">=5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"express": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"**/*",
|
|
52
|
+
"!**/*.map"
|
|
53
|
+
],
|
|
54
|
+
"author": "Junmin Ahn",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/egose/web-ts-toolkit/issues"
|
|
57
|
+
},
|
|
58
|
+
"license": "Apache-2.0",
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/egose/web-ts-toolkit.git",
|
|
62
|
+
"directory": "packages/express-request-attestation"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/signer.d.mts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { H as RequestSignerOptions, F as RequestSigner } from './time-policy-DlER-7rf.mjs';
|
|
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.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ATT-06 typed browser client failures (task sections 4.1/4.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: []`).
|
|
10
|
+
*
|
|
11
|
+
* 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
|
|
14
|
+
* delivered in JavaScript remains accessible to every caller despite
|
|
15
|
+
* non-extractable `CryptoKey` wrapping; this error taxonomy does not imply
|
|
16
|
+
* device or browser authenticity.
|
|
17
|
+
*
|
|
18
|
+
* No storage API, DOM mutation, global network patch, import-time requests,
|
|
19
|
+
* or polling timers. Importing this module has no side effects
|
|
20
|
+
* (`sideEffects: false`).
|
|
21
|
+
*/
|
|
22
|
+
/** Typed client preparation/load failure codes. */
|
|
23
|
+
type SignerClientErrorCode = 'SIGNER_LOAD_FAILED' | 'SIGNER_LOAD_TIMEOUT' | 'SIGNER_MODULE_INVALID' | 'SIGNER_ORIGIN_MISMATCH' | 'SIGNER_PREPARATION_FAILED';
|
|
24
|
+
/**
|
|
25
|
+
* Typed client-side failure. Messages are actionable and never echo raw key
|
|
26
|
+
* bytes, nonces, MACs, request bodies, or provider diagnostics. Key IDs,
|
|
27
|
+
* origins, and namespaces in messages are public configuration/discovery
|
|
28
|
+
* values, never secrets.
|
|
29
|
+
*/
|
|
30
|
+
declare class SignerClientError extends Error {
|
|
31
|
+
readonly name = "SignerClientError";
|
|
32
|
+
readonly code: SignerClientErrorCode;
|
|
33
|
+
constructor(code: SignerClientErrorCode, message: string, options?: {
|
|
34
|
+
readonly cause?: unknown;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Type guard for `SignerClientError`. */
|
|
38
|
+
declare function isSignerClientError(error: unknown): error is SignerClientError;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ATT-06 browser WebCrypto request signer (task sections 4.1/4.2/4.6).
|
|
42
|
+
*
|
|
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: []`.
|
|
47
|
+
*
|
|
48
|
+
* `createRequestSigner({ keyId, key, publicOrigin, replayNamespace })`
|
|
49
|
+
* copies and validates 32-byte material, then returns the typed
|
|
50
|
+
* `RequestSigner` (`version: 1` plus immutable protection-space binding plus
|
|
51
|
+
* `sign(input)`). `sign(input)` frames the exact shared MAC input
|
|
52
|
+
* (`buildMacInputBytes`) and returns the transaction ID
|
|
53
|
+
* (`encodeTransactionId`). With explicit timestamp/nonce/body hash the
|
|
54
|
+
* operation is deterministic; automatic nonce/timestamp selection belongs to
|
|
55
|
+
* ATT-07's request wrapper, never to this low-level API.
|
|
56
|
+
*
|
|
57
|
+
* Key handling:
|
|
58
|
+
*
|
|
59
|
+
* - Input bytes are copied on entry; later caller mutation cannot change the
|
|
60
|
+
* signer, and the signer never exposes key bytes.
|
|
61
|
+
* - Exactly one non-extractable HMAC-SHA-256 `CryptoKey` is imported lazily
|
|
62
|
+
* on first `sign` via `crypto.subtle.importKey('raw', copy,
|
|
63
|
+
* { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])`. Non-extractable
|
|
64
|
+
* wrapping does NOT make publicly delivered material confidential: any
|
|
65
|
+
* caller that can fetch the module can sign fresh proofs.
|
|
66
|
+
* - Required capabilities (`TextEncoder`, `crypto.subtle`,
|
|
67
|
+
* `crypto.getRandomValues`, secure context) are feature-detected ON USE,
|
|
68
|
+
* never at import. Importing this module performs no network requests,
|
|
69
|
+
* timers, key generation, or storage access (`sideEffects: false`).
|
|
70
|
+
* - Body-hash (`hashBodySha256Hex`) and nonce (`generateNonceHex`) helpers
|
|
71
|
+
* stay browser-safe with no `Math.random` or predictable fallback.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Browser-safe SHA-256 hex digest of the exact body bytes. An actually empty
|
|
76
|
+
* request hashes zero bytes. Uses `crypto.subtle.digest`; no Node imports.
|
|
77
|
+
*/
|
|
78
|
+
declare function hashBodySha256Hex(body: Uint8Array): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Browser-safe 128-bit nonce as 32 lowercase hex chars via
|
|
81
|
+
* `crypto.getRandomValues`. No `Math.random` fallback: missing capability
|
|
82
|
+
* throws an actionable local error.
|
|
83
|
+
*/
|
|
84
|
+
declare function generateNonceHex(): string;
|
|
85
|
+
/**
|
|
86
|
+
* Create the low-level WebCrypto HMAC signer. Copies/validates 32-byte
|
|
87
|
+
* material and returns immutable protection-space binding plus
|
|
88
|
+
* `sign(input)`. The HMAC key is imported lazily (non-extractable) on first
|
|
89
|
+
* `sign`; the factory itself performs no crypto, network, timer, or storage
|
|
90
|
+
* work.
|
|
91
|
+
*/
|
|
92
|
+
declare function createRequestSigner(options: RequestSignerOptions): RequestSigner;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* ATT-06 generation-aware ESM signer client (task sections 4.1/4.6).
|
|
96
|
+
*
|
|
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: []`).
|
|
100
|
+
*
|
|
101
|
+
* - `fetchSignerBundle({ metadataUrl, apiOrigin, replayNamespace, fetch?,
|
|
102
|
+
* loadModule?, signal? })` fetches bounded discovery metadata
|
|
103
|
+
* (`4096`-byte cap) with noncached, credential-omitting, redirect-rejecting
|
|
104
|
+
* defaults, validates origin/namespace/version, resolves the key-specific
|
|
105
|
+
* asset URL against the metadata location, and loads it through native
|
|
106
|
+
* dynamic `import()` (default) or an injectable typed loader. No `eval`,
|
|
107
|
+
* `new Function`, Blob-script workaround, or global `<script>` callback.
|
|
108
|
+
* Native import follows browser module rules; the helper cannot intercept
|
|
109
|
+
* its redirect chain, so the asset router must not redirect modules to
|
|
110
|
+
* other origins and the host CSP must scope executable origins.
|
|
111
|
+
* - `createSignerClient({ metadataUrl, apiOrigin, replayNamespace, fetch?,
|
|
112
|
+
* loadModule?, loadTimeoutMs? })` owns at most one active signer and one
|
|
113
|
+
* in-flight load (single-flight within this context, no cross-tab claim).
|
|
114
|
+
* `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
|
|
117
|
+
* 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.
|
|
121
|
+
* - Runtime module-cache retention is documented: invalidation cannot erase
|
|
122
|
+
* the browser module cache or guarantee immediate key deletion from RAM.
|
|
123
|
+
* Publicly distributed HMAC material remains accessible to every caller.
|
|
124
|
+
*
|
|
125
|
+
* 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).
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/** Bounded discovery metadata body: at most 4096 UTF-8 bytes. */
|
|
132
|
+
declare const SIGNER_METADATA_BYTE_CAP: 4096;
|
|
133
|
+
/** Default bound for one `getSigner()` load (metadata + import + one refetch). */
|
|
134
|
+
declare const DEFAULT_SIGNER_LOAD_TIMEOUT_MS: 5000;
|
|
135
|
+
/** Distributed-module shape: a module exports `signer`, nothing else trusted. */
|
|
136
|
+
interface SignerModule {
|
|
137
|
+
readonly signer: RequestSigner;
|
|
138
|
+
}
|
|
139
|
+
/** Typed native ESM loader. Tests/host bundlers inject a fixture loader. */
|
|
140
|
+
type SignerModuleLoader = (moduleUrl: string) => Promise<SignerModule>;
|
|
141
|
+
/** Options for `fetchSignerBundle` (single metadata + import attempt). */
|
|
142
|
+
interface FetchSignerBundleOptions {
|
|
143
|
+
readonly metadataUrl: string;
|
|
144
|
+
readonly apiOrigin: string;
|
|
145
|
+
readonly replayNamespace: string;
|
|
146
|
+
readonly fetch?: typeof fetch;
|
|
147
|
+
readonly loadModule?: SignerModuleLoader;
|
|
148
|
+
readonly signal?: AbortSignal;
|
|
149
|
+
}
|
|
150
|
+
/** Options for `createSignerClient`. */
|
|
151
|
+
interface SignerClientOptions {
|
|
152
|
+
readonly metadataUrl: string;
|
|
153
|
+
readonly apiOrigin: string;
|
|
154
|
+
readonly replayNamespace: string;
|
|
155
|
+
readonly fetch?: typeof fetch;
|
|
156
|
+
readonly loadModule?: SignerModuleLoader;
|
|
157
|
+
readonly loadTimeoutMs?: number;
|
|
158
|
+
}
|
|
159
|
+
/** Minimal owned signer-client surface. */
|
|
160
|
+
interface SignerClient {
|
|
161
|
+
getSigner(): Promise<RequestSigner>;
|
|
162
|
+
invalidate(observedKeyId: string): void;
|
|
163
|
+
dispose(): void;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
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.
|
|
169
|
+
*/
|
|
170
|
+
declare function fetchSignerBundle(options: FetchSignerBundleOptions): Promise<SignerModule>;
|
|
171
|
+
/** Create the generation-aware single-flight signer client. */
|
|
172
|
+
declare function createSignerClient(options: SignerClientOptions): SignerClient;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* ATT-07 bounded request-body materialization (task sections 4.3/4.6).
|
|
176
|
+
*
|
|
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: []`).
|
|
180
|
+
*
|
|
181
|
+
* A `Request` body is internally a stream. This module reads supported finite
|
|
182
|
+
* requests with a byte-counting reader, cancellation, and a bounded deadline
|
|
183
|
+
* instead of claiming the mere stream type proves the body unsupported.
|
|
184
|
+
* Caller-supplied streaming uploads that exceed the cap, stall past the
|
|
185
|
+
* deadline, or abort are rejected locally without unbounded reads.
|
|
186
|
+
*
|
|
187
|
+
* - `materializeRequestBodyBytes(request, { maxBodyBytes, signal, timeoutMs })`
|
|
188
|
+
* returns the exact serialized bytes for hashing and for both signed
|
|
189
|
+
* attempts. Empty (`body === null`) yields zero bytes. The reader is
|
|
190
|
+
* cancelled on limit/deadline/abort/error so no stream is left flowing.
|
|
191
|
+
* - `assertSupportedBodyInit(body)` rejects caller JSON objects (docs use
|
|
192
|
+
* `JSON.stringify`) and other non-`BodyInit` values before a `Request` is
|
|
193
|
+
* built. Supported finite values (`string`, `URLSearchParams`, `Blob`,
|
|
194
|
+
* `ArrayBuffer`/views, `FormData`, `ReadableStream`, `null`) pass through;
|
|
195
|
+
* `FormData` itself is never read via `.arrayBuffer()` — callers serialize
|
|
196
|
+
* it through a `Request` first and this module reads that request's bytes,
|
|
197
|
+
* preserving the generated multipart boundary.
|
|
198
|
+
* - `FormData` boundary preservation is owned by the caller in
|
|
199
|
+
* `attested-fetch.ts`: the bytes captured here are reused verbatim for the
|
|
200
|
+
* original attempt and the single stale-key retry, never reconstructed.
|
|
201
|
+
*
|
|
202
|
+
* 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).
|
|
206
|
+
*/
|
|
207
|
+
/** Default body budget for prepared fetch: 1 MiB (matches server default). */
|
|
208
|
+
declare const DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES: number;
|
|
209
|
+
/** Hard v1 cap for prepared fetch: 16 MiB (matches server hard cap). */
|
|
210
|
+
declare const ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP: number;
|
|
211
|
+
/** Default bounded read deadline for one body materialization: 5000 ms. */
|
|
212
|
+
declare const DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS$1: 5000;
|
|
213
|
+
interface MaterializeRequestBodyOptions {
|
|
214
|
+
readonly maxBodyBytes?: number;
|
|
215
|
+
readonly signal?: AbortSignal | null | undefined;
|
|
216
|
+
readonly timeoutMs?: number;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Reject caller JSON objects and other non-`BodyInit` values before a
|
|
220
|
+
* `Request` is built. Docs use `JSON.stringify`; a plain object is never a
|
|
221
|
+
* valid Fetch body and must fail locally rather than serialize as
|
|
222
|
+
* `"[object Object]"`.
|
|
223
|
+
*/
|
|
224
|
+
declare function assertSupportedBodyInit(body: unknown): void;
|
|
225
|
+
/**
|
|
226
|
+
* Materialize the exact serialized bytes of a `Request` with a byte-counting
|
|
227
|
+
* reader, caller-abort propagation, and a bounded deadline.
|
|
228
|
+
*
|
|
229
|
+
* - `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).
|
|
234
|
+
* - Exceeding `maxBodyBytes` cancels the reader and throws
|
|
235
|
+
* `SIGNER_PREPARATION_FAILED` (client-side 413 analogue: never sent).
|
|
236
|
+
* - Deadline expiry cancels the reader and throws
|
|
237
|
+
* `SIGNER_PREPARATION_FAILED` (stalled read). Caller abort cancels and
|
|
238
|
+
* throws the abort reason.
|
|
239
|
+
*/
|
|
240
|
+
declare function materializeRequestBodyBytes(request: Request, options?: MaterializeRequestBodyOptions): Promise<Uint8Array>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* ATT-07 prepared signed fetch with one stale-key retry (task sections 4.5/4.6).
|
|
244
|
+
*
|
|
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: []`).
|
|
248
|
+
*
|
|
249
|
+
* - `fetchWithAttestation(input, init, options)` builds one immutable request
|
|
250
|
+
* snapshot from `string | URL | Request` input plus `RequestInit`. Actual
|
|
251
|
+
* method, headers (including `Authorization`), explicit `credentials`
|
|
252
|
+
* (native default `same-origin`, never forced to `include`), `signal`, and
|
|
253
|
+
* explicit request options (`mode`, `cache`, `referrer`, `integrity`,
|
|
254
|
+
* `keepalive`) are preserved. Only the configured API origin is signed;
|
|
255
|
+
* unrelated origins fail locally without sending auth or signature material.
|
|
256
|
+
* - 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`
|
|
259
|
+
* values and replayable `Request`s are supported; caller JSON objects must
|
|
260
|
+
* be serialized with `JSON.stringify` first. `FormData` is serialized
|
|
261
|
+
* through a `Request` first and its bytes/boundary are preserved verbatim
|
|
262
|
+
* (never reconstructed on retry, never `.arrayBuffer()` on `FormData`
|
|
263
|
+
* itself). A `Request` body uses a byte-counting reader with cancellation
|
|
264
|
+
* and timeout rather than rejecting on stream type alone.
|
|
265
|
+
* - Each attempt mints one fresh 128-bit nonce (`generateNonceHex`) and one
|
|
266
|
+
* validated `now()` timestamp. Loaded signer protection space must match
|
|
267
|
+
* the allowed `apiOrigin`/`replayNamespace` before signing. The caller
|
|
268
|
+
* signature field is replaced (never appended).
|
|
269
|
+
* - Signed attempts use `redirect: 'manual'`; redirect results are
|
|
270
|
+
* exposed/returned without forwarding a signature. `Content-Encoding` other
|
|
271
|
+
* than identity/absent fails locally (v1 identity-only profile).
|
|
272
|
+
* - At most one retry, only on the pre-handler stale-key signal: expected
|
|
273
|
+
* `403` plus `X-Attestation-Error: stale-key`. The error response is
|
|
274
|
+
* inspected via headers only (never consumed), only the observed key is
|
|
275
|
+
* invalidated, and the same serialized request is re-signed with a fresh
|
|
276
|
+
* timestamp/nonce. A second stale challenge stops. Expired/future/replay/
|
|
277
|
+
* invalid proofs, operational `5xx`, auth failures, redirects, network
|
|
278
|
+
* uncertainty, and handler uncertainty never retry.
|
|
279
|
+
*
|
|
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
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* Browser CORS/CSP (application-configured, verified in tests):
|
|
305
|
+
*
|
|
306
|
+
* - Allow the configured signature field plus actual `Content-Type`,
|
|
307
|
+
* `Authorization`/DPoP headers as needed; expose `X-Attestation-Error` so
|
|
308
|
+
* the wrapper can see the retry marker; allow credentialed origins only when
|
|
309
|
+
* 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`).
|
|
314
|
+
*
|
|
315
|
+
* Public HMAC material stays accessible to every caller despite
|
|
316
|
+
* non-extractable `CryptoKey` wrapping; this wrapper provides request-format
|
|
317
|
+
* consistency and replay detection, not device/browser authenticity.
|
|
318
|
+
*/
|
|
319
|
+
|
|
320
|
+
/** Default bounded body-read deadline for one prepared fetch: 5000 ms. */
|
|
321
|
+
declare const DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS: 5000;
|
|
322
|
+
/** Options for `fetchWithAttestation` (all validation is strict). */
|
|
323
|
+
interface FetchWithAttestationOptions {
|
|
324
|
+
/** Generation-aware signer client (ATT-06). Required, retained by reference. */
|
|
325
|
+
readonly signerClient: SignerClient;
|
|
326
|
+
/** Pinned API origin; only this origin is signed. Required. */
|
|
327
|
+
readonly apiOrigin: string;
|
|
328
|
+
/** Protection-space namespace; must match the loaded signer. Required. */
|
|
329
|
+
readonly replayNamespace: string;
|
|
330
|
+
/** Injected fetch (preferred name, matches task text). Defaults to global fetch. */
|
|
331
|
+
readonly fetch?: typeof fetch;
|
|
332
|
+
/** Alias for `fetch` (matches ATT-01 scaffold). At most one may be set. */
|
|
333
|
+
readonly fetchFn?: typeof fetch;
|
|
334
|
+
/** Signature header field. Defaults to `x-client-transaction-id`. */
|
|
335
|
+
readonly headerName?: string;
|
|
336
|
+
/** Body budget, default 1 MiB, hard cap 16 MiB. Must match server/parser limits. */
|
|
337
|
+
readonly maxBodyBytes?: number;
|
|
338
|
+
/** Bounded body-read deadline, default 5000 ms, allowed 1–60000 ms. */
|
|
339
|
+
readonly bodyReadTimeoutMs?: number;
|
|
340
|
+
/** Epoch-millisecond clock for per-attempt timestamps. Defaults to `Date.now`. */
|
|
341
|
+
readonly now?: () => number;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Prepare one immutable request snapshot, sign the exact bytes sent, and
|
|
345
|
+
* retry at most once on the pre-handler stale-key marker.
|
|
346
|
+
*
|
|
347
|
+
* See the module header for the full preparation/retry/composition contract.
|
|
348
|
+
*/
|
|
349
|
+
declare function fetchWithAttestation(input: RequestInfo | URL, init: RequestInit | undefined, options: FetchWithAttestationOptions): Promise<Response>;
|
|
350
|
+
|
|
351
|
+
export { ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP, DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS$1 as DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS, DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS as DEFAULT_ATTESTED_FETCH_BODY_TIMEOUT_MS, DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES, DEFAULT_SIGNER_LOAD_TIMEOUT_MS, type FetchSignerBundleOptions, type FetchWithAttestationOptions, type MaterializeRequestBodyOptions, RequestSigner, RequestSignerOptions, SIGNER_METADATA_BYTE_CAP, type SignerClient, SignerClientError, type SignerClientErrorCode, type SignerClientOptions, type SignerModule, type SignerModuleLoader, assertSupportedBodyInit, createRequestSigner, createSignerClient, fetchSignerBundle, fetchWithAttestation, generateNonceHex, hashBodySha256Hex, isSignerClientError, materializeRequestBodyBytes };
|