@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/index.js
ADDED
|
@@ -0,0 +1,2881 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ATTESTATION_ERROR_RESPONSE_HEADER: () => ATTESTATION_ERROR_RESPONSE_HEADER,
|
|
34
|
+
AttestationCapacityError: () => AttestationCapacityError,
|
|
35
|
+
AttestationError: () => AttestationError,
|
|
36
|
+
AttestationMongoStoreError: () => AttestationMongoStoreError,
|
|
37
|
+
AttestationProtocolError: () => AttestationProtocolError,
|
|
38
|
+
AttestationRedisStoreError: () => AttestationRedisStoreError,
|
|
39
|
+
DEFAULT_ATTESTATION_HEADER: () => DEFAULT_ATTESTATION_HEADER,
|
|
40
|
+
DEFAULT_CLOCK_SKEW_MS: () => DEFAULT_CLOCK_SKEW_MS,
|
|
41
|
+
DEFAULT_CLUSTER_CLOCK_GUARD_MS: () => DEFAULT_CLUSTER_CLOCK_GUARD_MS,
|
|
42
|
+
DEFAULT_MAX_AGE_MS: () => DEFAULT_MAX_AGE_MS,
|
|
43
|
+
DEFAULT_MAX_BODY_BYTES: () => DEFAULT_MAX_BODY_BYTES,
|
|
44
|
+
EMPTY_BODY_SHA256_HEX: () => EMPTY_BODY_SHA256_HEX,
|
|
45
|
+
GLOBAL_MAX_RETENTION_MS: () => GLOBAL_MAX_RETENTION_MS,
|
|
46
|
+
KEY_BYTES_LENGTH: () => KEY_BYTES_LENGTH,
|
|
47
|
+
MAC_BYTES_LENGTH: () => MAC_BYTES_LENGTH,
|
|
48
|
+
MAC_INPUT_LABEL: () => MAC_INPUT_LABEL,
|
|
49
|
+
MAX_BODY_BYTES_HARD_CAP: () => MAX_BODY_BYTES_HARD_CAP,
|
|
50
|
+
MAX_CLOCK_SKEW_MS: () => MAX_CLOCK_SKEW_MS,
|
|
51
|
+
MAX_CLUSTER_CLOCK_GUARD_MS: () => MAX_CLUSTER_CLOCK_GUARD_MS,
|
|
52
|
+
MAX_CONTENT_TYPE_BYTES: () => MAX_CONTENT_TYPE_BYTES,
|
|
53
|
+
MAX_ENCODED_TRANSACTION_ID_BYTES: () => MAX_ENCODED_TRANSACTION_ID_BYTES,
|
|
54
|
+
MAX_HEADER_NAME_BYTES: () => MAX_HEADER_NAME_BYTES,
|
|
55
|
+
MAX_KEY_ID_LENGTH: () => MAX_KEY_ID_LENGTH,
|
|
56
|
+
MAX_MAX_AGE_MS: () => MAX_MAX_AGE_MS,
|
|
57
|
+
MAX_PUBLIC_ORIGIN_BYTES: () => MAX_PUBLIC_ORIGIN_BYTES,
|
|
58
|
+
MAX_REPLAY_KEY_BYTES: () => MAX_REPLAY_KEY_BYTES,
|
|
59
|
+
MAX_REPLAY_NAMESPACE_LENGTH: () => MAX_REPLAY_NAMESPACE_LENGTH,
|
|
60
|
+
MAX_REQUEST_TARGET_BYTES: () => MAX_REQUEST_TARGET_BYTES,
|
|
61
|
+
MIN_CLOCK_SKEW_MS: () => MIN_CLOCK_SKEW_MS,
|
|
62
|
+
MIN_CLUSTER_CLOCK_GUARD_MS: () => MIN_CLUSTER_CLOCK_GUARD_MS,
|
|
63
|
+
MIN_MAX_AGE_MS: () => MIN_MAX_AGE_MS,
|
|
64
|
+
MIN_REPLAY_KEY_BYTES: () => MIN_REPLAY_KEY_BYTES,
|
|
65
|
+
MONGO_ATTESTATION_STORE_DEFAULT_CAPACITY_COLLECTION: () => MONGO_ATTESTATION_STORE_DEFAULT_CAPACITY_COLLECTION,
|
|
66
|
+
MONGO_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES: () => MONGO_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES,
|
|
67
|
+
MONGO_ATTESTATION_STORE_DEFAULT_PROOFS_COLLECTION: () => MONGO_ATTESTATION_STORE_DEFAULT_PROOFS_COLLECTION,
|
|
68
|
+
NONCE_BYTES_LENGTH: () => NONCE_BYTES_LENGTH,
|
|
69
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
70
|
+
REDIS_ATTESTATION_STORE_DEFAULT_KEY_PREFIX: () => REDIS_ATTESTATION_STORE_DEFAULT_KEY_PREFIX,
|
|
71
|
+
REDIS_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES: () => REDIS_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES,
|
|
72
|
+
REPLAY_KEY_PREFIX: () => REPLAY_KEY_PREFIX,
|
|
73
|
+
assertAllowedContentEncoding: () => assertAllowedContentEncoding,
|
|
74
|
+
assertTimestampMs: () => assertTimestampMs,
|
|
75
|
+
base64urlDecode: () => base64urlDecode,
|
|
76
|
+
base64urlEncode: () => base64urlEncode,
|
|
77
|
+
buildMacInputBytes: () => buildMacInputBytes,
|
|
78
|
+
buildReplayKeyPreimageBytes: () => buildReplayKeyPreimageBytes,
|
|
79
|
+
canonicalJsonBytes: () => canonicalJsonBytes,
|
|
80
|
+
canonicalizeRequestTarget: () => canonicalizeRequestTarget,
|
|
81
|
+
checkProofTiming: () => checkProofTiming,
|
|
82
|
+
checkRetentionAdmissible: () => checkRetentionAdmissible,
|
|
83
|
+
copyAttestationSnapshot: () => copyAttestationSnapshot,
|
|
84
|
+
createAttestationBodyCapture: () => createAttestationBodyCapture,
|
|
85
|
+
createMemoryAttestationStore: () => createMemoryAttestationStore,
|
|
86
|
+
createMongoAttestationStore: () => createMongoAttestationStore,
|
|
87
|
+
createRedisAttestationStore: () => createRedisAttestationStore,
|
|
88
|
+
createRequestAttestationMiddleware: () => createRequestAttestationMiddleware,
|
|
89
|
+
createRotatingKeyProvider: () => createRotatingKeyProvider,
|
|
90
|
+
createSignerBundleRouter: () => createSignerBundleRouter,
|
|
91
|
+
createStaticKeyProvider: () => createStaticKeyProvider,
|
|
92
|
+
decodeTransactionId: () => decodeTransactionId,
|
|
93
|
+
deriveAttestationReplayKey: () => deriveAttestationReplayKey,
|
|
94
|
+
deriveReplayKey: () => deriveReplayKey,
|
|
95
|
+
encodeTransactionId: () => encodeTransactionId,
|
|
96
|
+
evaluateProofTiming: () => evaluateProofTiming,
|
|
97
|
+
extractSingleHeaderValue: () => extractSingleHeaderValue,
|
|
98
|
+
generateAttestationKey: () => generateAttestationKey,
|
|
99
|
+
getAttestationBodyRecord: () => getAttestationBodyRecord,
|
|
100
|
+
hashBodySha256Hex: () => hashBodySha256Hex,
|
|
101
|
+
isAttestationError: () => isAttestationError,
|
|
102
|
+
maxStoreAdmissionMs: () => maxStoreAdmissionMs,
|
|
103
|
+
maxVerifierRemainingMs: () => maxVerifierRemainingMs,
|
|
104
|
+
normalizeContentType: () => normalizeContentType,
|
|
105
|
+
normalizeMethod: () => normalizeMethod,
|
|
106
|
+
parseCanonicalJsonArray: () => parseCanonicalJsonArray,
|
|
107
|
+
resolveContentType: () => resolveContentType,
|
|
108
|
+
resolveRequestTarget: () => resolveRequestTarget,
|
|
109
|
+
resolveTimePolicy: () => resolveTimePolicy,
|
|
110
|
+
toAttestationErrorPayload: () => toAttestationErrorPayload,
|
|
111
|
+
utf8ByteLength: () => utf8ByteLength,
|
|
112
|
+
utf8Decode: () => utf8Decode,
|
|
113
|
+
utf8Encode: () => utf8Encode,
|
|
114
|
+
validateBodyHashHex: () => validateBodyHashHex,
|
|
115
|
+
validateHeaderName: () => validateHeaderName,
|
|
116
|
+
validateKeyBytes: () => validateKeyBytes,
|
|
117
|
+
validateKeyId: () => validateKeyId,
|
|
118
|
+
validateMac: () => validateMac,
|
|
119
|
+
validateNonceHex: () => validateNonceHex,
|
|
120
|
+
validatePublicOrigin: () => validatePublicOrigin,
|
|
121
|
+
validateReplayKey: () => validateReplayKey,
|
|
122
|
+
validateReplayNamespace: () => validateReplayNamespace,
|
|
123
|
+
verifyAttestationProof: () => verifyAttestationProof
|
|
124
|
+
});
|
|
125
|
+
module.exports = __toCommonJS(index_exports);
|
|
126
|
+
var import_node_crypto6 = require("crypto");
|
|
127
|
+
|
|
128
|
+
// src/shared/types.ts
|
|
129
|
+
var PROTOCOL_VERSION = 1;
|
|
130
|
+
var MAC_INPUT_LABEL = "wtt-request-signature";
|
|
131
|
+
var DEFAULT_ATTESTATION_HEADER = "x-client-transaction-id";
|
|
132
|
+
var ATTESTATION_ERROR_RESPONSE_HEADER = "x-attestation-error";
|
|
133
|
+
var REPLAY_KEY_PREFIX = "att:v1:";
|
|
134
|
+
var KEY_BYTES_LENGTH = 32;
|
|
135
|
+
var NONCE_BYTES_LENGTH = 16;
|
|
136
|
+
var MAC_BYTES_LENGTH = 32;
|
|
137
|
+
var MAX_KEY_ID_LENGTH = 64;
|
|
138
|
+
var MAX_REPLAY_NAMESPACE_LENGTH = 128;
|
|
139
|
+
var MAX_ENCODED_TRANSACTION_ID_BYTES = 1024;
|
|
140
|
+
var MAX_REQUEST_TARGET_BYTES = 8192;
|
|
141
|
+
var MAX_CONTENT_TYPE_BYTES = 256;
|
|
142
|
+
var MAX_PUBLIC_ORIGIN_BYTES = 512;
|
|
143
|
+
var MIN_REPLAY_KEY_BYTES = 1;
|
|
144
|
+
var MAX_REPLAY_KEY_BYTES = 256;
|
|
145
|
+
var MAX_HEADER_NAME_BYTES = 128;
|
|
146
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
147
|
+
var MAX_BODY_BYTES_HARD_CAP = 16777216;
|
|
148
|
+
var DEFAULT_MAX_AGE_MS = 3e4;
|
|
149
|
+
var MIN_MAX_AGE_MS = 1e3;
|
|
150
|
+
var MAX_MAX_AGE_MS = 12e4;
|
|
151
|
+
var DEFAULT_CLOCK_SKEW_MS = 5e3;
|
|
152
|
+
var MIN_CLOCK_SKEW_MS = 0;
|
|
153
|
+
var MAX_CLOCK_SKEW_MS = 3e4;
|
|
154
|
+
var DEFAULT_CLUSTER_CLOCK_GUARD_MS = 5e3;
|
|
155
|
+
var MIN_CLUSTER_CLOCK_GUARD_MS = 0;
|
|
156
|
+
var MAX_CLUSTER_CLOCK_GUARD_MS = 3e4;
|
|
157
|
+
var GLOBAL_MAX_RETENTION_MS = 24e4;
|
|
158
|
+
var EMPTY_BODY_SHA256_HEX = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
159
|
+
var AttestationProtocolError = class extends Error {
|
|
160
|
+
name = "AttestationProtocolError";
|
|
161
|
+
constructor(message) {
|
|
162
|
+
super(`[express-request-attestation] ${message}`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/shared/canonical.ts
|
|
167
|
+
var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
168
|
+
var BASE64URL_DECODE_TABLE = (() => {
|
|
169
|
+
const table = /* @__PURE__ */ Object.create(null);
|
|
170
|
+
for (let index = 0; index < BASE64URL_ALPHABET.length; index += 1) {
|
|
171
|
+
table[BASE64URL_ALPHABET[index]] = index;
|
|
172
|
+
}
|
|
173
|
+
return table;
|
|
174
|
+
})();
|
|
175
|
+
var textEncoder = new TextEncoder();
|
|
176
|
+
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
177
|
+
function utf8Encode(text) {
|
|
178
|
+
if (typeof text !== "string") {
|
|
179
|
+
throw new AttestationProtocolError("expected a string for UTF-8 encoding");
|
|
180
|
+
}
|
|
181
|
+
return textEncoder.encode(text);
|
|
182
|
+
}
|
|
183
|
+
function utf8Decode(bytes) {
|
|
184
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
185
|
+
throw new AttestationProtocolError("expected a Uint8Array for UTF-8 decoding");
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
return textDecoder.decode(bytes);
|
|
189
|
+
} catch {
|
|
190
|
+
throw new AttestationProtocolError("input is not valid UTF-8");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function utf8ByteLength(text) {
|
|
194
|
+
return utf8Encode(text).length;
|
|
195
|
+
}
|
|
196
|
+
function base64urlEncode(bytes) {
|
|
197
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
198
|
+
throw new AttestationProtocolError("expected a Uint8Array for base64url encoding");
|
|
199
|
+
}
|
|
200
|
+
let out = "";
|
|
201
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
202
|
+
const first = bytes[index];
|
|
203
|
+
const second = index + 1 < bytes.length ? bytes[index + 1] : 0;
|
|
204
|
+
const third = index + 2 < bytes.length ? bytes[index + 2] : 0;
|
|
205
|
+
const triplet = first << 16 | second << 8 | third;
|
|
206
|
+
out += BASE64URL_ALPHABET[triplet >> 18 & 63];
|
|
207
|
+
out += BASE64URL_ALPHABET[triplet >> 12 & 63];
|
|
208
|
+
if (index + 1 < bytes.length) {
|
|
209
|
+
out += BASE64URL_ALPHABET[triplet >> 6 & 63];
|
|
210
|
+
}
|
|
211
|
+
if (index + 2 < bytes.length) {
|
|
212
|
+
out += BASE64URL_ALPHABET[triplet & 63];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function base64urlDecode(input) {
|
|
218
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
219
|
+
throw new AttestationProtocolError("base64url input must be a non-empty string");
|
|
220
|
+
}
|
|
221
|
+
for (const char of input) {
|
|
222
|
+
if (!(char in BASE64URL_DECODE_TABLE)) {
|
|
223
|
+
throw new AttestationProtocolError("base64url input uses a non-canonical alphabet or padding");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (input.length % 4 === 1) {
|
|
227
|
+
throw new AttestationProtocolError("base64url input has an impossible length");
|
|
228
|
+
}
|
|
229
|
+
const outLength = Math.floor(input.length * 6 / 8);
|
|
230
|
+
const out = new Uint8Array(outLength);
|
|
231
|
+
let cursor = 0;
|
|
232
|
+
let buffer = 0;
|
|
233
|
+
let bits = 0;
|
|
234
|
+
for (const char of input) {
|
|
235
|
+
buffer = buffer << 6 | BASE64URL_DECODE_TABLE[char];
|
|
236
|
+
bits += 6;
|
|
237
|
+
if (bits >= 8) {
|
|
238
|
+
bits -= 8;
|
|
239
|
+
out[cursor] = buffer >> bits & 255;
|
|
240
|
+
cursor += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) {
|
|
244
|
+
throw new AttestationProtocolError("base64url input has non-canonical trailing bits");
|
|
245
|
+
}
|
|
246
|
+
const canonical = base64urlEncode(out);
|
|
247
|
+
if (canonical !== input) {
|
|
248
|
+
throw new AttestationProtocolError("base64url input is not canonical");
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function canonicalJsonBytes(value) {
|
|
253
|
+
let text;
|
|
254
|
+
try {
|
|
255
|
+
text = JSON.stringify(value);
|
|
256
|
+
} catch {
|
|
257
|
+
throw new AttestationProtocolError("value is not JSON-serializable");
|
|
258
|
+
}
|
|
259
|
+
if (typeof text !== "string") {
|
|
260
|
+
throw new AttestationProtocolError("value is not JSON-serializable");
|
|
261
|
+
}
|
|
262
|
+
return utf8Encode(text);
|
|
263
|
+
}
|
|
264
|
+
function parseCanonicalJsonArray(text, expectedLength) {
|
|
265
|
+
if (typeof text !== "string") {
|
|
266
|
+
throw new AttestationProtocolError("canonical JSON input must be a string");
|
|
267
|
+
}
|
|
268
|
+
let parsed;
|
|
269
|
+
try {
|
|
270
|
+
parsed = JSON.parse(text);
|
|
271
|
+
} catch {
|
|
272
|
+
throw new AttestationProtocolError("input is not valid JSON");
|
|
273
|
+
}
|
|
274
|
+
if (!Array.isArray(parsed)) {
|
|
275
|
+
throw new AttestationProtocolError("canonical JSON input must be an array");
|
|
276
|
+
}
|
|
277
|
+
if (JSON.stringify(parsed) !== text) {
|
|
278
|
+
throw new AttestationProtocolError("JSON input is not in canonical form");
|
|
279
|
+
}
|
|
280
|
+
if (expectedLength !== void 0 && parsed.length !== expectedLength) {
|
|
281
|
+
throw new AttestationProtocolError(`canonical JSON array must have exactly ${expectedLength} elements`);
|
|
282
|
+
}
|
|
283
|
+
return parsed;
|
|
284
|
+
}
|
|
285
|
+
function assertTimestampMs(name, value) {
|
|
286
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
287
|
+
throw new AttestationProtocolError(`${name} must be a non-negative safe integer`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/shared/codec.ts
|
|
292
|
+
var KEY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
293
|
+
var REPLAY_NAMESPACE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
294
|
+
var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
|
|
295
|
+
var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
296
|
+
var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
297
|
+
var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
|
|
298
|
+
var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
299
|
+
"authorization",
|
|
300
|
+
"proxy-authenticate",
|
|
301
|
+
"proxy-authorization",
|
|
302
|
+
"www-authenticate",
|
|
303
|
+
"cookie",
|
|
304
|
+
"set-cookie",
|
|
305
|
+
"host",
|
|
306
|
+
"origin",
|
|
307
|
+
"content-encoding",
|
|
308
|
+
"content-length",
|
|
309
|
+
"content-type",
|
|
310
|
+
"transfer-encoding",
|
|
311
|
+
ATTESTATION_ERROR_RESPONSE_HEADER
|
|
312
|
+
]);
|
|
313
|
+
function isLoopbackHostname(hostname) {
|
|
314
|
+
return hostname === "localhost" || hostname === "[::1]" || /^127\./.test(hostname);
|
|
315
|
+
}
|
|
316
|
+
function validateKeyId(keyId) {
|
|
317
|
+
if (typeof keyId !== "string" || !KEY_ID_PATTERN.test(keyId) || keyId.length > MAX_KEY_ID_LENGTH) {
|
|
318
|
+
throw new AttestationProtocolError("keyId must match [A-Za-z0-9_-]{1,64}");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function validateReplayNamespace(namespace) {
|
|
322
|
+
if (typeof namespace !== "string" || !REPLAY_NAMESPACE_PATTERN.test(namespace) || namespace.length > MAX_REPLAY_NAMESPACE_LENGTH) {
|
|
323
|
+
throw new AttestationProtocolError("replayNamespace must match [A-Za-z0-9_-]{1,128}");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function validateNonceHex(nonceHex) {
|
|
327
|
+
if (typeof nonceHex !== "string" || !NONCE_HEX_PATTERN.test(nonceHex)) {
|
|
328
|
+
throw new AttestationProtocolError("nonceHex must be exactly 32 lowercase hex characters");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function validateBodyHashHex(bodyHashHex) {
|
|
332
|
+
if (typeof bodyHashHex !== "string" || !BODY_HASH_HEX_PATTERN.test(bodyHashHex)) {
|
|
333
|
+
throw new AttestationProtocolError("bodyHashHex must be exactly 64 lowercase hex characters");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function validateMac(mac) {
|
|
337
|
+
if (typeof mac !== "string" || mac.length !== 43) {
|
|
338
|
+
throw new AttestationProtocolError("mac must be exactly 43 base64url characters");
|
|
339
|
+
}
|
|
340
|
+
const decoded = base64urlDecode(mac);
|
|
341
|
+
if (decoded.length !== MAC_BYTES_LENGTH) {
|
|
342
|
+
throw new AttestationProtocolError("mac must decode to 32 bytes");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function validateKeyBytes(key) {
|
|
346
|
+
if (!(key instanceof Uint8Array) || key.length !== KEY_BYTES_LENGTH) {
|
|
347
|
+
throw new AttestationProtocolError("signing key must be exactly 32 bytes");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function validatePublicOrigin(origin) {
|
|
351
|
+
if (typeof origin !== "string") {
|
|
352
|
+
throw new AttestationProtocolError("publicOrigin must be a string");
|
|
353
|
+
}
|
|
354
|
+
if (origin.length === 0 || utf8ByteLength(origin) > MAX_PUBLIC_ORIGIN_BYTES) {
|
|
355
|
+
throw new AttestationProtocolError(`publicOrigin must be 1-${MAX_PUBLIC_ORIGIN_BYTES} UTF-8 bytes`);
|
|
356
|
+
}
|
|
357
|
+
let url;
|
|
358
|
+
try {
|
|
359
|
+
url = new URL(origin);
|
|
360
|
+
} catch {
|
|
361
|
+
throw new AttestationProtocolError("publicOrigin must be a valid HTTP(S) origin");
|
|
362
|
+
}
|
|
363
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
364
|
+
throw new AttestationProtocolError("publicOrigin must use http or https");
|
|
365
|
+
}
|
|
366
|
+
if (url.username !== "" || url.password !== "") {
|
|
367
|
+
throw new AttestationProtocolError("publicOrigin must not contain userinfo");
|
|
368
|
+
}
|
|
369
|
+
const canonical = `${url.protocol}//${url.host}`;
|
|
370
|
+
if (origin !== canonical) {
|
|
371
|
+
throw new AttestationProtocolError(
|
|
372
|
+
"publicOrigin must be canonical (lowercase, no path, no trailing slash, no default port)"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (url.protocol === "http:" && !isLoopbackHostname(url.hostname)) {
|
|
376
|
+
throw new AttestationProtocolError("http publicOrigin is allowed only for loopback development");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function normalizeMethod(method) {
|
|
380
|
+
if (typeof method !== "string" || method.length === 0 || !HTTP_TOKEN_PATTERN.test(method)) {
|
|
381
|
+
throw new AttestationProtocolError("method must be a non-empty valid HTTP token");
|
|
382
|
+
}
|
|
383
|
+
return method.toUpperCase();
|
|
384
|
+
}
|
|
385
|
+
function normalizeContentType(contentType) {
|
|
386
|
+
if (contentType === void 0 || contentType === null) {
|
|
387
|
+
return "";
|
|
388
|
+
}
|
|
389
|
+
if (typeof contentType !== "string") {
|
|
390
|
+
throw new AttestationProtocolError("contentType must be a string");
|
|
391
|
+
}
|
|
392
|
+
const trimmed = contentType.replace(/^[ \t]+|[ \t]+$/g, "");
|
|
393
|
+
for (const char of trimmed) {
|
|
394
|
+
const code = char.codePointAt(0);
|
|
395
|
+
if (code === 9 || code === 32) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (code < 32 || code === 127) {
|
|
399
|
+
throw new AttestationProtocolError("contentType must not contain control characters");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (utf8ByteLength(trimmed) > MAX_CONTENT_TYPE_BYTES) {
|
|
403
|
+
throw new AttestationProtocolError(`contentType must be at most ${MAX_CONTENT_TYPE_BYTES} UTF-8 bytes`);
|
|
404
|
+
}
|
|
405
|
+
return trimmed;
|
|
406
|
+
}
|
|
407
|
+
function assertValidPercentEscapes(target) {
|
|
408
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
409
|
+
if (target[index] !== "%") {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const hex = target.slice(index + 1, index + 3);
|
|
413
|
+
if (!HEX_BYTE_PATTERN.test(hex)) {
|
|
414
|
+
throw new AttestationProtocolError("requestTarget contains a malformed percent escape");
|
|
415
|
+
}
|
|
416
|
+
index += 2;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function percentDecodeSegment(segment) {
|
|
420
|
+
return segment.replace(/%[0-9a-fA-F]{2}/g, (escape) => String.fromCharCode(Number.parseInt(escape.slice(1), 16)));
|
|
421
|
+
}
|
|
422
|
+
function canonicalizeRequestTarget(target) {
|
|
423
|
+
if (typeof target !== "string") {
|
|
424
|
+
throw new AttestationProtocolError("requestTarget must be a string");
|
|
425
|
+
}
|
|
426
|
+
if (target.length === 0) {
|
|
427
|
+
throw new AttestationProtocolError("requestTarget must not be empty");
|
|
428
|
+
}
|
|
429
|
+
if (utf8ByteLength(target) > MAX_REQUEST_TARGET_BYTES) {
|
|
430
|
+
throw new AttestationProtocolError(`requestTarget must be at most ${MAX_REQUEST_TARGET_BYTES} UTF-8 bytes`);
|
|
431
|
+
}
|
|
432
|
+
if (!target.startsWith("/")) {
|
|
433
|
+
throw new AttestationProtocolError("requestTarget must be an origin-form target starting with /");
|
|
434
|
+
}
|
|
435
|
+
if (target.startsWith("//")) {
|
|
436
|
+
throw new AttestationProtocolError("requestTarget must not resolve as an authority");
|
|
437
|
+
}
|
|
438
|
+
for (const char of target) {
|
|
439
|
+
const code = char.codePointAt(0);
|
|
440
|
+
if (code <= 32 || code === 127) {
|
|
441
|
+
throw new AttestationProtocolError("requestTarget must not contain controls or raw spaces");
|
|
442
|
+
}
|
|
443
|
+
if (char === "#" || char === "\\") {
|
|
444
|
+
throw new AttestationProtocolError("requestTarget must not contain fragments or backslashes");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
assertValidPercentEscapes(target);
|
|
448
|
+
const queryIndex = target.indexOf("?");
|
|
449
|
+
const path2 = queryIndex === -1 ? target : target.slice(0, queryIndex);
|
|
450
|
+
for (const segment of path2.split("/")) {
|
|
451
|
+
if (segment === "." || segment === "..") {
|
|
452
|
+
throw new AttestationProtocolError("requestTarget must not contain dot segments");
|
|
453
|
+
}
|
|
454
|
+
const decoded = percentDecodeSegment(segment).toLowerCase();
|
|
455
|
+
if (decoded === "." || decoded === "..") {
|
|
456
|
+
throw new AttestationProtocolError("requestTarget must not contain encoded dot segments");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (queryIndex === target.length - 1) {
|
|
460
|
+
return target.slice(0, queryIndex);
|
|
461
|
+
}
|
|
462
|
+
return target;
|
|
463
|
+
}
|
|
464
|
+
function validateHeaderName(name) {
|
|
465
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
466
|
+
throw new AttestationProtocolError("headerName must be a non-empty string");
|
|
467
|
+
}
|
|
468
|
+
if (utf8ByteLength(name) > MAX_HEADER_NAME_BYTES) {
|
|
469
|
+
throw new AttestationProtocolError(`headerName must be at most ${MAX_HEADER_NAME_BYTES} UTF-8 bytes`);
|
|
470
|
+
}
|
|
471
|
+
if (!HTTP_TOKEN_PATTERN.test(name)) {
|
|
472
|
+
throw new AttestationProtocolError("headerName must be a valid HTTP token");
|
|
473
|
+
}
|
|
474
|
+
if (RESERVED_HEADER_NAMES.has(name.toLowerCase())) {
|
|
475
|
+
throw new AttestationProtocolError(`headerName must not collide with reserved header ${name}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function extractSingleHeaderValue(raw) {
|
|
479
|
+
if (raw === void 0) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const values = (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry !== void 0);
|
|
483
|
+
if (values.length === 0) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
if (values.length > 1) {
|
|
487
|
+
throw new AttestationProtocolError("duplicate signature headers are not allowed");
|
|
488
|
+
}
|
|
489
|
+
const value = values[0];
|
|
490
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (value.includes(",")) {
|
|
494
|
+
throw new AttestationProtocolError("comma-joined signature headers are not allowed");
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function validateReplayKey(replayKey) {
|
|
499
|
+
if (typeof replayKey !== "string") {
|
|
500
|
+
throw new AttestationProtocolError("replayKey must be a string");
|
|
501
|
+
}
|
|
502
|
+
const length = replayKey.length;
|
|
503
|
+
if (length < MIN_REPLAY_KEY_BYTES || length > MAX_REPLAY_KEY_BYTES) {
|
|
504
|
+
throw new AttestationProtocolError(`replayKey must be ${MIN_REPLAY_KEY_BYTES}-${MAX_REPLAY_KEY_BYTES} characters`);
|
|
505
|
+
}
|
|
506
|
+
for (const char of replayKey) {
|
|
507
|
+
const code = char.codePointAt(0);
|
|
508
|
+
if (code < 32 || code > 126) {
|
|
509
|
+
throw new AttestationProtocolError("replayKey must be printable ASCII");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function buildMacInputBytes(fields) {
|
|
514
|
+
validateReplayNamespace(fields.replayNamespace);
|
|
515
|
+
validatePublicOrigin(fields.publicOrigin);
|
|
516
|
+
validateKeyId(fields.keyId);
|
|
517
|
+
assertTimestampMs("timestampMs", fields.timestampMs);
|
|
518
|
+
validateNonceHex(fields.nonceHex);
|
|
519
|
+
const method = normalizeMethod(fields.method);
|
|
520
|
+
const requestTarget = canonicalizeRequestTarget(fields.requestTarget);
|
|
521
|
+
const contentType = normalizeContentType(fields.contentType);
|
|
522
|
+
validateBodyHashHex(fields.bodyHashHex);
|
|
523
|
+
const tuple = [
|
|
524
|
+
MAC_INPUT_LABEL,
|
|
525
|
+
PROTOCOL_VERSION,
|
|
526
|
+
fields.replayNamespace,
|
|
527
|
+
fields.publicOrigin,
|
|
528
|
+
fields.keyId,
|
|
529
|
+
fields.timestampMs,
|
|
530
|
+
fields.nonceHex,
|
|
531
|
+
method,
|
|
532
|
+
requestTarget,
|
|
533
|
+
contentType,
|
|
534
|
+
fields.bodyHashHex
|
|
535
|
+
];
|
|
536
|
+
return canonicalJsonBytes(tuple);
|
|
537
|
+
}
|
|
538
|
+
function buildReplayKeyPreimageBytes(input) {
|
|
539
|
+
validateReplayNamespace(input.replayNamespace);
|
|
540
|
+
validatePublicOrigin(input.publicOrigin);
|
|
541
|
+
validateNonceHex(input.nonceHex);
|
|
542
|
+
return canonicalJsonBytes([input.replayNamespace, input.publicOrigin, input.nonceHex]);
|
|
543
|
+
}
|
|
544
|
+
function encodeTransactionId(fields) {
|
|
545
|
+
validateKeyId(fields.keyId);
|
|
546
|
+
assertTimestampMs("timestampMs", fields.timestampMs);
|
|
547
|
+
validateNonceHex(fields.nonceHex);
|
|
548
|
+
validateMac(fields.mac);
|
|
549
|
+
const encoded = base64urlEncode(
|
|
550
|
+
canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
|
|
551
|
+
);
|
|
552
|
+
if (utf8ByteLength(encoded) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
|
|
553
|
+
throw new AttestationProtocolError(
|
|
554
|
+
`encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return encoded;
|
|
558
|
+
}
|
|
559
|
+
function decodeTransactionId(headerValue) {
|
|
560
|
+
if (typeof headerValue !== "string" || headerValue.length === 0) {
|
|
561
|
+
throw new AttestationProtocolError("transactionId must be a non-empty string");
|
|
562
|
+
}
|
|
563
|
+
if (utf8ByteLength(headerValue) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
|
|
564
|
+
throw new AttestationProtocolError(
|
|
565
|
+
`encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const bytes = base64urlDecode(headerValue);
|
|
569
|
+
const text = utf8Decode(bytes);
|
|
570
|
+
const envelope = parseCanonicalJsonArray(text, 5);
|
|
571
|
+
const [version, keyId, timestampMs, nonceHex, mac] = envelope;
|
|
572
|
+
if (version !== PROTOCOL_VERSION) {
|
|
573
|
+
throw new AttestationProtocolError(`unsupported transactionId version ${String(version)}`);
|
|
574
|
+
}
|
|
575
|
+
validateKeyId(keyId);
|
|
576
|
+
assertTimestampMs("timestampMs", timestampMs);
|
|
577
|
+
validateNonceHex(nonceHex);
|
|
578
|
+
validateMac(mac);
|
|
579
|
+
return {
|
|
580
|
+
keyId,
|
|
581
|
+
timestampMs,
|
|
582
|
+
nonceHex,
|
|
583
|
+
mac
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/shared/time-policy.ts
|
|
588
|
+
function assertPolicyField(name, value, min, max) {
|
|
589
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
590
|
+
throw new AttestationProtocolError(`${name} must be a safe integer in [${min}, ${max}]`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function resolveTimePolicy(options = {}) {
|
|
594
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
595
|
+
throw new AttestationProtocolError("time policy options must be an object");
|
|
596
|
+
}
|
|
597
|
+
const allowed = /* @__PURE__ */ new Set(["maxAgeMs", "clockSkewMs", "clusterClockGuardMs"]);
|
|
598
|
+
for (const key of Object.keys(options)) {
|
|
599
|
+
if (!allowed.has(key)) {
|
|
600
|
+
throw new AttestationProtocolError(`unknown time policy option ${key}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
604
|
+
const clockSkewMs = options.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS;
|
|
605
|
+
const clusterClockGuardMs = options.clusterClockGuardMs ?? DEFAULT_CLUSTER_CLOCK_GUARD_MS;
|
|
606
|
+
assertPolicyField("maxAgeMs", maxAgeMs, MIN_MAX_AGE_MS, MAX_MAX_AGE_MS);
|
|
607
|
+
assertPolicyField("clockSkewMs", clockSkewMs, MIN_CLOCK_SKEW_MS, MAX_CLOCK_SKEW_MS);
|
|
608
|
+
assertPolicyField("clusterClockGuardMs", clusterClockGuardMs, MIN_CLUSTER_CLOCK_GUARD_MS, MAX_CLUSTER_CLOCK_GUARD_MS);
|
|
609
|
+
return Object.freeze({ maxAgeMs, clockSkewMs, clusterClockGuardMs });
|
|
610
|
+
}
|
|
611
|
+
function evaluateProofTiming(input) {
|
|
612
|
+
assertTimestampMs("timestampMs", input.timestampMs);
|
|
613
|
+
assertTimestampMs("nowMs", input.nowMs);
|
|
614
|
+
assertTimestampMs("acceptFrom", input.acceptFrom);
|
|
615
|
+
assertTimestampMs("acceptUntil", input.acceptUntil);
|
|
616
|
+
if (input.acceptFrom >= input.acceptUntil) {
|
|
617
|
+
throw new AttestationProtocolError("key acceptance window must satisfy acceptFrom < acceptUntil");
|
|
618
|
+
}
|
|
619
|
+
const { timestampMs, nowMs, acceptFrom, acceptUntil, policy } = input;
|
|
620
|
+
const active = acceptFrom <= nowMs && nowMs < acceptUntil;
|
|
621
|
+
if (!active) {
|
|
622
|
+
const timeDeadlineMs2 = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
|
|
623
|
+
const proofDeadlineMs2 = timeDeadlineMs2 === null ? acceptUntil : Math.min(timeDeadlineMs2, acceptUntil);
|
|
624
|
+
return {
|
|
625
|
+
decision: "stale-key",
|
|
626
|
+
timeDeadlineMs: timeDeadlineMs2,
|
|
627
|
+
proofDeadlineMs: proofDeadlineMs2,
|
|
628
|
+
retainUntilMs: safeAdd(proofDeadlineMs2, policy.clusterClockGuardMs)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (timestampMs > nowMs + policy.clockSkewMs) {
|
|
632
|
+
const timeDeadlineMs2 = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
|
|
633
|
+
const proofDeadlineMs2 = timeDeadlineMs2 === null ? acceptUntil : Math.min(timeDeadlineMs2, acceptUntil);
|
|
634
|
+
return {
|
|
635
|
+
decision: "future",
|
|
636
|
+
timeDeadlineMs: timeDeadlineMs2,
|
|
637
|
+
proofDeadlineMs: proofDeadlineMs2,
|
|
638
|
+
retainUntilMs: safeAdd(proofDeadlineMs2, policy.clusterClockGuardMs)
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const timeDeadlineMs = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
|
|
642
|
+
if (timeDeadlineMs === null) {
|
|
643
|
+
return { decision: "expired", timeDeadlineMs: null, proofDeadlineMs: null, retainUntilMs: null };
|
|
644
|
+
}
|
|
645
|
+
const proofDeadlineMs = Math.min(timeDeadlineMs, acceptUntil);
|
|
646
|
+
if (nowMs >= proofDeadlineMs) {
|
|
647
|
+
return {
|
|
648
|
+
decision: "expired",
|
|
649
|
+
timeDeadlineMs,
|
|
650
|
+
proofDeadlineMs,
|
|
651
|
+
retainUntilMs: safeAdd(proofDeadlineMs, policy.clusterClockGuardMs)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
decision: "valid",
|
|
656
|
+
timeDeadlineMs,
|
|
657
|
+
proofDeadlineMs,
|
|
658
|
+
retainUntilMs: safeAdd(proofDeadlineMs, policy.clusterClockGuardMs)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function safeAdd(first, second) {
|
|
662
|
+
const sum = first + second;
|
|
663
|
+
return Number.isSafeInteger(sum) ? sum : null;
|
|
664
|
+
}
|
|
665
|
+
function safeAdd3(first, second, third) {
|
|
666
|
+
const sum = first + second + third;
|
|
667
|
+
return Number.isSafeInteger(sum) ? sum : null;
|
|
668
|
+
}
|
|
669
|
+
function maxVerifierRemainingMs(policy) {
|
|
670
|
+
return policy.maxAgeMs + 2 * policy.clockSkewMs + policy.clusterClockGuardMs;
|
|
671
|
+
}
|
|
672
|
+
function maxStoreAdmissionMs(policy) {
|
|
673
|
+
return Math.min(GLOBAL_MAX_RETENTION_MS, policy.maxAgeMs + 2 * policy.clockSkewMs + 2 * policy.clusterClockGuardMs);
|
|
674
|
+
}
|
|
675
|
+
function checkRetentionAdmissible(input) {
|
|
676
|
+
assertTimestampMs("retainUntilMs", input.retainUntilMs);
|
|
677
|
+
assertTimestampMs("nowMs", input.nowMs);
|
|
678
|
+
const { retainUntilMs, nowMs, policy } = input;
|
|
679
|
+
if (retainUntilMs <= nowMs) {
|
|
680
|
+
return "expired";
|
|
681
|
+
}
|
|
682
|
+
const remaining = retainUntilMs - nowMs;
|
|
683
|
+
if (!Number.isSafeInteger(remaining)) {
|
|
684
|
+
return "overlong";
|
|
685
|
+
}
|
|
686
|
+
if (remaining > GLOBAL_MAX_RETENTION_MS) {
|
|
687
|
+
return "overlong";
|
|
688
|
+
}
|
|
689
|
+
if (remaining > maxStoreAdmissionMs(policy)) {
|
|
690
|
+
return "overlong";
|
|
691
|
+
}
|
|
692
|
+
return "admissible";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/server-types.ts
|
|
696
|
+
var ERROR_DETAILS = {
|
|
697
|
+
ATTESTATION_MISSING: { status: 403, message: "Request signature is required." },
|
|
698
|
+
ATTESTATION_MALFORMED: { status: 403, message: "Request signature is invalid." },
|
|
699
|
+
ATTESTATION_STALE_KEY: { status: 403, message: "Request signing key is no longer accepted." },
|
|
700
|
+
ATTESTATION_EXPIRED: { status: 403, message: "Request signature has expired." },
|
|
701
|
+
ATTESTATION_FUTURE: { status: 403, message: "Request signature timestamp is ahead of the allowed clock window." },
|
|
702
|
+
ATTESTATION_INVALID_SIGNATURE: { status: 403, message: "Request signature does not match." },
|
|
703
|
+
ATTESTATION_REPLAY: { status: 403, message: "Request signature was already used." },
|
|
704
|
+
ATTESTATION_BODY_TOO_LARGE: { status: 413, message: "Request body exceeds the configured byte limit." },
|
|
705
|
+
ATTESTATION_UNSUPPORTED_ENCODING: { status: 415, message: "Request body encoding is unsupported." },
|
|
706
|
+
ATTESTATION_BODY_CAPTURE_REQUIRED: { status: 500, message: "Request body capture is not configured." },
|
|
707
|
+
ATTESTATION_INTERNAL_ERROR: { status: 500, message: "Request signature verification failed." },
|
|
708
|
+
ATTESTATION_KEYS_UNAVAILABLE: { status: 503, message: "Request signing keys are unavailable." },
|
|
709
|
+
ATTESTATION_REPLAY_UNAVAILABLE: { status: 503, message: "Request replay protection is unavailable." }
|
|
710
|
+
};
|
|
711
|
+
var AttestationError = class extends Error {
|
|
712
|
+
name = "AttestationError";
|
|
713
|
+
code;
|
|
714
|
+
status;
|
|
715
|
+
constructor(code) {
|
|
716
|
+
super(ERROR_DETAILS[code].message);
|
|
717
|
+
this.code = code;
|
|
718
|
+
this.status = ERROR_DETAILS[code].status;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function isAttestationError(error) {
|
|
722
|
+
return error instanceof AttestationError;
|
|
723
|
+
}
|
|
724
|
+
function toAttestationErrorPayload(error) {
|
|
725
|
+
return { code: error.code, message: error.message };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/body-capture.ts
|
|
729
|
+
var import_node_crypto = require("crypto");
|
|
730
|
+
var BODY_CAPTURE_OPTION_KEYS = /* @__PURE__ */ new Set(["maxBodyBytes"]);
|
|
731
|
+
var bodyRecords = /* @__PURE__ */ new WeakMap();
|
|
732
|
+
function toBytes(body) {
|
|
733
|
+
if (body === null || body === void 0) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) {
|
|
737
|
+
return new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
|
|
738
|
+
}
|
|
739
|
+
if (body instanceof Uint8Array) {
|
|
740
|
+
return new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
|
|
741
|
+
}
|
|
742
|
+
if (typeof body === "string") {
|
|
743
|
+
return new TextEncoder().encode(body);
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
function getAttestationBodyRecord(req) {
|
|
748
|
+
if (req === null || typeof req !== "object" && typeof req !== "function") {
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
const record = bodyRecords.get(req);
|
|
752
|
+
if (record === void 0) {
|
|
753
|
+
return void 0;
|
|
754
|
+
}
|
|
755
|
+
return Object.freeze({ digestHex: record.digestHex, byteLength: record.byteLength });
|
|
756
|
+
}
|
|
757
|
+
function isParserSizeError(error) {
|
|
758
|
+
if (error === null || typeof error !== "object") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const record = error;
|
|
762
|
+
if (record.type === "entity.too.large") {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
function isParserEncodingError(error) {
|
|
768
|
+
if (error === null || typeof error !== "object") {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
const record = error;
|
|
772
|
+
return record.type === "charset.unsupported" || record.type === "encoding.unsupported";
|
|
773
|
+
}
|
|
774
|
+
function sendCaptureError(res, status, code) {
|
|
775
|
+
const error = new AttestationError(code);
|
|
776
|
+
const target = res;
|
|
777
|
+
if (typeof target.setHeader === "function") {
|
|
778
|
+
target.setHeader("Cache-Control", "no-store");
|
|
779
|
+
} else if (typeof target.set === "function") {
|
|
780
|
+
target.set("Cache-Control", "no-store");
|
|
781
|
+
}
|
|
782
|
+
if (typeof target.status === "function") {
|
|
783
|
+
target.status(status).json({ code: error.code, message: error.message });
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
throw new AttestationProtocolError("attestation body capture error handler requires an Express response");
|
|
787
|
+
}
|
|
788
|
+
function createAttestationBodyCapture(options = {}) {
|
|
789
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
790
|
+
throw new AttestationProtocolError("attestation body capture options must be an object");
|
|
791
|
+
}
|
|
792
|
+
for (const key of Object.keys(options)) {
|
|
793
|
+
if (!BODY_CAPTURE_OPTION_KEYS.has(key)) {
|
|
794
|
+
throw new AttestationProtocolError(`unknown attestation body capture option ${key}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
798
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {
|
|
799
|
+
throw new AttestationProtocolError("maxBodyBytes must be a positive safe integer");
|
|
800
|
+
}
|
|
801
|
+
if (maxBodyBytes > MAX_BODY_BYTES_HARD_CAP) {
|
|
802
|
+
throw new AttestationProtocolError(
|
|
803
|
+
`maxBodyBytes must be at most ${MAX_BODY_BYTES_HARD_CAP} bytes (16 MiB v1 hard cap)`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const verify = (req, _res, body, _encoding) => {
|
|
807
|
+
void _encoding;
|
|
808
|
+
if (req === null || typeof req !== "object" && typeof req !== "function") {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const bytes = toBytes(body);
|
|
812
|
+
if (bytes === null) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (bytes.length > maxBodyBytes) {
|
|
816
|
+
const overflow = new Error("Request body exceeds the configured byte limit.");
|
|
817
|
+
overflow.type = "entity.too.large";
|
|
818
|
+
overflow.status = 413;
|
|
819
|
+
overflow.statusCode = 413;
|
|
820
|
+
throw overflow;
|
|
821
|
+
}
|
|
822
|
+
const digestHex = (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
823
|
+
bodyRecords.set(req, Object.freeze({ digestHex, byteLength: bytes.length }));
|
|
824
|
+
};
|
|
825
|
+
const errorHandler = (err, _req, res, next) => {
|
|
826
|
+
if (err === null || err === void 0) {
|
|
827
|
+
next();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const target = res;
|
|
831
|
+
if (target?.headersSent === true) {
|
|
832
|
+
next(err);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (isParserSizeError(err)) {
|
|
836
|
+
sendCaptureError(res, 413, "ATTESTATION_BODY_TOO_LARGE");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (isParserEncodingError(err)) {
|
|
840
|
+
sendCaptureError(res, 415, "ATTESTATION_UNSUPPORTED_ENCODING");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
next(err);
|
|
844
|
+
};
|
|
845
|
+
const getRecord = (req) => getAttestationBodyRecord(req);
|
|
846
|
+
return Object.freeze({
|
|
847
|
+
verify,
|
|
848
|
+
errorHandler,
|
|
849
|
+
getRecord,
|
|
850
|
+
maxBodyBytes
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/verify.ts
|
|
855
|
+
var import_node_crypto2 = require("crypto");
|
|
856
|
+
function fail(code) {
|
|
857
|
+
throw new AttestationError(code);
|
|
858
|
+
}
|
|
859
|
+
function copyAttestationSnapshot(snapshot) {
|
|
860
|
+
try {
|
|
861
|
+
if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
862
|
+
throw new AttestationProtocolError("key snapshot must be an object");
|
|
863
|
+
}
|
|
864
|
+
const record = snapshot;
|
|
865
|
+
validateKeyId(record.currentKeyId);
|
|
866
|
+
if (!Array.isArray(record.keys)) {
|
|
867
|
+
throw new AttestationProtocolError("key snapshot keys must be an array");
|
|
868
|
+
}
|
|
869
|
+
if (record.keys.length === 0 || record.keys.length > 16) {
|
|
870
|
+
throw new AttestationProtocolError("key snapshot must retain 1-16 keys");
|
|
871
|
+
}
|
|
872
|
+
const seen = /* @__PURE__ */ new Set();
|
|
873
|
+
const copied = record.keys.map((entry) => {
|
|
874
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
875
|
+
throw new AttestationProtocolError("key snapshot entry must be an object");
|
|
876
|
+
}
|
|
877
|
+
const row = entry;
|
|
878
|
+
validateKeyId(row.keyId);
|
|
879
|
+
validateKeyBytes(row.key);
|
|
880
|
+
assertTimestampMs("acceptFrom", row.acceptFrom);
|
|
881
|
+
assertTimestampMs("acceptUntil", row.acceptUntil);
|
|
882
|
+
const acceptFrom = row.acceptFrom;
|
|
883
|
+
const acceptUntil = row.acceptUntil;
|
|
884
|
+
if (acceptFrom >= acceptUntil) {
|
|
885
|
+
throw new AttestationProtocolError("key acceptance window must satisfy acceptFrom < acceptUntil");
|
|
886
|
+
}
|
|
887
|
+
const keyId = row.keyId;
|
|
888
|
+
if (seen.has(keyId)) {
|
|
889
|
+
throw new AttestationProtocolError("key snapshot keyIds must be unique");
|
|
890
|
+
}
|
|
891
|
+
seen.add(keyId);
|
|
892
|
+
const key = new Uint8Array(row.key);
|
|
893
|
+
return Object.freeze({ keyId, key, acceptFrom, acceptUntil });
|
|
894
|
+
});
|
|
895
|
+
if (!seen.has(record.currentKeyId)) {
|
|
896
|
+
throw new AttestationProtocolError("key snapshot currentKeyId must name a present entry");
|
|
897
|
+
}
|
|
898
|
+
const currentKeyId = record.currentKeyId;
|
|
899
|
+
return Object.freeze({ currentKeyId, keys: Object.freeze(copied) });
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (error instanceof AttestationError) {
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
fail("ATTESTATION_KEYS_UNAVAILABLE");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function checkProofTiming(input) {
|
|
908
|
+
let timing;
|
|
909
|
+
try {
|
|
910
|
+
timing = evaluateProofTiming({
|
|
911
|
+
timestampMs: input.timestampMs,
|
|
912
|
+
nowMs: input.nowMs,
|
|
913
|
+
acceptFrom: input.keyEntry.acceptFrom,
|
|
914
|
+
acceptUntil: input.keyEntry.acceptUntil,
|
|
915
|
+
policy: input.policy
|
|
916
|
+
});
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error instanceof AttestationError) {
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
fail("ATTESTATION_INTERNAL_ERROR");
|
|
922
|
+
}
|
|
923
|
+
const decision = timing.decision;
|
|
924
|
+
if (decision === "valid") {
|
|
925
|
+
return timing;
|
|
926
|
+
}
|
|
927
|
+
if (decision === "future") {
|
|
928
|
+
fail("ATTESTATION_FUTURE");
|
|
929
|
+
}
|
|
930
|
+
if (decision === "expired") {
|
|
931
|
+
fail("ATTESTATION_EXPIRED");
|
|
932
|
+
}
|
|
933
|
+
fail("ATTESTATION_STALE_KEY");
|
|
934
|
+
}
|
|
935
|
+
function resolveRequestTarget(originalUrl, publicPathPrefix) {
|
|
936
|
+
try {
|
|
937
|
+
if (typeof originalUrl !== "string" || originalUrl.length === 0) {
|
|
938
|
+
throw new AttestationProtocolError("request target must be a non-empty string");
|
|
939
|
+
}
|
|
940
|
+
const full = `${publicPathPrefix}${originalUrl}`;
|
|
941
|
+
return canonicalizeRequestTarget(full);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
if (error instanceof AttestationError) {
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
946
|
+
fail("ATTESTATION_MALFORMED");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function resolveContentType(header) {
|
|
950
|
+
try {
|
|
951
|
+
if (header === void 0 || header === null) {
|
|
952
|
+
return "";
|
|
953
|
+
}
|
|
954
|
+
if (Array.isArray(header)) {
|
|
955
|
+
if (header.length === 0) {
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
if (header.length > 1) {
|
|
959
|
+
throw new AttestationProtocolError("duplicate content-type headers are not allowed");
|
|
960
|
+
}
|
|
961
|
+
return normalizeContentType(header[0]);
|
|
962
|
+
}
|
|
963
|
+
return normalizeContentType(header);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
if (error instanceof AttestationError) {
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
fail("ATTESTATION_MALFORMED");
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function assertAllowedContentEncoding(header) {
|
|
972
|
+
if (header === void 0 || header === null) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const values = Array.isArray(header) ? [...header] : [header];
|
|
976
|
+
const tokens = [];
|
|
977
|
+
for (const value of values) {
|
|
978
|
+
if (typeof value !== "string") {
|
|
979
|
+
fail("ATTESTATION_MALFORMED");
|
|
980
|
+
}
|
|
981
|
+
for (const token of value.split(",")) {
|
|
982
|
+
const trimmed = token.trim();
|
|
983
|
+
if (trimmed === "") {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
tokens.push(trimmed.toLowerCase());
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const token of tokens) {
|
|
990
|
+
if (token !== "identity") {
|
|
991
|
+
fail("ATTESTATION_UNSUPPORTED_ENCODING");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function resolveBodyDigest(digest) {
|
|
996
|
+
try {
|
|
997
|
+
validateBodyHashHex(digest);
|
|
998
|
+
return digest;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
if (error instanceof AttestationError) {
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
fail("ATTESTATION_INTERNAL_ERROR");
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function deriveReplayKey(input) {
|
|
1007
|
+
const preimage = buildReplayKeyPreimageBytes(input);
|
|
1008
|
+
const digest = (0, import_node_crypto2.createHash)("sha256").update(preimage).digest("hex");
|
|
1009
|
+
return `${REPLAY_KEY_PREFIX}${digest}`;
|
|
1010
|
+
}
|
|
1011
|
+
function verifyAttestationProof(input) {
|
|
1012
|
+
let headerValue;
|
|
1013
|
+
try {
|
|
1014
|
+
headerValue = extractSingleHeaderValue(input.headerValue);
|
|
1015
|
+
} catch {
|
|
1016
|
+
fail("ATTESTATION_MALFORMED");
|
|
1017
|
+
}
|
|
1018
|
+
if (headerValue === null) {
|
|
1019
|
+
fail("ATTESTATION_MISSING");
|
|
1020
|
+
}
|
|
1021
|
+
let envelope;
|
|
1022
|
+
try {
|
|
1023
|
+
envelope = decodeTransactionId(headerValue);
|
|
1024
|
+
} catch {
|
|
1025
|
+
fail("ATTESTATION_MALFORMED");
|
|
1026
|
+
}
|
|
1027
|
+
const snapshot = copyAttestationSnapshot(input.snapshot);
|
|
1028
|
+
const keyEntry = snapshot.keys.find((entry) => entry.keyId === envelope.keyId);
|
|
1029
|
+
if (keyEntry === void 0) {
|
|
1030
|
+
fail("ATTESTATION_STALE_KEY");
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
assertTimestampMs("nowMs", input.nowMs);
|
|
1034
|
+
} catch {
|
|
1035
|
+
fail("ATTESTATION_INTERNAL_ERROR");
|
|
1036
|
+
}
|
|
1037
|
+
const timing = checkProofTiming({
|
|
1038
|
+
timestampMs: envelope.timestampMs,
|
|
1039
|
+
nowMs: input.nowMs,
|
|
1040
|
+
keyEntry,
|
|
1041
|
+
policy: input.policy
|
|
1042
|
+
});
|
|
1043
|
+
const requestTarget = resolveRequestTarget(input.originalUrl, input.publicPathPrefix ?? "");
|
|
1044
|
+
let method;
|
|
1045
|
+
try {
|
|
1046
|
+
method = normalizeMethod(input.method);
|
|
1047
|
+
} catch {
|
|
1048
|
+
fail("ATTESTATION_MALFORMED");
|
|
1049
|
+
}
|
|
1050
|
+
assertAllowedContentEncoding(input.contentEncodingHeader);
|
|
1051
|
+
const contentType = resolveContentType(input.contentTypeHeader);
|
|
1052
|
+
const bodyHashHex = resolveBodyDigest(input.bodyDigestHex);
|
|
1053
|
+
let macInput;
|
|
1054
|
+
try {
|
|
1055
|
+
macInput = buildMacInputBytes({
|
|
1056
|
+
replayNamespace: input.replayNamespace,
|
|
1057
|
+
publicOrigin: input.publicOrigin,
|
|
1058
|
+
keyId: envelope.keyId,
|
|
1059
|
+
timestampMs: envelope.timestampMs,
|
|
1060
|
+
nonceHex: envelope.nonceHex,
|
|
1061
|
+
method,
|
|
1062
|
+
requestTarget,
|
|
1063
|
+
contentType,
|
|
1064
|
+
bodyHashHex
|
|
1065
|
+
});
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
if (error instanceof AttestationError) {
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
fail("ATTESTATION_INTERNAL_ERROR");
|
|
1071
|
+
}
|
|
1072
|
+
let presented;
|
|
1073
|
+
try {
|
|
1074
|
+
presented = base64urlDecode(envelope.mac);
|
|
1075
|
+
} catch {
|
|
1076
|
+
fail("ATTESTATION_MALFORMED");
|
|
1077
|
+
}
|
|
1078
|
+
if (presented.length !== 32) {
|
|
1079
|
+
fail("ATTESTATION_INVALID_SIGNATURE");
|
|
1080
|
+
}
|
|
1081
|
+
const expected = (0, import_node_crypto2.createHmac)("sha256", Buffer.from(keyEntry.key)).update(macInput).digest();
|
|
1082
|
+
const presentedBuf = Buffer.from(presented);
|
|
1083
|
+
if (presentedBuf.length !== expected.length || !(0, import_node_crypto2.timingSafeEqual)(presentedBuf, expected)) {
|
|
1084
|
+
fail("ATTESTATION_INVALID_SIGNATURE");
|
|
1085
|
+
}
|
|
1086
|
+
let replayKey;
|
|
1087
|
+
try {
|
|
1088
|
+
replayKey = deriveReplayKey({
|
|
1089
|
+
replayNamespace: input.replayNamespace,
|
|
1090
|
+
publicOrigin: input.publicOrigin,
|
|
1091
|
+
nonceHex: envelope.nonceHex
|
|
1092
|
+
});
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error instanceof AttestationError) {
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
fail("ATTESTATION_INTERNAL_ERROR");
|
|
1098
|
+
}
|
|
1099
|
+
const retainUntilMs = timing.retainUntilMs;
|
|
1100
|
+
if (retainUntilMs === null || !Number.isSafeInteger(retainUntilMs)) {
|
|
1101
|
+
fail("ATTESTATION_EXPIRED");
|
|
1102
|
+
}
|
|
1103
|
+
return Object.freeze({
|
|
1104
|
+
keyId: envelope.keyId,
|
|
1105
|
+
timestampMs: envelope.timestampMs,
|
|
1106
|
+
nonceHex: envelope.nonceHex,
|
|
1107
|
+
keyEntry,
|
|
1108
|
+
timing,
|
|
1109
|
+
replayKey,
|
|
1110
|
+
retainUntilMs,
|
|
1111
|
+
method,
|
|
1112
|
+
requestTarget,
|
|
1113
|
+
contentType,
|
|
1114
|
+
bodyHashHex
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/stores/memory.ts
|
|
1119
|
+
var MEMORY_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES = 5e4;
|
|
1120
|
+
var EXPIRY_SWEEP_BATCH_SIZE = 64;
|
|
1121
|
+
var STORE_OPTION_KEYS = /* @__PURE__ */ new Set(["maxEntries", "now"]);
|
|
1122
|
+
var AttestationCapacityError = class extends Error {
|
|
1123
|
+
name = "AttestationCapacityError";
|
|
1124
|
+
constructor(message = "Attestation replay store capacity is exhausted.") {
|
|
1125
|
+
super(message);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var MemoryAttestationStore = class {
|
|
1129
|
+
reservations = /* @__PURE__ */ new Map();
|
|
1130
|
+
expiryHeap = [];
|
|
1131
|
+
maxEntries;
|
|
1132
|
+
now;
|
|
1133
|
+
constructor(options = {}) {
|
|
1134
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1135
|
+
throw new AttestationProtocolError("memory attestation store options must be an object");
|
|
1136
|
+
}
|
|
1137
|
+
for (const key of Object.keys(options)) {
|
|
1138
|
+
if (!STORE_OPTION_KEYS.has(key)) {
|
|
1139
|
+
throw new AttestationProtocolError(`unknown memory attestation store option ${key}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const maxEntries = options.maxEntries === void 0 ? MEMORY_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES : options.maxEntries;
|
|
1143
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) {
|
|
1144
|
+
throw new AttestationProtocolError("memory attestation store maxEntries must be a positive safe integer");
|
|
1145
|
+
}
|
|
1146
|
+
const now = options.now === void 0 ? () => Date.now() : options.now;
|
|
1147
|
+
if (typeof now !== "function") {
|
|
1148
|
+
throw new AttestationProtocolError("memory attestation store now must be a function");
|
|
1149
|
+
}
|
|
1150
|
+
this.maxEntries = maxEntries;
|
|
1151
|
+
this.now = now;
|
|
1152
|
+
}
|
|
1153
|
+
async reserve(input) {
|
|
1154
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1155
|
+
throw new AttestationProtocolError("attestation store reserve input must be an object");
|
|
1156
|
+
}
|
|
1157
|
+
const record = input;
|
|
1158
|
+
const replayKey = record.replayKey;
|
|
1159
|
+
const retainUntilMs = record.retainUntilMs ?? record.retainUntil;
|
|
1160
|
+
const nowMs = this.now();
|
|
1161
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
1162
|
+
throw new AttestationProtocolError("attestation store clock must return a non-negative safe integer");
|
|
1163
|
+
}
|
|
1164
|
+
validateReplayKey(replayKey);
|
|
1165
|
+
if (retainUntilMs === void 0) {
|
|
1166
|
+
throw new AttestationProtocolError("retainUntilMs must be a non-negative safe integer");
|
|
1167
|
+
}
|
|
1168
|
+
assertTimestampMs("retainUntilMs", retainUntilMs);
|
|
1169
|
+
if (retainUntilMs <= nowMs) {
|
|
1170
|
+
return "expired";
|
|
1171
|
+
}
|
|
1172
|
+
const remainingMs = retainUntilMs - nowMs;
|
|
1173
|
+
if (remainingMs > GLOBAL_MAX_RETENTION_MS) {
|
|
1174
|
+
throw new AttestationProtocolError(
|
|
1175
|
+
`retainUntilMs exceeds the ${GLOBAL_MAX_RETENTION_MS} ms global retention cap`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
const existing = this.reservations.get(replayKey);
|
|
1179
|
+
if (existing !== void 0 && existing.retainUntilMs > nowMs) {
|
|
1180
|
+
return "duplicate";
|
|
1181
|
+
}
|
|
1182
|
+
if (existing !== void 0) {
|
|
1183
|
+
this.remove(existing.index);
|
|
1184
|
+
}
|
|
1185
|
+
for (let cleaned = 0; cleaned < EXPIRY_SWEEP_BATCH_SIZE && this.expiryHeap[0] !== void 0 && this.expiryHeap[0].retainUntilMs <= nowMs; cleaned += 1) {
|
|
1186
|
+
this.remove(0);
|
|
1187
|
+
}
|
|
1188
|
+
if (this.reservations.size >= this.maxEntries) {
|
|
1189
|
+
throw new AttestationCapacityError();
|
|
1190
|
+
}
|
|
1191
|
+
const reservation = {
|
|
1192
|
+
replayKey,
|
|
1193
|
+
retainUntilMs,
|
|
1194
|
+
index: this.expiryHeap.length
|
|
1195
|
+
};
|
|
1196
|
+
this.reservations.set(replayKey, reservation);
|
|
1197
|
+
this.expiryHeap.push(reservation);
|
|
1198
|
+
this.moveUp(reservation.index);
|
|
1199
|
+
return "reserved";
|
|
1200
|
+
}
|
|
1201
|
+
remove(index) {
|
|
1202
|
+
const removed = this.expiryHeap[index];
|
|
1203
|
+
const last = this.expiryHeap.pop();
|
|
1204
|
+
this.reservations.delete(removed.replayKey);
|
|
1205
|
+
if (index === this.expiryHeap.length) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
this.expiryHeap[index] = last;
|
|
1209
|
+
last.index = index;
|
|
1210
|
+
this.moveUp(index);
|
|
1211
|
+
this.moveDown(last.index);
|
|
1212
|
+
}
|
|
1213
|
+
swap(left, right) {
|
|
1214
|
+
const leftRow = this.expiryHeap[left];
|
|
1215
|
+
const rightRow = this.expiryHeap[right];
|
|
1216
|
+
this.expiryHeap[left] = rightRow;
|
|
1217
|
+
this.expiryHeap[right] = leftRow;
|
|
1218
|
+
rightRow.index = left;
|
|
1219
|
+
leftRow.index = right;
|
|
1220
|
+
}
|
|
1221
|
+
moveUp(index) {
|
|
1222
|
+
while (index > 0) {
|
|
1223
|
+
const parent = Math.floor((index - 1) / 2);
|
|
1224
|
+
if (this.expiryHeap[parent].retainUntilMs <= this.expiryHeap[index].retainUntilMs) {
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
this.swap(parent, index);
|
|
1228
|
+
index = parent;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
moveDown(index) {
|
|
1232
|
+
for (; ; ) {
|
|
1233
|
+
const left = index * 2 + 1;
|
|
1234
|
+
if (left >= this.expiryHeap.length) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const right = left + 1;
|
|
1238
|
+
const child = right < this.expiryHeap.length && this.expiryHeap[right].retainUntilMs < this.expiryHeap[left].retainUntilMs ? right : left;
|
|
1239
|
+
if (this.expiryHeap[index].retainUntilMs <= this.expiryHeap[child].retainUntilMs) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
this.swap(index, child);
|
|
1243
|
+
index = child;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
function createMemoryAttestationStore(options = {}) {
|
|
1248
|
+
return new MemoryAttestationStore(options);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/middleware.ts
|
|
1252
|
+
var MIDDLEWARE_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
1253
|
+
"publicOrigin",
|
|
1254
|
+
"replayNamespace",
|
|
1255
|
+
"publicPathPrefix",
|
|
1256
|
+
"keyProvider",
|
|
1257
|
+
"store",
|
|
1258
|
+
"maxAgeMs",
|
|
1259
|
+
"clockSkewMs",
|
|
1260
|
+
"clusterClockGuardMs",
|
|
1261
|
+
"headerName",
|
|
1262
|
+
"now",
|
|
1263
|
+
"operationTimeoutMs",
|
|
1264
|
+
"onDiagnostic"
|
|
1265
|
+
]);
|
|
1266
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 5e3;
|
|
1267
|
+
var MIN_OPERATION_TIMEOUT_MS = 1;
|
|
1268
|
+
var MAX_OPERATION_TIMEOUT_MS = 6e4;
|
|
1269
|
+
var AttestationOperationTimeoutError = class extends Error {
|
|
1270
|
+
name = "AttestationOperationTimeoutError";
|
|
1271
|
+
};
|
|
1272
|
+
function validatePublicPathPrefix(value) {
|
|
1273
|
+
if (value === void 0) {
|
|
1274
|
+
return "";
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof value !== "string") {
|
|
1277
|
+
throw new AttestationProtocolError("publicPathPrefix must be a string");
|
|
1278
|
+
}
|
|
1279
|
+
if (value === "") {
|
|
1280
|
+
return "";
|
|
1281
|
+
}
|
|
1282
|
+
if (!value.startsWith("/")) {
|
|
1283
|
+
throw new AttestationProtocolError("publicPathPrefix must start with /");
|
|
1284
|
+
}
|
|
1285
|
+
if (value.endsWith("/")) {
|
|
1286
|
+
throw new AttestationProtocolError("publicPathPrefix must not end with /");
|
|
1287
|
+
}
|
|
1288
|
+
if (value.includes("?") || value.includes("#") || value.includes("\\")) {
|
|
1289
|
+
throw new AttestationProtocolError("publicPathPrefix must not contain query, fragment, or backslashes");
|
|
1290
|
+
}
|
|
1291
|
+
for (const char of value) {
|
|
1292
|
+
const code = char.codePointAt(0);
|
|
1293
|
+
if (code <= 32 || code === 127) {
|
|
1294
|
+
throw new AttestationProtocolError("publicPathPrefix must not contain controls or spaces");
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return value;
|
|
1298
|
+
}
|
|
1299
|
+
function checkedNow(now) {
|
|
1300
|
+
let value;
|
|
1301
|
+
try {
|
|
1302
|
+
value = now();
|
|
1303
|
+
} catch {
|
|
1304
|
+
throw new AttestationError("ATTESTATION_INTERNAL_ERROR");
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
assertTimestampMs("nowMs", value);
|
|
1308
|
+
} catch {
|
|
1309
|
+
throw new AttestationError("ATTESTATION_INTERNAL_ERROR");
|
|
1310
|
+
}
|
|
1311
|
+
return value;
|
|
1312
|
+
}
|
|
1313
|
+
function withOperationTimeout(task, ms) {
|
|
1314
|
+
let timer;
|
|
1315
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1316
|
+
timer = setTimeout(() => {
|
|
1317
|
+
reject(new AttestationOperationTimeoutError(`operation timed out after ${ms} ms`));
|
|
1318
|
+
}, ms);
|
|
1319
|
+
if (typeof timer.unref === "function") {
|
|
1320
|
+
timer.unref();
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
const raced = Promise.race([task, timeout]);
|
|
1324
|
+
return raced.finally(() => {
|
|
1325
|
+
if (timer !== void 0) {
|
|
1326
|
+
clearTimeout(timer);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function emitDiagnostic(hook, error) {
|
|
1331
|
+
if (hook === void 0) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
try {
|
|
1335
|
+
hook({ code: error.code, status: error.status });
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function sendControlledError(res, error, hooks) {
|
|
1340
|
+
emitDiagnostic(hooks.onDiagnostic, error);
|
|
1341
|
+
const target = res;
|
|
1342
|
+
if (target?.headersSent === true) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (typeof target.setHeader === "function") {
|
|
1346
|
+
target.setHeader("Cache-Control", "no-store");
|
|
1347
|
+
} else if (typeof target.set === "function") {
|
|
1348
|
+
target.set("Cache-Control", "no-store");
|
|
1349
|
+
}
|
|
1350
|
+
if (error.code === "ATTESTATION_STALE_KEY") {
|
|
1351
|
+
if (typeof target.setHeader === "function") {
|
|
1352
|
+
target.setHeader("X-Attestation-Error", "stale-key");
|
|
1353
|
+
} else if (typeof target.set === "function") {
|
|
1354
|
+
target.set("X-Attestation-Error", "stale-key");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (typeof target.status === "function") {
|
|
1358
|
+
target.status(error.status).json({ code: error.code, message: error.message });
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function extractRawHeader(req, lowerName) {
|
|
1362
|
+
const record = req;
|
|
1363
|
+
const headers = record.headers;
|
|
1364
|
+
const rawHeaders = record.rawHeaders;
|
|
1365
|
+
let rawCount = 0;
|
|
1366
|
+
let firstRaw;
|
|
1367
|
+
if (Array.isArray(rawHeaders)) {
|
|
1368
|
+
for (let index = 0; index + 1 < rawHeaders.length; index += 2) {
|
|
1369
|
+
const name = rawHeaders[index];
|
|
1370
|
+
const value2 = rawHeaders[index + 1];
|
|
1371
|
+
if (typeof name === "string" && name.toLowerCase() === lowerName) {
|
|
1372
|
+
rawCount += 1;
|
|
1373
|
+
if (firstRaw === void 0 && typeof value2 === "string") {
|
|
1374
|
+
firstRaw = value2;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (rawCount > 1) {
|
|
1380
|
+
throw new AttestationError("ATTESTATION_MALFORMED");
|
|
1381
|
+
}
|
|
1382
|
+
const value = headers?.[lowerName];
|
|
1383
|
+
if (value === void 0) {
|
|
1384
|
+
if (rawCount === 1 && firstRaw !== void 0) {
|
|
1385
|
+
return firstRaw;
|
|
1386
|
+
}
|
|
1387
|
+
return void 0;
|
|
1388
|
+
}
|
|
1389
|
+
if (Array.isArray(value)) {
|
|
1390
|
+
const filtered = value.filter((entry) => entry !== void 0);
|
|
1391
|
+
if (filtered.length === 0) {
|
|
1392
|
+
return void 0;
|
|
1393
|
+
}
|
|
1394
|
+
if (filtered.length > 1) {
|
|
1395
|
+
throw new AttestationError("ATTESTATION_MALFORMED");
|
|
1396
|
+
}
|
|
1397
|
+
return filtered[0];
|
|
1398
|
+
}
|
|
1399
|
+
return value;
|
|
1400
|
+
}
|
|
1401
|
+
function resolveBodyDigestHex(req) {
|
|
1402
|
+
const record = getAttestationBodyRecord(req);
|
|
1403
|
+
if (record !== void 0) {
|
|
1404
|
+
return record.digestHex;
|
|
1405
|
+
}
|
|
1406
|
+
const snapshot = req;
|
|
1407
|
+
const headers = snapshot.headers ?? {};
|
|
1408
|
+
const contentLengthRaw = headers["content-length"];
|
|
1409
|
+
if (typeof contentLengthRaw === "string" && contentLengthRaw.trim() !== "") {
|
|
1410
|
+
const parsed = Number(contentLengthRaw.trim());
|
|
1411
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) {
|
|
1412
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1413
|
+
}
|
|
1414
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
1415
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1416
|
+
}
|
|
1417
|
+
} else if (Array.isArray(contentLengthRaw) && contentLengthRaw.length > 0) {
|
|
1418
|
+
for (const entry of contentLengthRaw) {
|
|
1419
|
+
if (typeof entry === "string" && entry.trim() !== "") {
|
|
1420
|
+
const parsed = Number(entry.trim());
|
|
1421
|
+
if (!Number.isSafeInteger(parsed) || parsed !== 0) {
|
|
1422
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const transferEncoding = headers["transfer-encoding"];
|
|
1428
|
+
if (typeof transferEncoding === "string" && transferEncoding.trim() !== "") {
|
|
1429
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1430
|
+
}
|
|
1431
|
+
if (Array.isArray(transferEncoding) && transferEncoding.length > 0) {
|
|
1432
|
+
const nonEmpty = transferEncoding.some((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
1433
|
+
if (nonEmpty) {
|
|
1434
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const body = snapshot.body;
|
|
1438
|
+
if (body !== void 0 && body !== null) {
|
|
1439
|
+
if (typeof body === "string" && body.length > 0) {
|
|
1440
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1441
|
+
}
|
|
1442
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body) && body.length > 0) {
|
|
1443
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1444
|
+
}
|
|
1445
|
+
if (body instanceof Uint8Array && body.length > 0) {
|
|
1446
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1447
|
+
}
|
|
1448
|
+
if (Array.isArray(body) && body.length > 0) {
|
|
1449
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1450
|
+
}
|
|
1451
|
+
if (typeof body === "object" && !Array.isArray(body)) {
|
|
1452
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) {
|
|
1453
|
+
} else if (!(body instanceof Uint8Array)) {
|
|
1454
|
+
const keys = Object.keys(body);
|
|
1455
|
+
if (keys.length > 0) {
|
|
1456
|
+
throw new AttestationError("ATTESTATION_BODY_CAPTURE_REQUIRED");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return EMPTY_BODY_SHA256_HEX;
|
|
1462
|
+
}
|
|
1463
|
+
function resolveConfig(options) {
|
|
1464
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1465
|
+
throw new AttestationProtocolError("attestation middleware options must be an object");
|
|
1466
|
+
}
|
|
1467
|
+
for (const key of Object.keys(options)) {
|
|
1468
|
+
if (!MIDDLEWARE_OPTION_KEYS.has(key)) {
|
|
1469
|
+
throw new AttestationProtocolError(`unknown attestation middleware option ${key}`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
validatePublicOrigin(options.publicOrigin);
|
|
1473
|
+
validateReplayNamespace(options.replayNamespace);
|
|
1474
|
+
const publicPathPrefix = validatePublicPathPrefix(options.publicPathPrefix);
|
|
1475
|
+
const keyProvider = options.keyProvider;
|
|
1476
|
+
if (keyProvider === null || typeof keyProvider !== "object" || typeof keyProvider.getSnapshot !== "function") {
|
|
1477
|
+
throw new AttestationProtocolError("attestation middleware keyProvider must expose getSnapshot()");
|
|
1478
|
+
}
|
|
1479
|
+
const store = options.store;
|
|
1480
|
+
if (store === null || typeof store !== "object" || typeof store.reserve !== "function") {
|
|
1481
|
+
throw new AttestationProtocolError("attestation middleware store must expose reserve()");
|
|
1482
|
+
}
|
|
1483
|
+
const policy = resolveTimePolicy({
|
|
1484
|
+
...options.maxAgeMs === void 0 ? {} : { maxAgeMs: options.maxAgeMs },
|
|
1485
|
+
...options.clockSkewMs === void 0 ? {} : { clockSkewMs: options.clockSkewMs },
|
|
1486
|
+
...options.clusterClockGuardMs === void 0 ? {} : { clusterClockGuardMs: options.clusterClockGuardMs }
|
|
1487
|
+
});
|
|
1488
|
+
const headerName = options.headerName ?? DEFAULT_ATTESTATION_HEADER;
|
|
1489
|
+
validateHeaderName(headerName);
|
|
1490
|
+
const now = options.now ?? Date.now;
|
|
1491
|
+
if (typeof now !== "function") {
|
|
1492
|
+
throw new AttestationProtocolError("attestation middleware now must be a function");
|
|
1493
|
+
}
|
|
1494
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
1495
|
+
if (!Number.isSafeInteger(operationTimeoutMs) || operationTimeoutMs < MIN_OPERATION_TIMEOUT_MS || operationTimeoutMs > MAX_OPERATION_TIMEOUT_MS) {
|
|
1496
|
+
throw new AttestationProtocolError(
|
|
1497
|
+
`operationTimeoutMs must be a safe integer in [${MIN_OPERATION_TIMEOUT_MS}, ${MAX_OPERATION_TIMEOUT_MS}]`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
const onDiagnostic = options.onDiagnostic;
|
|
1501
|
+
if (onDiagnostic !== void 0 && typeof onDiagnostic !== "function") {
|
|
1502
|
+
throw new AttestationProtocolError("attestation middleware onDiagnostic must be a function");
|
|
1503
|
+
}
|
|
1504
|
+
return Object.freeze({
|
|
1505
|
+
publicOrigin: options.publicOrigin,
|
|
1506
|
+
replayNamespace: options.replayNamespace,
|
|
1507
|
+
publicPathPrefix,
|
|
1508
|
+
keyProvider,
|
|
1509
|
+
store,
|
|
1510
|
+
policy,
|
|
1511
|
+
headerName,
|
|
1512
|
+
headerNameLower: headerName.toLowerCase(),
|
|
1513
|
+
now,
|
|
1514
|
+
operationTimeoutMs,
|
|
1515
|
+
onDiagnostic
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function createRequestAttestationMiddleware(options) {
|
|
1519
|
+
const config = resolveConfig(options);
|
|
1520
|
+
const middleware = (req, res, next) => {
|
|
1521
|
+
void Promise.resolve().then(async () => {
|
|
1522
|
+
let nowMs = checkedNow(config.now);
|
|
1523
|
+
let headerValue;
|
|
1524
|
+
try {
|
|
1525
|
+
headerValue = extractRawHeader(req, config.headerNameLower);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
if (error instanceof AttestationError) {
|
|
1528
|
+
sendControlledError(res, error, config);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
let snapshotRaw;
|
|
1534
|
+
try {
|
|
1535
|
+
const pending = config.keyProvider.getSnapshot();
|
|
1536
|
+
snapshotRaw = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
if (error instanceof AttestationError) {
|
|
1539
|
+
sendControlledError(res, error, config);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
let snapshot;
|
|
1546
|
+
try {
|
|
1547
|
+
snapshot = copyAttestationSnapshot(snapshotRaw);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
if (error instanceof AttestationError) {
|
|
1550
|
+
sendControlledError(res, error, config);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const record = req;
|
|
1557
|
+
const originalUrl = typeof record.originalUrl === "string" ? record.originalUrl : record.url;
|
|
1558
|
+
const contentTypeHeader = record.headers?.["content-type"] ?? void 0;
|
|
1559
|
+
const contentEncodingHeader = record.headers?.["content-encoding"] ?? void 0;
|
|
1560
|
+
let bodyDigestHex;
|
|
1561
|
+
try {
|
|
1562
|
+
bodyDigestHex = resolveBodyDigestHex(req);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if (error instanceof AttestationError) {
|
|
1565
|
+
sendControlledError(res, error, config);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
throw error;
|
|
1569
|
+
}
|
|
1570
|
+
let verified;
|
|
1571
|
+
try {
|
|
1572
|
+
verified = verifyAttestationProof({
|
|
1573
|
+
headerValue,
|
|
1574
|
+
method: record.method,
|
|
1575
|
+
originalUrl,
|
|
1576
|
+
publicPathPrefix: config.publicPathPrefix,
|
|
1577
|
+
contentTypeHeader,
|
|
1578
|
+
contentEncodingHeader,
|
|
1579
|
+
bodyDigestHex,
|
|
1580
|
+
snapshot,
|
|
1581
|
+
nowMs,
|
|
1582
|
+
policy: config.policy,
|
|
1583
|
+
replayNamespace: config.replayNamespace,
|
|
1584
|
+
publicOrigin: config.publicOrigin
|
|
1585
|
+
});
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
if (error instanceof AttestationError) {
|
|
1588
|
+
sendControlledError(res, error, config);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
sendControlledError(res, new AttestationError("ATTESTATION_INTERNAL_ERROR"), config);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
nowMs = checkedNow(config.now);
|
|
1595
|
+
let rechecked;
|
|
1596
|
+
try {
|
|
1597
|
+
rechecked = checkProofTiming({
|
|
1598
|
+
timestampMs: verified.timestampMs,
|
|
1599
|
+
nowMs,
|
|
1600
|
+
keyEntry: verified.keyEntry,
|
|
1601
|
+
policy: config.policy
|
|
1602
|
+
});
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
if (error instanceof AttestationError) {
|
|
1605
|
+
sendControlledError(res, error, config);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
sendControlledError(res, new AttestationError("ATTESTATION_INTERNAL_ERROR"), config);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const retainUntilMs = rechecked.retainUntilMs;
|
|
1612
|
+
if (retainUntilMs === null || !Number.isSafeInteger(retainUntilMs)) {
|
|
1613
|
+
sendControlledError(res, new AttestationError("ATTESTATION_EXPIRED"), config);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
let reserveResult;
|
|
1617
|
+
try {
|
|
1618
|
+
const pending = config.store.reserve({
|
|
1619
|
+
replayKey: verified.replayKey,
|
|
1620
|
+
retainUntilMs
|
|
1621
|
+
});
|
|
1622
|
+
reserveResult = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
if (error instanceof AttestationCapacityError) {
|
|
1625
|
+
sendControlledError(res, new AttestationError("ATTESTATION_REPLAY_UNAVAILABLE"), config);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
sendControlledError(res, new AttestationError("ATTESTATION_REPLAY_UNAVAILABLE"), config);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (reserveResult === "duplicate") {
|
|
1632
|
+
sendControlledError(res, new AttestationError("ATTESTATION_REPLAY"), config);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (reserveResult === "expired") {
|
|
1636
|
+
sendControlledError(res, new AttestationError("ATTESTATION_EXPIRED"), config);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (reserveResult !== "reserved") {
|
|
1640
|
+
sendControlledError(res, new AttestationError("ATTESTATION_REPLAY_UNAVAILABLE"), config);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
nowMs = checkedNow(config.now);
|
|
1644
|
+
try {
|
|
1645
|
+
checkProofTiming({
|
|
1646
|
+
timestampMs: verified.timestampMs,
|
|
1647
|
+
nowMs,
|
|
1648
|
+
keyEntry: verified.keyEntry,
|
|
1649
|
+
policy: config.policy
|
|
1650
|
+
});
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
if (error instanceof AttestationError) {
|
|
1653
|
+
sendControlledError(res, error, config);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
sendControlledError(res, new AttestationError("ATTESTATION_INTERNAL_ERROR"), config);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
next();
|
|
1660
|
+
}).catch((error) => {
|
|
1661
|
+
if (error instanceof AttestationError) {
|
|
1662
|
+
const target2 = res;
|
|
1663
|
+
if (target2?.headersSent !== true) {
|
|
1664
|
+
sendControlledError(res, error, config);
|
|
1665
|
+
}
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const target = res;
|
|
1669
|
+
if (target?.headersSent === true) {
|
|
1670
|
+
try {
|
|
1671
|
+
next(error);
|
|
1672
|
+
} catch {
|
|
1673
|
+
}
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
sendControlledError(res, new AttestationError("ATTESTATION_INTERNAL_ERROR"), config);
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
return middleware;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/stores/redis-script.ts
|
|
1683
|
+
var import_node_crypto3 = require("crypto");
|
|
1684
|
+
var RESERVE_ATTESTATION_SCRIPT = `
|
|
1685
|
+
local function finiteNumber(value)
|
|
1686
|
+
return type(value) == 'number' and value == value and value > -math.huge and value < math.huge
|
|
1687
|
+
end
|
|
1688
|
+
local function serverNow()
|
|
1689
|
+
local time = redis.call('TIME')
|
|
1690
|
+
return tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
1691
|
+
end
|
|
1692
|
+
local retainUntil = tonumber(ARGV[2])
|
|
1693
|
+
if not finiteNumber(retainUntil) or retainUntil ~= math.floor(retainUntil)
|
|
1694
|
+
or retainUntil < 0 or retainUntil > 9007199254740991 then
|
|
1695
|
+
return 4
|
|
1696
|
+
end
|
|
1697
|
+
local maxEntries = tonumber(ARGV[3])
|
|
1698
|
+
if not finiteNumber(maxEntries) or maxEntries ~= math.floor(maxEntries)
|
|
1699
|
+
or maxEntries <= 0 or maxEntries > 9007199254740991 then
|
|
1700
|
+
return 4
|
|
1701
|
+
end
|
|
1702
|
+
local now = serverNow()
|
|
1703
|
+
if retainUntil <= now then
|
|
1704
|
+
return 2
|
|
1705
|
+
end
|
|
1706
|
+
if retainUntil - now > 240000 then
|
|
1707
|
+
return 4
|
|
1708
|
+
end
|
|
1709
|
+
local keyType = redis.call('TYPE', KEYS[1])['ok']
|
|
1710
|
+
if keyType ~= 'none' and keyType ~= 'zset' then
|
|
1711
|
+
error('Attestation Redis replay key has unexpected type')
|
|
1712
|
+
end
|
|
1713
|
+
local existing = redis.call('ZSCORE', KEYS[1], ARGV[1])
|
|
1714
|
+
if existing ~= false and tonumber(existing) > now then
|
|
1715
|
+
return 0
|
|
1716
|
+
end
|
|
1717
|
+
local expired = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now, 'LIMIT', 0, 64)
|
|
1718
|
+
if #expired > 0 then
|
|
1719
|
+
redis.call('ZREM', KEYS[1], unpack(expired))
|
|
1720
|
+
end
|
|
1721
|
+
if existing ~= false then
|
|
1722
|
+
redis.call('ZREM', KEYS[1], ARGV[1])
|
|
1723
|
+
end
|
|
1724
|
+
if redis.call('ZCARD', KEYS[1]) >= maxEntries then
|
|
1725
|
+
return 3
|
|
1726
|
+
end
|
|
1727
|
+
redis.call('ZADD', KEYS[1], retainUntil, ARGV[1])
|
|
1728
|
+
local last = redis.call('ZREVRANGE', KEYS[1], 0, 0, 'WITHSCORES')
|
|
1729
|
+
redis.call('PEXPIREAT', KEYS[1], last[2])
|
|
1730
|
+
return 1
|
|
1731
|
+
`;
|
|
1732
|
+
function buildReserveAttestationCommand(ledgerKey, replayKey, retainUntilMs, maxEntries) {
|
|
1733
|
+
return ["EVAL", RESERVE_ATTESTATION_SCRIPT, "1", ledgerKey, replayKey, String(retainUntilMs), String(maxEntries)];
|
|
1734
|
+
}
|
|
1735
|
+
function scriptSha1(script) {
|
|
1736
|
+
return (0, import_node_crypto3.createHash)("sha1").update(script).digest("hex");
|
|
1737
|
+
}
|
|
1738
|
+
var NOSCRIPT_ERROR_PREFIX = "NOSCRIPT";
|
|
1739
|
+
function isNoScriptError(error) {
|
|
1740
|
+
if (error instanceof Error) {
|
|
1741
|
+
return error.message.startsWith(NOSCRIPT_ERROR_PREFIX);
|
|
1742
|
+
}
|
|
1743
|
+
if (typeof error === "string") {
|
|
1744
|
+
return error.startsWith(NOSCRIPT_ERROR_PREFIX);
|
|
1745
|
+
}
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
var RedisScriptRunner = class {
|
|
1749
|
+
client;
|
|
1750
|
+
digests = /* @__PURE__ */ new Map();
|
|
1751
|
+
constructor(client) {
|
|
1752
|
+
this.client = client;
|
|
1753
|
+
}
|
|
1754
|
+
/** Drop cached digests (e.g. after a deliberate reconnect known to flush). */
|
|
1755
|
+
reset() {
|
|
1756
|
+
this.digests.clear();
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Execute an `EVAL`-form command via cached `EVALSHA`. The leading `EVAL`
|
|
1760
|
+
* and script body are replaced with `EVALSHA` + digest; the remaining
|
|
1761
|
+
* positional layout (key count, keys, args) is preserved exactly.
|
|
1762
|
+
*/
|
|
1763
|
+
async run(command) {
|
|
1764
|
+
if (command[0] !== "EVAL" || typeof command[1] !== "string" || typeof command[2] !== "string") {
|
|
1765
|
+
throw new Error("RedisScriptRunner.run expects an EVAL-form command.");
|
|
1766
|
+
}
|
|
1767
|
+
const script = command[1];
|
|
1768
|
+
const tail = command.slice(2);
|
|
1769
|
+
return this.runDigest(script, tail);
|
|
1770
|
+
}
|
|
1771
|
+
async runDigest(script, tail) {
|
|
1772
|
+
let digest = this.digests.get(script);
|
|
1773
|
+
if (!digest) {
|
|
1774
|
+
digest = scriptSha1(script);
|
|
1775
|
+
this.digests.set(script, digest);
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
return await this.client.sendCommand(["EVALSHA", digest, ...tail]);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
if (!isNoScriptError(error)) {
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1783
|
+
const loaded = await this.client.sendCommand(["SCRIPT", "LOAD", script]);
|
|
1784
|
+
if (typeof loaded === "string" && loaded.length > 0) {
|
|
1785
|
+
digest = loaded;
|
|
1786
|
+
this.digests.set(script, digest);
|
|
1787
|
+
}
|
|
1788
|
+
return this.client.sendCommand(["EVALSHA", digest, ...tail]);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/** Exposed for tests: the digest the runner would currently send. */
|
|
1792
|
+
digestFor(script) {
|
|
1793
|
+
return this.digests.get(script) ?? scriptSha1(script);
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/stores/redis.ts
|
|
1798
|
+
var REDIS_ATTESTATION_STORE_DEFAULT_KEY_PREFIX = "wtt-attestation";
|
|
1799
|
+
var REDIS_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES = 5e4;
|
|
1800
|
+
var LEDGER_KEY_SUFFIX = "attestation-proofs";
|
|
1801
|
+
var STORE_OPTION_KEYS2 = /* @__PURE__ */ new Set(["client", "keyPrefix", "maxEntries"]);
|
|
1802
|
+
var AttestationRedisStoreError = class extends Error {
|
|
1803
|
+
name = "AttestationRedisStoreError";
|
|
1804
|
+
constructor(message = "Attestation Redis replay store is unavailable.") {
|
|
1805
|
+
super(message);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
function ledgerKeyFor(keyPrefix) {
|
|
1809
|
+
return `${keyPrefix}:${LEDGER_KEY_SUFFIX}`;
|
|
1810
|
+
}
|
|
1811
|
+
var RedisAttestationStore = class {
|
|
1812
|
+
client;
|
|
1813
|
+
ledgerKey;
|
|
1814
|
+
maxEntries;
|
|
1815
|
+
runner;
|
|
1816
|
+
constructor(options) {
|
|
1817
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1818
|
+
throw new AttestationProtocolError("redis attestation store options must be an object");
|
|
1819
|
+
}
|
|
1820
|
+
for (const key of Object.keys(options)) {
|
|
1821
|
+
if (!STORE_OPTION_KEYS2.has(key)) {
|
|
1822
|
+
throw new AttestationProtocolError(`unknown redis attestation store option ${key}`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
const client = options.client;
|
|
1826
|
+
if (client === null || typeof client !== "object" || typeof client.sendCommand !== "function") {
|
|
1827
|
+
throw new AttestationProtocolError("redis attestation store client must expose sendCommand()");
|
|
1828
|
+
}
|
|
1829
|
+
const keyPrefix = options.keyPrefix === void 0 ? REDIS_ATTESTATION_STORE_DEFAULT_KEY_PREFIX : options.keyPrefix;
|
|
1830
|
+
if (typeof keyPrefix !== "string" || keyPrefix.length === 0 || keyPrefix.length > 256) {
|
|
1831
|
+
throw new AttestationProtocolError("redis attestation store keyPrefix must be a 1-256 character string");
|
|
1832
|
+
}
|
|
1833
|
+
const maxEntries = options.maxEntries === void 0 ? REDIS_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES : options.maxEntries;
|
|
1834
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) {
|
|
1835
|
+
throw new AttestationProtocolError("redis attestation store maxEntries must be a positive safe integer");
|
|
1836
|
+
}
|
|
1837
|
+
this.client = client;
|
|
1838
|
+
this.ledgerKey = ledgerKeyFor(keyPrefix);
|
|
1839
|
+
this.maxEntries = maxEntries;
|
|
1840
|
+
this.runner = new RedisScriptRunner(this.client);
|
|
1841
|
+
}
|
|
1842
|
+
async reserve(input) {
|
|
1843
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1844
|
+
throw new AttestationProtocolError("attestation store reserve input must be an object");
|
|
1845
|
+
}
|
|
1846
|
+
const record = input;
|
|
1847
|
+
const replayKey = record.replayKey;
|
|
1848
|
+
const retainUntilMs = record.retainUntilMs ?? record.retainUntil;
|
|
1849
|
+
validateReplayKey(replayKey);
|
|
1850
|
+
if (retainUntilMs === void 0) {
|
|
1851
|
+
throw new AttestationProtocolError("retainUntilMs must be a non-negative safe integer");
|
|
1852
|
+
}
|
|
1853
|
+
assertTimestampMs("retainUntilMs", retainUntilMs);
|
|
1854
|
+
const replayKeySnapshot = replayKey;
|
|
1855
|
+
const retainUntilSnapshot = retainUntilMs;
|
|
1856
|
+
const ledgerKey = this.ledgerKey;
|
|
1857
|
+
const maxEntries = this.maxEntries;
|
|
1858
|
+
let reply;
|
|
1859
|
+
try {
|
|
1860
|
+
reply = await this.runner.run(
|
|
1861
|
+
buildReserveAttestationCommand(ledgerKey, replayKeySnapshot, retainUntilSnapshot, maxEntries)
|
|
1862
|
+
);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
if (error instanceof AttestationCapacityError || error instanceof AttestationProtocolError) {
|
|
1865
|
+
throw error;
|
|
1866
|
+
}
|
|
1867
|
+
if (error instanceof AttestationRedisStoreError) {
|
|
1868
|
+
throw error;
|
|
1869
|
+
}
|
|
1870
|
+
throw new AttestationRedisStoreError(
|
|
1871
|
+
error instanceof Error ? `Attestation Redis reservation failed: ${error.message}` : "Attestation Redis reservation failed."
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (reply === 1 || reply === "1") {
|
|
1875
|
+
return "reserved";
|
|
1876
|
+
}
|
|
1877
|
+
if (reply === 0 || reply === "0") {
|
|
1878
|
+
return "duplicate";
|
|
1879
|
+
}
|
|
1880
|
+
if (reply === 2 || reply === "2") {
|
|
1881
|
+
return "expired";
|
|
1882
|
+
}
|
|
1883
|
+
if (reply === 3 || reply === "3") {
|
|
1884
|
+
throw new AttestationCapacityError();
|
|
1885
|
+
}
|
|
1886
|
+
if (reply === 4 || reply === "4") {
|
|
1887
|
+
throw new AttestationProtocolError(
|
|
1888
|
+
"retainUntilMs exceeds the 240000 ms global retention cap or is not a safe live deadline"
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
throw new AttestationRedisStoreError("Attestation Redis store received an unexpected script reply.");
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
function createRedisAttestationStore(options) {
|
|
1895
|
+
return new RedisAttestationStore(options);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/stores/mongodb.ts
|
|
1899
|
+
var MONGO_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES = 5e4;
|
|
1900
|
+
var MONGO_ATTESTATION_STORE_DEFAULT_PROOFS_COLLECTION = "attestation_proofs";
|
|
1901
|
+
var MONGO_ATTESTATION_STORE_DEFAULT_CAPACITY_COLLECTION = "attestation_replay_capacity";
|
|
1902
|
+
var EXPIRY_SWEEP_BATCH_SIZE2 = 64;
|
|
1903
|
+
var CAPACITY_ID = "capacity";
|
|
1904
|
+
var STORE_OPTION_KEYS3 = /* @__PURE__ */ new Set([
|
|
1905
|
+
"db",
|
|
1906
|
+
"proofsCollectionName",
|
|
1907
|
+
"capacityCollectionName",
|
|
1908
|
+
"maxEntries",
|
|
1909
|
+
"now"
|
|
1910
|
+
]);
|
|
1911
|
+
var reservationId = (replayKey) => `proof:${replayKey}`;
|
|
1912
|
+
var AttestationMongoStoreError = class extends Error {
|
|
1913
|
+
name = "AttestationMongoStoreError";
|
|
1914
|
+
constructor(message = "Attestation MongoDB replay store is unavailable.") {
|
|
1915
|
+
super(message);
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
function validateCollectionName(role, name) {
|
|
1919
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1920
|
+
throw new AttestationProtocolError(`mongo attestation store ${role} collection name must be a non-empty string`);
|
|
1921
|
+
}
|
|
1922
|
+
if (name.includes("\0")) {
|
|
1923
|
+
throw new AttestationProtocolError(`mongo attestation store ${role} collection name must not contain null bytes`);
|
|
1924
|
+
}
|
|
1925
|
+
if (name.startsWith("system.")) {
|
|
1926
|
+
throw new AttestationProtocolError(
|
|
1927
|
+
`mongo attestation store ${role} collection name must not use the reserved system namespace`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
if (name.includes("$")) {
|
|
1931
|
+
throw new AttestationProtocolError(`mongo attestation store ${role} collection name must not contain '$'`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
function unwrapFindOneAndUpdate(reply) {
|
|
1935
|
+
if (reply !== null && typeof reply === "object" && "value" in reply && "ok" in reply) {
|
|
1936
|
+
return reply.value;
|
|
1937
|
+
}
|
|
1938
|
+
return reply;
|
|
1939
|
+
}
|
|
1940
|
+
function deletedCountOf(reply) {
|
|
1941
|
+
if (reply !== null && typeof reply === "object" && "deletedCount" in reply) {
|
|
1942
|
+
const count = reply.deletedCount;
|
|
1943
|
+
if (typeof count === "number" && Number.isSafeInteger(count) && count >= 0) {
|
|
1944
|
+
return count;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return 0;
|
|
1948
|
+
}
|
|
1949
|
+
var MongoAttestationStore = class {
|
|
1950
|
+
db;
|
|
1951
|
+
proofs;
|
|
1952
|
+
capacity;
|
|
1953
|
+
maxEntries;
|
|
1954
|
+
now;
|
|
1955
|
+
constructor(options) {
|
|
1956
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1957
|
+
throw new AttestationProtocolError("mongo attestation store options must be an object");
|
|
1958
|
+
}
|
|
1959
|
+
for (const key of Object.keys(options)) {
|
|
1960
|
+
if (!STORE_OPTION_KEYS3.has(key)) {
|
|
1961
|
+
throw new AttestationProtocolError(`unknown mongo attestation store option ${key}`);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const db = options.db;
|
|
1965
|
+
if (db === null || typeof db !== "object" || typeof db.collection !== "function" || db.client === null || typeof db.client !== "object" || typeof db.client.startSession !== "function") {
|
|
1966
|
+
throw new AttestationProtocolError(
|
|
1967
|
+
"mongo attestation store db must expose collection() and client.startSession()"
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
const proofsCollectionName = options.proofsCollectionName === void 0 ? MONGO_ATTESTATION_STORE_DEFAULT_PROOFS_COLLECTION : options.proofsCollectionName;
|
|
1971
|
+
const capacityCollectionName = options.capacityCollectionName === void 0 ? MONGO_ATTESTATION_STORE_DEFAULT_CAPACITY_COLLECTION : options.capacityCollectionName;
|
|
1972
|
+
validateCollectionName("proofs", proofsCollectionName);
|
|
1973
|
+
validateCollectionName("capacity", capacityCollectionName);
|
|
1974
|
+
if (proofsCollectionName === capacityCollectionName) {
|
|
1975
|
+
throw new AttestationProtocolError("mongo attestation store proofs and capacity collections must be distinct");
|
|
1976
|
+
}
|
|
1977
|
+
const maxEntries = options.maxEntries === void 0 ? MONGO_ATTESTATION_STORE_DEFAULT_MAX_ENTRIES : options.maxEntries;
|
|
1978
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) {
|
|
1979
|
+
throw new AttestationProtocolError("mongo attestation store maxEntries must be a positive safe integer");
|
|
1980
|
+
}
|
|
1981
|
+
const now = options.now === void 0 ? () => Date.now() : options.now;
|
|
1982
|
+
if (typeof now !== "function") {
|
|
1983
|
+
throw new AttestationProtocolError("mongo attestation store now must be a function");
|
|
1984
|
+
}
|
|
1985
|
+
const liveDb = db;
|
|
1986
|
+
let proofs;
|
|
1987
|
+
let capacity;
|
|
1988
|
+
try {
|
|
1989
|
+
proofs = liveDb.collection(proofsCollectionName);
|
|
1990
|
+
capacity = liveDb.collection(capacityCollectionName);
|
|
1991
|
+
} catch {
|
|
1992
|
+
throw new AttestationProtocolError("mongo attestation store db.collection() failed");
|
|
1993
|
+
}
|
|
1994
|
+
if (proofs === null || typeof proofs !== "object" || capacity === null || typeof capacity !== "object") {
|
|
1995
|
+
throw new AttestationProtocolError("mongo attestation store db.collection() failed");
|
|
1996
|
+
}
|
|
1997
|
+
this.db = liveDb;
|
|
1998
|
+
this.proofs = proofs;
|
|
1999
|
+
this.capacity = capacity;
|
|
2000
|
+
this.maxEntries = maxEntries;
|
|
2001
|
+
this.now = now;
|
|
2002
|
+
}
|
|
2003
|
+
async reserve(input) {
|
|
2004
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
2005
|
+
throw new AttestationProtocolError("attestation store reserve input must be an object");
|
|
2006
|
+
}
|
|
2007
|
+
const record = input;
|
|
2008
|
+
const replayKey = record.replayKey;
|
|
2009
|
+
const retainUntilMs = record.retainUntilMs ?? record.retainUntil;
|
|
2010
|
+
const nowMs = this.now();
|
|
2011
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
2012
|
+
throw new AttestationProtocolError("attestation store clock must return a non-negative safe integer");
|
|
2013
|
+
}
|
|
2014
|
+
validateReplayKey(replayKey);
|
|
2015
|
+
if (retainUntilMs === void 0) {
|
|
2016
|
+
throw new AttestationProtocolError("retainUntilMs must be a non-negative safe integer");
|
|
2017
|
+
}
|
|
2018
|
+
assertTimestampMs("retainUntilMs", retainUntilMs);
|
|
2019
|
+
if (retainUntilMs <= nowMs) {
|
|
2020
|
+
return "expired";
|
|
2021
|
+
}
|
|
2022
|
+
if (retainUntilMs - nowMs > GLOBAL_MAX_RETENTION_MS) {
|
|
2023
|
+
throw new AttestationProtocolError(
|
|
2024
|
+
`retainUntilMs exceeds the ${GLOBAL_MAX_RETENTION_MS} ms global retention cap`
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
const replayKeySnapshot = replayKey;
|
|
2028
|
+
const retainUntilSnapshot = retainUntilMs;
|
|
2029
|
+
let session;
|
|
2030
|
+
try {
|
|
2031
|
+
session = this.db.client.startSession();
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
throw new AttestationMongoStoreError(
|
|
2034
|
+
error instanceof Error ? `Attestation MongoDB session failed: ${error.message}` : "Attestation MongoDB session failed."
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const result = await session.withTransaction(
|
|
2039
|
+
async () => {
|
|
2040
|
+
const nowTx = this.now();
|
|
2041
|
+
if (!Number.isSafeInteger(nowTx) || nowTx < 0) {
|
|
2042
|
+
throw new AttestationProtocolError("attestation store clock must return a non-negative safe integer");
|
|
2043
|
+
}
|
|
2044
|
+
if (retainUntilSnapshot <= nowTx) {
|
|
2045
|
+
return "expired";
|
|
2046
|
+
}
|
|
2047
|
+
if (retainUntilSnapshot - nowTx > GLOBAL_MAX_RETENTION_MS) {
|
|
2048
|
+
throw new AttestationProtocolError(
|
|
2049
|
+
`retainUntilMs exceeds the ${GLOBAL_MAX_RETENTION_MS} ms global retention cap`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
const capacityRaw = unwrapFindOneAndUpdate(
|
|
2053
|
+
await this.capacity.findOneAndUpdate(
|
|
2054
|
+
{ _id: CAPACITY_ID },
|
|
2055
|
+
{
|
|
2056
|
+
// Note: `$inc` must not share `revision` with `$setOnInsert`
|
|
2057
|
+
// (MongoDB rejects the conflicting path on insert); `$inc`
|
|
2058
|
+
// creates `revision: 1` for a fresh capacity row.
|
|
2059
|
+
$inc: { revision: 1 },
|
|
2060
|
+
$setOnInsert: {
|
|
2061
|
+
kind: "capacity",
|
|
2062
|
+
entries: 0,
|
|
2063
|
+
maxEntries: this.maxEntries,
|
|
2064
|
+
proofsCollectionName: this.proofs.collectionName
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{ session, returnDocument: "after", upsert: true }
|
|
2068
|
+
)
|
|
2069
|
+
);
|
|
2070
|
+
this.validateCapacity(capacityRaw);
|
|
2071
|
+
const capacityDoc = capacityRaw;
|
|
2072
|
+
const proofRaw = await this.proofs.findOne({ _id: replayKeySnapshot }, { session });
|
|
2073
|
+
if (proofRaw !== null) {
|
|
2074
|
+
this.validateProof(proofRaw, replayKeySnapshot);
|
|
2075
|
+
if (proofRaw.retainUntilMs > nowTx) {
|
|
2076
|
+
return "duplicate";
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
const ledgerId = reservationId(replayKeySnapshot);
|
|
2080
|
+
const existingRaw = await this.capacity.findOne({ _id: ledgerId }, { session });
|
|
2081
|
+
if (existingRaw !== null) {
|
|
2082
|
+
this.validateReservation(existingRaw);
|
|
2083
|
+
if (existingRaw.retainUntilMs > nowTx) {
|
|
2084
|
+
return "duplicate";
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
let removed = 0;
|
|
2088
|
+
if (existingRaw !== null) {
|
|
2089
|
+
removed += deletedCountOf(await this.capacity.deleteOne({ _id: ledgerId }, { session }));
|
|
2090
|
+
}
|
|
2091
|
+
const expiredRows = await this.capacity.find({ kind: "reservation", retainUntilMs: { $lte: nowTx } }, { session }).sort({ retainUntilMs: 1 }).limit(EXPIRY_SWEEP_BATCH_SIZE2).toArray();
|
|
2092
|
+
if (!Array.isArray(expiredRows)) {
|
|
2093
|
+
throw new AttestationMongoStoreError("Attestation MongoDB expiry accounting is malformed.");
|
|
2094
|
+
}
|
|
2095
|
+
for (const row of expiredRows) {
|
|
2096
|
+
this.validateReservation(row);
|
|
2097
|
+
}
|
|
2098
|
+
if (expiredRows.length > 0) {
|
|
2099
|
+
const ids = expiredRows.map((row) => row._id);
|
|
2100
|
+
const replayKeys = expiredRows.map((row) => row.replayKey);
|
|
2101
|
+
removed += deletedCountOf(
|
|
2102
|
+
await this.capacity.deleteMany({ _id: { $in: ids }, kind: "reservation" }, { session })
|
|
2103
|
+
);
|
|
2104
|
+
await this.proofs.deleteMany({ _id: { $in: replayKeys }, retainUntilMs: { $lte: nowTx } }, { session });
|
|
2105
|
+
}
|
|
2106
|
+
const entries = capacityDoc.entries - removed;
|
|
2107
|
+
if (!Number.isSafeInteger(entries) || entries < 0) {
|
|
2108
|
+
throw new AttestationMongoStoreError("Attestation MongoDB replay capacity accounting is inconsistent.");
|
|
2109
|
+
}
|
|
2110
|
+
if (entries >= this.maxEntries) {
|
|
2111
|
+
throw new AttestationCapacityError();
|
|
2112
|
+
}
|
|
2113
|
+
await this.proofs.replaceOne(
|
|
2114
|
+
{ _id: replayKeySnapshot },
|
|
2115
|
+
{ _id: replayKeySnapshot, retainUntilMs: retainUntilSnapshot },
|
|
2116
|
+
{ upsert: true, session }
|
|
2117
|
+
);
|
|
2118
|
+
await this.capacity.insertOne(
|
|
2119
|
+
{
|
|
2120
|
+
_id: ledgerId,
|
|
2121
|
+
kind: "reservation",
|
|
2122
|
+
replayKey: replayKeySnapshot,
|
|
2123
|
+
retainUntilMs: retainUntilSnapshot
|
|
2124
|
+
},
|
|
2125
|
+
{ session }
|
|
2126
|
+
);
|
|
2127
|
+
await this.capacity.updateOne({ _id: CAPACITY_ID }, { $set: { entries: entries + 1 } }, { session });
|
|
2128
|
+
return "reserved";
|
|
2129
|
+
},
|
|
2130
|
+
{ readConcern: { level: "snapshot" } }
|
|
2131
|
+
);
|
|
2132
|
+
if (result === "reserved" || result === "duplicate" || result === "expired") {
|
|
2133
|
+
return result;
|
|
2134
|
+
}
|
|
2135
|
+
throw new AttestationMongoStoreError("Attestation MongoDB reservation did not commit.");
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
if (error instanceof AttestationCapacityError || error instanceof AttestationProtocolError || error instanceof AttestationMongoStoreError) {
|
|
2138
|
+
throw error;
|
|
2139
|
+
}
|
|
2140
|
+
throw new AttestationMongoStoreError(
|
|
2141
|
+
error instanceof Error ? `Attestation MongoDB reservation failed: ${error.message}` : "Attestation MongoDB reservation failed."
|
|
2142
|
+
);
|
|
2143
|
+
} finally {
|
|
2144
|
+
await session.endSession();
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
validateCapacity(row) {
|
|
2148
|
+
if (row === null || typeof row !== "object" || row._id !== CAPACITY_ID || row.kind !== "capacity" || row.maxEntries !== this.maxEntries || row.proofsCollectionName !== this.proofs.collectionName || !Number.isSafeInteger(row.entries) || row.entries < 0 || row.entries > this.maxEntries || !Number.isSafeInteger(row.revision)) {
|
|
2149
|
+
throw new AttestationMongoStoreError(
|
|
2150
|
+
"Attestation MongoDB replay capacity configuration/accounting is inconsistent."
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
validateReservation(row) {
|
|
2155
|
+
if (row.kind !== "reservation" || typeof row.replayKey !== "string" || row._id !== reservationId(row.replayKey) || !Number.isSafeInteger(row.retainUntilMs) || row.retainUntilMs < 0) {
|
|
2156
|
+
throw new AttestationMongoStoreError("Attestation MongoDB replay expiry accounting is malformed.");
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
validateProof(row, replayKey) {
|
|
2160
|
+
if (row._id !== replayKey || !Number.isSafeInteger(row.retainUntilMs) || row.retainUntilMs < 0) {
|
|
2161
|
+
throw new AttestationMongoStoreError("Attestation MongoDB replay proof is malformed.");
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
function createMongoAttestationStore(options) {
|
|
2166
|
+
return new MongoAttestationStore(options);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/keys.ts
|
|
2170
|
+
var import_node_crypto4 = require("crypto");
|
|
2171
|
+
var MAX_RETAINED_ATTESTATION_KEYS = 16;
|
|
2172
|
+
var SNAPSHOT_OPTION_KEYS = /* @__PURE__ */ new Set(["currentKeyId", "keys"]);
|
|
2173
|
+
var KEY_ENTRY_KEYS = /* @__PURE__ */ new Set(["keyId", "key", "acceptFrom", "acceptUntil"]);
|
|
2174
|
+
var KEY_WINDOW_KEYS = /* @__PURE__ */ new Set(["acceptFrom", "acceptUntil"]);
|
|
2175
|
+
function bytesEqual(left, right) {
|
|
2176
|
+
if (left.length !== right.length) {
|
|
2177
|
+
return false;
|
|
2178
|
+
}
|
|
2179
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
2180
|
+
if (left[index] !== right[index]) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return true;
|
|
2185
|
+
}
|
|
2186
|
+
function copyKeyEntry(entry) {
|
|
2187
|
+
const keyCopy = new Uint8Array(entry.key);
|
|
2188
|
+
return Object.freeze({
|
|
2189
|
+
keyId: entry.keyId,
|
|
2190
|
+
key: keyCopy,
|
|
2191
|
+
acceptFrom: entry.acceptFrom,
|
|
2192
|
+
acceptUntil: entry.acceptUntil
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
function copySnapshot(snapshot) {
|
|
2196
|
+
const keys = snapshot.keys.map(copyKeyEntry);
|
|
2197
|
+
return Object.freeze({
|
|
2198
|
+
currentKeyId: snapshot.currentKeyId,
|
|
2199
|
+
keys: Object.freeze(keys)
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
function assertValidSnapshotShape(snapshot) {
|
|
2203
|
+
if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
2204
|
+
throw new AttestationProtocolError("key snapshot must be an object");
|
|
2205
|
+
}
|
|
2206
|
+
for (const key of Object.keys(snapshot)) {
|
|
2207
|
+
if (!SNAPSHOT_OPTION_KEYS.has(key)) {
|
|
2208
|
+
throw new AttestationProtocolError(`unknown key snapshot option ${key}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const record = snapshot;
|
|
2212
|
+
validateKeyId(record.currentKeyId);
|
|
2213
|
+
if (!Array.isArray(record.keys)) {
|
|
2214
|
+
throw new AttestationProtocolError("key snapshot keys must be an array");
|
|
2215
|
+
}
|
|
2216
|
+
if (record.keys.length === 0 || record.keys.length > MAX_RETAINED_ATTESTATION_KEYS) {
|
|
2217
|
+
throw new AttestationProtocolError(`key snapshot must retain 1-${MAX_RETAINED_ATTESTATION_KEYS} keys`);
|
|
2218
|
+
}
|
|
2219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2220
|
+
for (const entry of record.keys) {
|
|
2221
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2222
|
+
throw new AttestationProtocolError("key snapshot entry must be an object");
|
|
2223
|
+
}
|
|
2224
|
+
for (const key of Object.keys(entry)) {
|
|
2225
|
+
if (!KEY_ENTRY_KEYS.has(key)) {
|
|
2226
|
+
throw new AttestationProtocolError(`unknown key snapshot entry option ${key}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const row = entry;
|
|
2230
|
+
validateKeyId(row.keyId);
|
|
2231
|
+
validateKeyBytes(row.key);
|
|
2232
|
+
assertTimestampMs("acceptFrom", row.acceptFrom);
|
|
2233
|
+
assertTimestampMs("acceptUntil", row.acceptUntil);
|
|
2234
|
+
const acceptFrom = row.acceptFrom;
|
|
2235
|
+
const acceptUntil = row.acceptUntil;
|
|
2236
|
+
if (acceptFrom >= acceptUntil) {
|
|
2237
|
+
throw new AttestationProtocolError("key acceptance window must satisfy acceptFrom < acceptUntil");
|
|
2238
|
+
}
|
|
2239
|
+
const keyId = row.keyId;
|
|
2240
|
+
if (seen.has(keyId)) {
|
|
2241
|
+
throw new AttestationProtocolError("key snapshot keyIds must be unique");
|
|
2242
|
+
}
|
|
2243
|
+
seen.add(keyId);
|
|
2244
|
+
}
|
|
2245
|
+
if (!seen.has(record.currentKeyId)) {
|
|
2246
|
+
throw new AttestationProtocolError("key snapshot currentKeyId must name a present entry");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
function ownSnapshot(input) {
|
|
2250
|
+
assertValidSnapshotShape(input);
|
|
2251
|
+
const keys = input.keys.map(
|
|
2252
|
+
(entry) => Object.freeze({
|
|
2253
|
+
keyId: entry.keyId,
|
|
2254
|
+
key: new Uint8Array(entry.key),
|
|
2255
|
+
acceptFrom: entry.acceptFrom,
|
|
2256
|
+
acceptUntil: entry.acceptUntil
|
|
2257
|
+
})
|
|
2258
|
+
);
|
|
2259
|
+
return Object.freeze({
|
|
2260
|
+
currentKeyId: input.currentKeyId,
|
|
2261
|
+
keys: Object.freeze(keys)
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
function generateAttestationKey(keyId, window) {
|
|
2265
|
+
validateKeyId(keyId);
|
|
2266
|
+
if (window === null || typeof window !== "object" || Array.isArray(window)) {
|
|
2267
|
+
throw new AttestationProtocolError("key window must be an object");
|
|
2268
|
+
}
|
|
2269
|
+
for (const key2 of Object.keys(window)) {
|
|
2270
|
+
if (!KEY_WINDOW_KEYS.has(key2)) {
|
|
2271
|
+
throw new AttestationProtocolError(`unknown key window option ${key2}`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
assertTimestampMs("acceptFrom", window.acceptFrom);
|
|
2275
|
+
assertTimestampMs("acceptUntil", window.acceptUntil);
|
|
2276
|
+
if (window.acceptFrom >= window.acceptUntil) {
|
|
2277
|
+
throw new AttestationProtocolError("key acceptance window must satisfy acceptFrom < acceptUntil");
|
|
2278
|
+
}
|
|
2279
|
+
const key = new Uint8Array((0, import_node_crypto4.randomBytes)(32));
|
|
2280
|
+
return Object.freeze({
|
|
2281
|
+
keyId,
|
|
2282
|
+
key,
|
|
2283
|
+
acceptFrom: window.acceptFrom,
|
|
2284
|
+
acceptUntil: window.acceptUntil
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
function createStaticKeyProvider(snapshot) {
|
|
2288
|
+
const owned = ownSnapshot(snapshot);
|
|
2289
|
+
const provider = {
|
|
2290
|
+
getSnapshot() {
|
|
2291
|
+
return copySnapshot(owned);
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
return Object.freeze(provider);
|
|
2295
|
+
}
|
|
2296
|
+
function createRotatingKeyProvider(snapshot) {
|
|
2297
|
+
let current = ownSnapshot(snapshot);
|
|
2298
|
+
function checkRemappingPreserved(previous, next) {
|
|
2299
|
+
const previousById = /* @__PURE__ */ new Map();
|
|
2300
|
+
for (const entry of previous.keys) {
|
|
2301
|
+
previousById.set(entry.keyId, entry);
|
|
2302
|
+
}
|
|
2303
|
+
for (const entry of next.keys) {
|
|
2304
|
+
const prior = previousById.get(entry.keyId);
|
|
2305
|
+
if (prior === void 0) {
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
const nextKey = entry.key;
|
|
2309
|
+
if (!bytesEqual(prior.key, nextKey)) {
|
|
2310
|
+
throw new AttestationProtocolError(
|
|
2311
|
+
`key snapshot must not remap retained keyId ${entry.keyId} to different key material`
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
if (prior.acceptFrom !== entry.acceptFrom || prior.acceptUntil !== entry.acceptUntil) {
|
|
2315
|
+
throw new AttestationProtocolError(
|
|
2316
|
+
`key snapshot must not remap retained keyId ${entry.keyId} to a different acceptance window`
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const provider = {
|
|
2322
|
+
getSnapshot() {
|
|
2323
|
+
return copySnapshot(current);
|
|
2324
|
+
},
|
|
2325
|
+
replace(next) {
|
|
2326
|
+
assertValidSnapshotShape(next);
|
|
2327
|
+
checkRemappingPreserved(current, next);
|
|
2328
|
+
current = ownSnapshot(next);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
return Object.freeze(provider);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// src/signer-bundle.ts
|
|
2335
|
+
var import_express = require("express");
|
|
2336
|
+
|
|
2337
|
+
// src/signer-assets.ts
|
|
2338
|
+
var import_node_crypto5 = require("crypto");
|
|
2339
|
+
var import_node_fs = require("fs");
|
|
2340
|
+
var import_node_path = __toESM(require("path"));
|
|
2341
|
+
var import_node_url = require("url");
|
|
2342
|
+
var SIGNER_RUNTIME_BYTE_CAP = 524288;
|
|
2343
|
+
var SIGNER_KEY_MODULE_BYTE_CAP = 8192;
|
|
2344
|
+
var SIGNER_ASSET_METADATA_BYTE_CAP = 4096;
|
|
2345
|
+
var CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
2346
|
+
function isContentHash(value) {
|
|
2347
|
+
return typeof value === "string" && CONTENT_HASH_PATTERN.test(value);
|
|
2348
|
+
}
|
|
2349
|
+
function sha256Hex(bytes) {
|
|
2350
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
2351
|
+
throw new AttestationProtocolError("content hash input must be a Uint8Array");
|
|
2352
|
+
}
|
|
2353
|
+
return (0, import_node_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
2354
|
+
}
|
|
2355
|
+
function dirFromStack() {
|
|
2356
|
+
try {
|
|
2357
|
+
const stack = new Error().stack;
|
|
2358
|
+
if (typeof stack !== "string") {
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
for (const line of stack.split("\n")) {
|
|
2362
|
+
const fileUrlMatch = /(file:\/\/[^\s:)]+)/.exec(line);
|
|
2363
|
+
if (fileUrlMatch?.[1] !== void 0) {
|
|
2364
|
+
try {
|
|
2365
|
+
const filePath = (0, import_node_url.fileURLToPath)(new URL(fileUrlMatch[1]));
|
|
2366
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) {
|
|
2367
|
+
return import_node_path.default.dirname(filePath);
|
|
2368
|
+
}
|
|
2369
|
+
} catch {
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const pathMatch = /\((\/[^():]+\.m?[jt]s):\d+:\d+\)/.exec(line) ?? /at\s+(\/[^():\s]+\.m?[jt]s):\d+:\d+/.exec(line);
|
|
2374
|
+
if (pathMatch?.[1] !== void 0) {
|
|
2375
|
+
return import_node_path.default.dirname(pathMatch[1]);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
} catch {
|
|
2379
|
+
}
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
function currentFileCandidates() {
|
|
2383
|
+
const candidates = [];
|
|
2384
|
+
const stackDir = dirFromStack();
|
|
2385
|
+
if (stackDir !== null) {
|
|
2386
|
+
candidates.push(import_node_path.default.join(stackDir, "signer.mjs"));
|
|
2387
|
+
candidates.push(import_node_path.default.join(stackDir, "..", "dist", "signer.mjs"));
|
|
2388
|
+
candidates.push(import_node_path.default.join(stackDir, "dist", "signer.mjs"));
|
|
2389
|
+
}
|
|
2390
|
+
try {
|
|
2391
|
+
const dirname = globalThis.__dirname;
|
|
2392
|
+
if (typeof dirname === "string" && dirname.length > 0) {
|
|
2393
|
+
candidates.push(import_node_path.default.join(dirname, "signer.mjs"));
|
|
2394
|
+
candidates.push(import_node_path.default.join(dirname, "..", "dist", "signer.mjs"));
|
|
2395
|
+
}
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
const filename = globalThis.__filename;
|
|
2400
|
+
if (typeof filename === "string" && filename.length > 0) {
|
|
2401
|
+
candidates.push(import_node_path.default.join(import_node_path.default.dirname(filename), "signer.mjs"));
|
|
2402
|
+
candidates.push(import_node_path.default.join(import_node_path.default.dirname(filename), "..", "dist", "signer.mjs"));
|
|
2403
|
+
}
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
return candidates;
|
|
2407
|
+
}
|
|
2408
|
+
var cachedRuntimeBytes = null;
|
|
2409
|
+
var cachedRuntimeHash = null;
|
|
2410
|
+
function loadSignerRuntimeBytes() {
|
|
2411
|
+
if (cachedRuntimeBytes !== null) {
|
|
2412
|
+
return new Uint8Array(cachedRuntimeBytes);
|
|
2413
|
+
}
|
|
2414
|
+
const candidates = currentFileCandidates();
|
|
2415
|
+
for (const candidate of candidates) {
|
|
2416
|
+
try {
|
|
2417
|
+
const read = (0, import_node_fs.readFileSync)(candidate);
|
|
2418
|
+
if (read.length === 0 || read.length > SIGNER_RUNTIME_BYTE_CAP) {
|
|
2419
|
+
throw new AttestationProtocolError(`signer runtime exceeds the ${SIGNER_RUNTIME_BYTE_CAP}-byte cap`);
|
|
2420
|
+
}
|
|
2421
|
+
const bytes = new Uint8Array(read.buffer.slice(read.byteOffset, read.byteOffset + read.byteLength));
|
|
2422
|
+
cachedRuntimeBytes = bytes;
|
|
2423
|
+
cachedRuntimeHash = sha256Hex(bytes);
|
|
2424
|
+
return new Uint8Array(bytes);
|
|
2425
|
+
} catch (error) {
|
|
2426
|
+
const code = error.code;
|
|
2427
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (error instanceof AttestationProtocolError) {
|
|
2431
|
+
throw error;
|
|
2432
|
+
}
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
throw new AttestationProtocolError(
|
|
2437
|
+
`signer runtime (dist/signer.mjs sibling) could not be located${candidates.length > 0 ? ` (tried ${candidates.length} candidate path(s))` : ""}. Build the package before serving signer assets.`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
function getSignerRuntimeAsset() {
|
|
2441
|
+
const bytes = loadSignerRuntimeBytes();
|
|
2442
|
+
const contentHash = cachedRuntimeHash ?? sha256Hex(bytes);
|
|
2443
|
+
if (cachedRuntimeHash === null) {
|
|
2444
|
+
cachedRuntimeHash = contentHash;
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
bytes,
|
|
2448
|
+
contentHash,
|
|
2449
|
+
fileName: `runtime.${contentHash}.mjs`
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
function buildSignerKeyModuleSource(input) {
|
|
2453
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
2454
|
+
throw new AttestationProtocolError("signer key module input must be an object");
|
|
2455
|
+
}
|
|
2456
|
+
const { keyId, key, publicOrigin, replayNamespace, runtimeFileName } = input;
|
|
2457
|
+
if (typeof keyId !== "string" || typeof publicOrigin !== "string" || typeof replayNamespace !== "string") {
|
|
2458
|
+
throw new AttestationProtocolError("signer key module fields must be strings");
|
|
2459
|
+
}
|
|
2460
|
+
if (!(key instanceof Uint8Array) || key.length !== 32) {
|
|
2461
|
+
throw new AttestationProtocolError("signer key module key must be 32 bytes");
|
|
2462
|
+
}
|
|
2463
|
+
if (typeof runtimeFileName !== "string" || runtimeFileName.length === 0) {
|
|
2464
|
+
throw new AttestationProtocolError("signer key module runtime file name must be a non-empty string");
|
|
2465
|
+
}
|
|
2466
|
+
if (runtimeFileName.includes("\0") || runtimeFileName.includes("\\")) {
|
|
2467
|
+
throw new AttestationProtocolError("signer key module runtime file name is invalid");
|
|
2468
|
+
}
|
|
2469
|
+
const runtimeSpecifier = `./${runtimeFileName}`;
|
|
2470
|
+
const lines = [
|
|
2471
|
+
`import { createRequestSigner } from ${JSON.stringify(runtimeSpecifier)};`,
|
|
2472
|
+
`const __attestationKeyBytes = new Uint8Array(${JSON.stringify(Array.from(key))});`,
|
|
2473
|
+
`export const signer = createRequestSigner({`,
|
|
2474
|
+
` keyId: ${JSON.stringify(keyId)},`,
|
|
2475
|
+
` key: __attestationKeyBytes,`,
|
|
2476
|
+
` publicOrigin: ${JSON.stringify(publicOrigin)},`,
|
|
2477
|
+
` replayNamespace: ${JSON.stringify(replayNamespace)},`,
|
|
2478
|
+
`});`,
|
|
2479
|
+
``
|
|
2480
|
+
];
|
|
2481
|
+
return lines.join("\n");
|
|
2482
|
+
}
|
|
2483
|
+
function buildSignerKeyModuleBytes(input) {
|
|
2484
|
+
const source = buildSignerKeyModuleSource({
|
|
2485
|
+
keyId: input.entry.keyId,
|
|
2486
|
+
key: input.entry.key,
|
|
2487
|
+
publicOrigin: input.publicOrigin,
|
|
2488
|
+
replayNamespace: input.replayNamespace,
|
|
2489
|
+
runtimeFileName: input.runtimeFileName
|
|
2490
|
+
});
|
|
2491
|
+
if (utf8ByteLength(source) > SIGNER_KEY_MODULE_BYTE_CAP) {
|
|
2492
|
+
throw new AttestationProtocolError(`signer key module exceeds the ${SIGNER_KEY_MODULE_BYTE_CAP}-byte cap`);
|
|
2493
|
+
}
|
|
2494
|
+
return utf8Encode(source);
|
|
2495
|
+
}
|
|
2496
|
+
function getSignerKeyModuleAsset(input) {
|
|
2497
|
+
const bytes = buildSignerKeyModuleBytes(input);
|
|
2498
|
+
const contentHash = sha256Hex(bytes);
|
|
2499
|
+
return {
|
|
2500
|
+
bytes,
|
|
2501
|
+
contentHash,
|
|
2502
|
+
fileName: `signer.${contentHash}.mjs`
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/signer-bundle.ts
|
|
2507
|
+
var SIGNER_BUNDLE_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2508
|
+
"publicOrigin",
|
|
2509
|
+
"replayNamespace",
|
|
2510
|
+
"keyProvider",
|
|
2511
|
+
"basePath",
|
|
2512
|
+
"publicPathPrefix",
|
|
2513
|
+
"now",
|
|
2514
|
+
"operationTimeoutMs"
|
|
2515
|
+
]);
|
|
2516
|
+
var DEFAULT_SIGNER_BASE_PATH = "/attestation";
|
|
2517
|
+
var DEFAULT_OPERATION_TIMEOUT_MS2 = 5e3;
|
|
2518
|
+
var MIN_OPERATION_TIMEOUT_MS2 = 1;
|
|
2519
|
+
var MAX_OPERATION_TIMEOUT_MS2 = 6e4;
|
|
2520
|
+
var SignerBundleOperationTimeoutError = class extends Error {
|
|
2521
|
+
name = "SignerBundleOperationTimeoutError";
|
|
2522
|
+
};
|
|
2523
|
+
function validateBasePath(value) {
|
|
2524
|
+
if (value === void 0) {
|
|
2525
|
+
return DEFAULT_SIGNER_BASE_PATH;
|
|
2526
|
+
}
|
|
2527
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2528
|
+
throw new AttestationProtocolError("signer bundle basePath must be a non-empty string");
|
|
2529
|
+
}
|
|
2530
|
+
if (!value.startsWith("/")) {
|
|
2531
|
+
throw new AttestationProtocolError("signer bundle basePath must start with /");
|
|
2532
|
+
}
|
|
2533
|
+
if (value.length > 1 && value.endsWith("/")) {
|
|
2534
|
+
throw new AttestationProtocolError("signer bundle basePath must not end with /");
|
|
2535
|
+
}
|
|
2536
|
+
if (value.includes("?") || value.includes("#") || value.includes("\\")) {
|
|
2537
|
+
throw new AttestationProtocolError("signer bundle basePath must not contain query, fragment, or backslashes");
|
|
2538
|
+
}
|
|
2539
|
+
for (const char of value) {
|
|
2540
|
+
const code = char.codePointAt(0);
|
|
2541
|
+
if (code <= 32 || code === 127) {
|
|
2542
|
+
throw new AttestationProtocolError("signer bundle basePath must not contain controls or spaces");
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return value;
|
|
2546
|
+
}
|
|
2547
|
+
function validatePublicPathPrefix2(value) {
|
|
2548
|
+
if (value === void 0) {
|
|
2549
|
+
return "";
|
|
2550
|
+
}
|
|
2551
|
+
if (typeof value !== "string") {
|
|
2552
|
+
throw new AttestationProtocolError("signer bundle publicPathPrefix must be a string");
|
|
2553
|
+
}
|
|
2554
|
+
if (value === "") {
|
|
2555
|
+
return "";
|
|
2556
|
+
}
|
|
2557
|
+
if (!value.startsWith("/")) {
|
|
2558
|
+
throw new AttestationProtocolError("signer bundle publicPathPrefix must start with /");
|
|
2559
|
+
}
|
|
2560
|
+
if (value.endsWith("/")) {
|
|
2561
|
+
throw new AttestationProtocolError("signer bundle publicPathPrefix must not end with /");
|
|
2562
|
+
}
|
|
2563
|
+
if (value.includes("?") || value.includes("#") || value.includes("\\")) {
|
|
2564
|
+
throw new AttestationProtocolError(
|
|
2565
|
+
"signer bundle publicPathPrefix must not contain query, fragment, or backslashes"
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
for (const char of value) {
|
|
2569
|
+
const code = char.codePointAt(0);
|
|
2570
|
+
if (code <= 32 || code === 127) {
|
|
2571
|
+
throw new AttestationProtocolError("signer bundle publicPathPrefix must not contain controls or spaces");
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return value;
|
|
2575
|
+
}
|
|
2576
|
+
function checkedNow2(now) {
|
|
2577
|
+
let value;
|
|
2578
|
+
try {
|
|
2579
|
+
value = now();
|
|
2580
|
+
} catch {
|
|
2581
|
+
throw new AttestationError("ATTESTATION_INTERNAL_ERROR");
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
assertTimestampMs("nowMs", value);
|
|
2585
|
+
} catch {
|
|
2586
|
+
throw new AttestationError("ATTESTATION_INTERNAL_ERROR");
|
|
2587
|
+
}
|
|
2588
|
+
return value;
|
|
2589
|
+
}
|
|
2590
|
+
function withOperationTimeout2(task, ms) {
|
|
2591
|
+
let timer;
|
|
2592
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
2593
|
+
timer = setTimeout(() => {
|
|
2594
|
+
reject(new SignerBundleOperationTimeoutError(`operation timed out after ${ms} ms`));
|
|
2595
|
+
}, ms);
|
|
2596
|
+
const handle = timer;
|
|
2597
|
+
if (typeof handle.unref === "function") {
|
|
2598
|
+
handle.unref();
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
const raced = Promise.race([task, timeout]);
|
|
2602
|
+
return raced.finally(() => {
|
|
2603
|
+
if (timer !== void 0) {
|
|
2604
|
+
clearTimeout(timer);
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
function resolveConfig2(options) {
|
|
2609
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
2610
|
+
throw new AttestationProtocolError("signer bundle router options must be an object");
|
|
2611
|
+
}
|
|
2612
|
+
for (const key of Object.keys(options)) {
|
|
2613
|
+
if (!SIGNER_BUNDLE_OPTION_KEYS.has(key)) {
|
|
2614
|
+
throw new AttestationProtocolError(`unknown signer bundle router option ${key}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
validatePublicOrigin(options.publicOrigin);
|
|
2618
|
+
validateReplayNamespace(options.replayNamespace);
|
|
2619
|
+
const keyProvider = options.keyProvider;
|
|
2620
|
+
if (keyProvider === null || typeof keyProvider !== "object" || typeof keyProvider.getSnapshot !== "function") {
|
|
2621
|
+
throw new AttestationProtocolError("signer bundle router keyProvider must expose getSnapshot()");
|
|
2622
|
+
}
|
|
2623
|
+
const basePath = validateBasePath(options.basePath);
|
|
2624
|
+
const publicPathPrefix = validatePublicPathPrefix2(options.publicPathPrefix);
|
|
2625
|
+
const now = options.now ?? Date.now;
|
|
2626
|
+
if (typeof now !== "function") {
|
|
2627
|
+
throw new AttestationProtocolError("signer bundle router now must be a function");
|
|
2628
|
+
}
|
|
2629
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS2;
|
|
2630
|
+
if (!Number.isSafeInteger(operationTimeoutMs) || operationTimeoutMs < MIN_OPERATION_TIMEOUT_MS2 || operationTimeoutMs > MAX_OPERATION_TIMEOUT_MS2) {
|
|
2631
|
+
throw new AttestationProtocolError(
|
|
2632
|
+
`operationTimeoutMs must be a safe integer in [${MIN_OPERATION_TIMEOUT_MS2}, ${MAX_OPERATION_TIMEOUT_MS2}]`
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
return Object.freeze({
|
|
2636
|
+
publicOrigin: options.publicOrigin,
|
|
2637
|
+
replayNamespace: options.replayNamespace,
|
|
2638
|
+
keyProvider,
|
|
2639
|
+
basePath,
|
|
2640
|
+
publicPathPrefix,
|
|
2641
|
+
now,
|
|
2642
|
+
operationTimeoutMs
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
function sendKeysUnavailable(res) {
|
|
2646
|
+
const error = new AttestationError("ATTESTATION_KEYS_UNAVAILABLE");
|
|
2647
|
+
res.set("Cache-Control", "no-store");
|
|
2648
|
+
res.status(error.status).json({ code: error.code, message: error.message });
|
|
2649
|
+
}
|
|
2650
|
+
function sendInternalError(res) {
|
|
2651
|
+
const error = new AttestationError("ATTESTATION_INTERNAL_ERROR");
|
|
2652
|
+
try {
|
|
2653
|
+
res.set("Cache-Control", "no-store");
|
|
2654
|
+
} catch {
|
|
2655
|
+
}
|
|
2656
|
+
try {
|
|
2657
|
+
res.status(error.status).json({ code: error.code, message: error.message });
|
|
2658
|
+
} catch {
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function sendAssetNotFound(res, gone) {
|
|
2662
|
+
res.set("Cache-Control", "no-store");
|
|
2663
|
+
if (gone) {
|
|
2664
|
+
res.status(410).json({
|
|
2665
|
+
code: "ATTESTATION_ASSET_GONE",
|
|
2666
|
+
message: "Signer asset is no longer available."
|
|
2667
|
+
});
|
|
2668
|
+
} else {
|
|
2669
|
+
res.status(404).json({
|
|
2670
|
+
code: "ATTESTATION_ASSET_NOT_FOUND",
|
|
2671
|
+
message: "Signer asset is not available."
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
function createSignerBundleRouter(options) {
|
|
2676
|
+
const config = resolveConfig2(options);
|
|
2677
|
+
const router = (0, import_express.Router)();
|
|
2678
|
+
router.get("/signer-meta", (req, res) => {
|
|
2679
|
+
void Promise.resolve().then(async () => {
|
|
2680
|
+
let nowMs;
|
|
2681
|
+
try {
|
|
2682
|
+
nowMs = checkedNow2(config.now);
|
|
2683
|
+
} catch (error) {
|
|
2684
|
+
if (error instanceof AttestationError) {
|
|
2685
|
+
sendInternalError(res);
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
throw error;
|
|
2689
|
+
}
|
|
2690
|
+
let snapshotRaw;
|
|
2691
|
+
try {
|
|
2692
|
+
const pending = config.keyProvider.getSnapshot();
|
|
2693
|
+
snapshotRaw = await withOperationTimeout2(Promise.resolve(pending), config.operationTimeoutMs);
|
|
2694
|
+
} catch {
|
|
2695
|
+
sendKeysUnavailable(res);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
let snapshot;
|
|
2699
|
+
try {
|
|
2700
|
+
snapshot = copyAttestationSnapshot(snapshotRaw);
|
|
2701
|
+
} catch {
|
|
2702
|
+
sendKeysUnavailable(res);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const current = snapshot.keys.find((entry) => entry.keyId === snapshot.currentKeyId);
|
|
2706
|
+
if (current === void 0) {
|
|
2707
|
+
sendKeysUnavailable(res);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
if (!(current.acceptFrom <= nowMs && nowMs < current.acceptUntil)) {
|
|
2711
|
+
sendKeysUnavailable(res);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
let runtimeFileName;
|
|
2715
|
+
try {
|
|
2716
|
+
const runtime = getSignerRuntimeAsset();
|
|
2717
|
+
runtimeFileName = runtime.fileName;
|
|
2718
|
+
} catch {
|
|
2719
|
+
sendInternalError(res);
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
let keyAsset;
|
|
2723
|
+
try {
|
|
2724
|
+
keyAsset = getSignerKeyModuleAsset({
|
|
2725
|
+
entry: current,
|
|
2726
|
+
publicOrigin: config.publicOrigin,
|
|
2727
|
+
replayNamespace: config.replayNamespace,
|
|
2728
|
+
runtimeFileName
|
|
2729
|
+
});
|
|
2730
|
+
} catch {
|
|
2731
|
+
sendInternalError(res);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
const signerUrl = `${config.publicOrigin}${config.publicPathPrefix}${config.basePath}/signer.${keyAsset.contentHash}.mjs`;
|
|
2735
|
+
const document = {
|
|
2736
|
+
version: PROTOCOL_VERSION,
|
|
2737
|
+
keyId: current.keyId,
|
|
2738
|
+
signerUrl,
|
|
2739
|
+
publicOrigin: config.publicOrigin,
|
|
2740
|
+
replayNamespace: config.replayNamespace
|
|
2741
|
+
};
|
|
2742
|
+
let body;
|
|
2743
|
+
try {
|
|
2744
|
+
body = JSON.stringify(document);
|
|
2745
|
+
} catch {
|
|
2746
|
+
sendInternalError(res);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (utf8ByteLength(body) > SIGNER_ASSET_METADATA_BYTE_CAP) {
|
|
2750
|
+
sendInternalError(res);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
res.set("Cache-Control", "no-store");
|
|
2754
|
+
res.set("Content-Type", "application/json; charset=utf-8");
|
|
2755
|
+
res.status(200).send(body);
|
|
2756
|
+
}).catch(() => {
|
|
2757
|
+
try {
|
|
2758
|
+
if (!res.headersSent) {
|
|
2759
|
+
sendInternalError(res);
|
|
2760
|
+
}
|
|
2761
|
+
} catch {
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
});
|
|
2765
|
+
router.get("/signer.:contentHash.mjs", (req, res) => {
|
|
2766
|
+
void Promise.resolve().then(async () => {
|
|
2767
|
+
const params = req.params ?? {};
|
|
2768
|
+
const requested = params["contentHash"];
|
|
2769
|
+
if (typeof requested !== "string" || requested.length === 0) {
|
|
2770
|
+
sendAssetNotFound(res, false);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (!isContentHash(requested)) {
|
|
2774
|
+
sendAssetNotFound(res, false);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
let snapshotRaw;
|
|
2778
|
+
try {
|
|
2779
|
+
const pending = config.keyProvider.getSnapshot();
|
|
2780
|
+
snapshotRaw = await withOperationTimeout2(Promise.resolve(pending), config.operationTimeoutMs);
|
|
2781
|
+
} catch {
|
|
2782
|
+
sendKeysUnavailable(res);
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
let snapshot;
|
|
2786
|
+
try {
|
|
2787
|
+
snapshot = copyAttestationSnapshot(snapshotRaw);
|
|
2788
|
+
} catch {
|
|
2789
|
+
sendKeysUnavailable(res);
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
let runtimeFileName;
|
|
2793
|
+
try {
|
|
2794
|
+
runtimeFileName = getSignerRuntimeAsset().fileName;
|
|
2795
|
+
} catch {
|
|
2796
|
+
sendInternalError(res);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
for (const entry of snapshot.keys) {
|
|
2800
|
+
let asset;
|
|
2801
|
+
try {
|
|
2802
|
+
asset = getSignerKeyModuleAsset({
|
|
2803
|
+
entry,
|
|
2804
|
+
publicOrigin: config.publicOrigin,
|
|
2805
|
+
replayNamespace: config.replayNamespace,
|
|
2806
|
+
runtimeFileName
|
|
2807
|
+
});
|
|
2808
|
+
} catch {
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
if (asset.contentHash === requested) {
|
|
2812
|
+
res.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
2813
|
+
res.set("Content-Type", "text/javascript; charset=utf-8");
|
|
2814
|
+
res.set("X-Content-Type-Options", "nosniff");
|
|
2815
|
+
res.status(200).send(Buffer.from(asset.bytes));
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
sendAssetNotFound(res, true);
|
|
2820
|
+
}).catch(() => {
|
|
2821
|
+
try {
|
|
2822
|
+
if (!res.headersSent) {
|
|
2823
|
+
sendInternalError(res);
|
|
2824
|
+
}
|
|
2825
|
+
} catch {
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
});
|
|
2829
|
+
router.get("/runtime.:contentHash.mjs", (req, res) => {
|
|
2830
|
+
try {
|
|
2831
|
+
const params = req.params ?? {};
|
|
2832
|
+
const requested = params["contentHash"];
|
|
2833
|
+
if (typeof requested !== "string" || requested.length === 0) {
|
|
2834
|
+
sendAssetNotFound(res, false);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (!isContentHash(requested)) {
|
|
2838
|
+
sendAssetNotFound(res, false);
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
let asset;
|
|
2842
|
+
try {
|
|
2843
|
+
asset = getSignerRuntimeAsset();
|
|
2844
|
+
} catch {
|
|
2845
|
+
sendInternalError(res);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
if (asset.contentHash !== requested) {
|
|
2849
|
+
sendAssetNotFound(res, true);
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
res.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
2853
|
+
res.set("Content-Type", "text/javascript; charset=utf-8");
|
|
2854
|
+
res.set("X-Content-Type-Options", "nosniff");
|
|
2855
|
+
res.status(200).send(Buffer.from(asset.bytes));
|
|
2856
|
+
} catch {
|
|
2857
|
+
try {
|
|
2858
|
+
if (!res.headersSent) {
|
|
2859
|
+
sendInternalError(res);
|
|
2860
|
+
}
|
|
2861
|
+
} catch {
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
});
|
|
2865
|
+
return router;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/index.ts
|
|
2869
|
+
function hashBodySha256Hex(body) {
|
|
2870
|
+
if (!(body instanceof Uint8Array)) {
|
|
2871
|
+
throw new AttestationProtocolError("body must be a Uint8Array");
|
|
2872
|
+
}
|
|
2873
|
+
return (0, import_node_crypto6.createHash)("sha256").update(body).digest("hex");
|
|
2874
|
+
}
|
|
2875
|
+
function deriveAttestationReplayKey(input) {
|
|
2876
|
+
const preimage = buildReplayKeyPreimageBytes(input);
|
|
2877
|
+
const digest = (0, import_node_crypto6.createHash)("sha256").update(preimage).digest("hex");
|
|
2878
|
+
const replayKey = `${REPLAY_KEY_PREFIX}${digest}`;
|
|
2879
|
+
validateReplayKey(replayKey);
|
|
2880
|
+
return replayKey;
|
|
2881
|
+
}
|