@invisible-labs/sdk 0.6.0-devnet.4 → 0.6.0-devnet.5

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.
Files changed (46) hide show
  1. package/README.md +70 -13
  2. package/dist/.invisible-sdk-build-target.json +2 -2
  3. package/dist/{chunk-UADJCUPK.js → chunk-3MDHAS4D.js} +6 -5
  4. package/dist/chunk-3MDHAS4D.js.map +1 -0
  5. package/dist/{chunk-E4DCEDAG.js → chunk-3TGQ4CZ3.js} +178 -2
  6. package/dist/chunk-3TGQ4CZ3.js.map +1 -0
  7. package/dist/chunk-DCTYBUY3.js +258 -0
  8. package/dist/chunk-DCTYBUY3.js.map +1 -0
  9. package/dist/{chunk-NHEHCUS5.js → chunk-EBWTAXNT.js} +3 -3
  10. package/dist/{chunk-NHEHCUS5.js.map → chunk-EBWTAXNT.js.map} +1 -1
  11. package/dist/{chunk-V5HXJRBW.js → chunk-HJFMDMPY.js} +3 -3
  12. package/dist/chunk-HJFMDMPY.js.map +1 -0
  13. package/dist/{chunk-GHBQF3A7.js → chunk-QRN46R3F.js} +5 -2
  14. package/dist/chunk-QRN46R3F.js.map +1 -0
  15. package/dist/chunk-Y7AK6RBV.js +4189 -0
  16. package/dist/chunk-Y7AK6RBV.js.map +1 -0
  17. package/dist/{coordinator-ZNLGAYXD.js → coordinator-PEWHNZ7I.js} +3 -3
  18. package/dist/{coordinator-ZNLGAYXD.js.map → coordinator-PEWHNZ7I.js.map} +1 -1
  19. package/dist/events.js +3 -3
  20. package/dist/index.d.ts +14 -4
  21. package/dist/index.js +74 -11
  22. package/dist/index.js.map +1 -1
  23. package/dist/lp.d.ts +33 -9
  24. package/dist/lp.js +305 -45
  25. package/dist/lp.js.map +1 -1
  26. package/dist/presets.d.ts +6 -2
  27. package/dist/presets.js +8 -8
  28. package/dist/presets.js.map +1 -1
  29. package/dist/stats.js +1 -1
  30. package/dist/storage.js +2 -243
  31. package/dist/storage.js.map +1 -1
  32. package/dist/{types.generated-DJBTjLpP.d.ts → types.generated-YhFu9ZBR.d.ts} +29 -4
  33. package/dist/user.d.ts +31 -4
  34. package/dist/user.js +409 -57
  35. package/dist/user.js.map +1 -1
  36. package/package.json +4 -1
  37. package/dist/chunk-5NNEO7IK.js +0 -1150
  38. package/dist/chunk-5NNEO7IK.js.map +0 -1
  39. package/dist/chunk-BRDLLGCZ.js +0 -59
  40. package/dist/chunk-BRDLLGCZ.js.map +0 -1
  41. package/dist/chunk-DIA6U2CV.js +0 -1927
  42. package/dist/chunk-DIA6U2CV.js.map +0 -1
  43. package/dist/chunk-E4DCEDAG.js.map +0 -1
  44. package/dist/chunk-GHBQF3A7.js.map +0 -1
  45. package/dist/chunk-UADJCUPK.js.map +0 -1
  46. package/dist/chunk-V5HXJRBW.js.map +0 -1
@@ -0,0 +1,4189 @@
1
+ import { getSessionState, failSessionClosed, createSessionHandle, closeSessionState } from './chunk-HJFMDMPY.js';
2
+ import { getStorageBacking, browserStorage, extensionStorage } from './chunk-DCTYBUY3.js';
3
+ import { TransportError, StorageError, AttestationError } from './chunk-QRN46R3F.js';
4
+ import * as x509 from '@peculiar/x509';
5
+ import { generateKeyPair, sharedKey } from '@stablelib/x25519';
6
+ import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305';
7
+ import { hash, SHA256 } from '@stablelib/sha256';
8
+ import { HMAC } from '@stablelib/hmac';
9
+ import { Ajv2020 } from 'ajv/dist/2020.js';
10
+
11
+ // src/core/exponentialBackoff.ts
12
+ function createExponentialBackoff(options) {
13
+ if (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs <= 0) {
14
+ throw new Error("initialDelayMs must be a positive finite number");
15
+ }
16
+ if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < options.initialDelayMs) {
17
+ throw new Error("maxDelayMs must be finite and >= initialDelayMs");
18
+ }
19
+ if (options.multiplier !== void 0 && (!Number.isFinite(options.multiplier) || options.multiplier < 1)) {
20
+ throw new Error("multiplier must be finite and >= 1");
21
+ }
22
+ if (options.jitterRatio !== void 0 && (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0 || options.jitterRatio > 1)) {
23
+ throw new Error("jitterRatio must be between 0 and 1");
24
+ }
25
+ const initialDelayMs = options.initialDelayMs;
26
+ const maxDelayMs = options.maxDelayMs;
27
+ const multiplier = options.multiplier ?? 2;
28
+ const jitterRatio = options.jitterRatio ?? 0;
29
+ const random = options.random ?? Math.random;
30
+ let attempt = 0;
31
+ return {
32
+ nextDelayMs() {
33
+ const baseDelayMs = Math.min(maxDelayMs, initialDelayMs * multiplier ** attempt);
34
+ attempt += 1;
35
+ if (jitterRatio === 0) return Math.round(baseDelayMs);
36
+ const lowerBound = baseDelayMs * (1 - jitterRatio);
37
+ const upperBound = baseDelayMs * (1 + jitterRatio);
38
+ const boundedRandom = Math.min(Math.max(random(), 0), 1);
39
+ return Math.min(
40
+ maxDelayMs,
41
+ Math.round(lowerBound + (upperBound - lowerBound) * boundedRandom)
42
+ );
43
+ },
44
+ reset() {
45
+ attempt = 0;
46
+ }
47
+ };
48
+ }
49
+
50
+ // src/transport/stateController.ts
51
+ function createStateController(initial) {
52
+ let state = initial;
53
+ const listeners = /* @__PURE__ */ new Set();
54
+ return {
55
+ get: () => state,
56
+ set(next) {
57
+ if (next === state) return;
58
+ state = next;
59
+ for (const listener of listeners) listener(next);
60
+ },
61
+ onChange(listener) {
62
+ listeners.add(listener);
63
+ return () => listeners.delete(listener);
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/transport/noiseWebSocketTransport.ts
69
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
70
+ function defaultWebSocketFactory(url) {
71
+ const ctor = globalThis.WebSocket;
72
+ if (!ctor) {
73
+ throw new TransportError(
74
+ "WS_HANDSHAKE_FAILED",
75
+ "no global WebSocket available; pass options.webSocketFactory"
76
+ );
77
+ }
78
+ return new ctor(url);
79
+ }
80
+ function toUint8Array(data) {
81
+ if (data instanceof Uint8Array) return data;
82
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
83
+ if (ArrayBuffer.isView(data)) {
84
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
85
+ }
86
+ if (typeof data === "string") return new TextEncoder().encode(data);
87
+ throw new TransportError("NOISE_FRAME_TOO_LARGE", "unsupported inbound frame type");
88
+ }
89
+ function noiseWebSocketTransport(options) {
90
+ const factory = options.webSocketFactory ?? defaultWebSocketFactory;
91
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
92
+ const sc = createStateController("idle");
93
+ const messageListeners = /* @__PURE__ */ new Set();
94
+ let socket = null;
95
+ function teardown() {
96
+ if (sc.get() === "closed") return;
97
+ sc.set("closing");
98
+ const ws = socket;
99
+ socket = null;
100
+ if (ws) {
101
+ ws.onopen = null;
102
+ ws.onerror = null;
103
+ ws.onmessage = null;
104
+ ws.onclose = null;
105
+ try {
106
+ ws.close();
107
+ } catch {
108
+ }
109
+ }
110
+ sc.set("closed");
111
+ }
112
+ return {
113
+ state: () => sc.get(),
114
+ onStateChange: (listener) => sc.onChange(listener),
115
+ onMessage(listener) {
116
+ messageListeners.add(listener);
117
+ return () => messageListeners.delete(listener);
118
+ },
119
+ connect(signal) {
120
+ if (sc.get() === "open") return Promise.resolve();
121
+ if (signal?.aborted) {
122
+ return Promise.reject(
123
+ new TransportError("CONNECTION_LOST", "connect aborted before it started")
124
+ );
125
+ }
126
+ sc.set("connecting");
127
+ return new Promise((resolve, reject) => {
128
+ let settled = false;
129
+ const ws = factory(options.wsUrl);
130
+ socket = ws;
131
+ ws.binaryType = "arraybuffer";
132
+ const cleanupConnect = () => {
133
+ clearTimeout(timer);
134
+ signal?.removeEventListener("abort", onAbort);
135
+ };
136
+ const fail = (error) => {
137
+ if (settled) return;
138
+ settled = true;
139
+ cleanupConnect();
140
+ teardown();
141
+ reject(error);
142
+ };
143
+ const onAbort = () => fail(new TransportError("CONNECTION_LOST", "connect aborted"));
144
+ const timer = setTimeout(
145
+ () => fail(
146
+ new TransportError(
147
+ "WS_HANDSHAKE_FAILED",
148
+ `connect timed out after ${connectTimeoutMs}ms`
149
+ )
150
+ ),
151
+ connectTimeoutMs
152
+ );
153
+ signal?.addEventListener("abort", onAbort, { once: true });
154
+ ws.onopen = () => {
155
+ if (settled) return;
156
+ settled = true;
157
+ cleanupConnect();
158
+ ws.onmessage = (event) => {
159
+ const frame = toUint8Array(event.data);
160
+ for (const listener of messageListeners) listener(frame);
161
+ };
162
+ ws.onclose = () => teardown();
163
+ ws.onerror = () => teardown();
164
+ sc.set("open");
165
+ resolve();
166
+ };
167
+ ws.onerror = () => fail(new TransportError("WS_HANDSHAKE_FAILED", "socket error during connect"));
168
+ ws.onclose = () => fail(new TransportError("CONNECTION_LOST", "socket closed during connect"));
169
+ });
170
+ },
171
+ send(frame) {
172
+ const ws = socket;
173
+ if (sc.get() !== "open" || !ws) {
174
+ throw new TransportError("CONNECTION_LOST", "cannot send on a non-open transport");
175
+ }
176
+ ws.send(frame);
177
+ },
178
+ close() {
179
+ teardown();
180
+ }
181
+ };
182
+ }
183
+
184
+ // src/storage/clientPersistence.ts
185
+ var CLIENT_PERSISTENCE_NAMESPACE = "normal-user-transfers";
186
+ var CLIENT_PERSISTENCE_SCHEMA_VERSION = 1;
187
+ var CLIENT_PERSISTENCE_MAX_RECORD_BYTES = 64 * 1024;
188
+ var CLIENT_PERSISTENCE_MAX_RECORD_COUNT = 100;
189
+ var CLIENT_PERSISTENCE_MAX_NAMESPACE_BYTES = 1024 * 1024;
190
+ var HEX_BYTE_LENGTH = 32;
191
+ var HEX_LENGTH = HEX_BYTE_LENGTH * 2;
192
+ var MAX_IDENTIFIER_LENGTH = 256;
193
+ var MAX_ADDRESS_LENGTH = 128;
194
+ var MAX_POLICY_DESTINATION_COUNT = CLIENT_PERSISTENCE_MAX_RECORD_COUNT;
195
+ var MAX_POLICY_JSON_BYTES = CLIENT_PERSISTENCE_MAX_RECORD_BYTES;
196
+ var CLIENT_PERSISTENCE_PROBE_KEY_PREFIX = "__sdk_probe__";
197
+ var CLIENT_PERSISTENCE_PROBE_BYTES = Uint8Array.from([83, 68, 75, 49]);
198
+ var CLIENT_PERSISTENCE_NAMESPACE_GATE_KEY = "__sdk_namespace_gate__";
199
+ var PERSISTED_STAGE_VALUES = /* @__PURE__ */ new Set([
200
+ "allocation",
201
+ "ready",
202
+ "terminal-cleaned"
203
+ ]);
204
+ var CLIENT_SWAP_STATE_VALUES = /* @__PURE__ */ new Set([
205
+ "new",
206
+ "awaiting_dkg",
207
+ "awaiting_deposit",
208
+ "awaiting_delegation",
209
+ "awaiting_delegate_ack",
210
+ "awaiting_delegate_ack_after_deposit",
211
+ "ready_for_settlement",
212
+ "settling",
213
+ "completed",
214
+ "refunded",
215
+ "failed"
216
+ ]);
217
+ var SERIALIZED_RECORD_KEYS = /* @__PURE__ */ new Set([
218
+ "schemaVersion",
219
+ "stage",
220
+ "swapId",
221
+ "amountLamports",
222
+ "destinationAddress",
223
+ "policySnapshot",
224
+ "status",
225
+ "stateVersion",
226
+ "updatedAtMs",
227
+ "coordinatorTimestampMs",
228
+ "depositAddress",
229
+ "depositExpiresAtMs",
230
+ "delegatedAtMs",
231
+ "syncSecretHex",
232
+ "recoveryCodeHex"
233
+ ]);
234
+ var POLICY_SNAPSHOT_KEYS = /* @__PURE__ */ new Set([
235
+ "asset",
236
+ "entry_amount_lamports",
237
+ "total_committed_lamports",
238
+ "payout_deadline_ms",
239
+ "payout_window_ms",
240
+ "payout_schedule",
241
+ "fragment_count",
242
+ "payout_mode",
243
+ "matching_mode",
244
+ "fragmentation_allowed",
245
+ "min_fee_bps",
246
+ "fee_bps",
247
+ "fee_lamports",
248
+ "net_payout_lamports"
249
+ ]);
250
+ var POLICY_DESTINATION_KEYS = /* @__PURE__ */ new Set(["destination_address"]);
251
+ var HEX_PATTERN = /^[0-9a-f]+$/u;
252
+ var terminalWriteBarriers = /* @__PURE__ */ new WeakMap();
253
+ var namespaceWriteGates = /* @__PURE__ */ new WeakMap();
254
+ var automaticStorageAdapters = /* @__PURE__ */ new WeakMap();
255
+ var clientPersistenceProbeSequence = 0;
256
+ function disabledClientPersistence() {
257
+ return { enabled: false, adapter: null };
258
+ }
259
+ async function resolveClientPersistence(enabled, storage) {
260
+ let adapter = null;
261
+ let automaticBackend = null;
262
+ try {
263
+ if (storage === void 0) {
264
+ const selection = automaticStorageAdapter();
265
+ adapter = selection.adapter;
266
+ automaticBackend = selection.backend;
267
+ } else {
268
+ adapter = validateStorageAdapter(storage);
269
+ }
270
+ if (adapter === null) {
271
+ throw new StorageError("STORAGE_NOT_AVAILABLE", "client persistence storage is unavailable");
272
+ }
273
+ await probeStorage(adapter);
274
+ return { enabled: true, adapter };
275
+ } catch (error) {
276
+ if (automaticBackend !== null && adapter !== null && automaticStorageAdapters.get(automaticBackend) === adapter) {
277
+ automaticStorageAdapters.delete(automaticBackend);
278
+ }
279
+ throw withContext(error, { phase: "session-init" });
280
+ }
281
+ }
282
+ async function listPersistedTransfers(session) {
283
+ const state = getSessionState(session).clientPersistence;
284
+ const records = await readAllRecords(state, "read");
285
+ return records.map(toPublicRecord);
286
+ }
287
+ async function getPersistedTransfer(session, swapId) {
288
+ const state = getSessionState(session).clientPersistence;
289
+ assertSwapId(swapId, "read");
290
+ if (isReservedPersistenceKey(swapId)) return null;
291
+ const record = await readOptionalRecord(state, swapId, "read");
292
+ return record === null ? null : toPublicRecord(record);
293
+ }
294
+ async function removePersistedTransfer(session, swapId) {
295
+ const state = getSessionState(session).clientPersistence;
296
+ assertSwapId(swapId, "remove");
297
+ const adapter = requireAdapter(state, "remove");
298
+ const terminalWrite = beginTerminalWrite(state, swapId);
299
+ try {
300
+ await terminalWrite?.waitForNonTerminalWrites;
301
+ await withNamespaceWriteGate(
302
+ adapter,
303
+ CLIENT_PERSISTENCE_NAMESPACE,
304
+ swapId,
305
+ () => adapter.remove(CLIENT_PERSISTENCE_NAMESPACE, swapId),
306
+ { exclusive: true }
307
+ );
308
+ } catch (error) {
309
+ throw withContext(error, { phase: "remove" });
310
+ } finally {
311
+ terminalWrite?.release();
312
+ }
313
+ }
314
+ async function purgePersistedTransfers(session) {
315
+ const state = getSessionState(session).clientPersistence;
316
+ const adapter = requireAdapter(state, "purge");
317
+ try {
318
+ await withNamespaceWriteGate(
319
+ adapter,
320
+ CLIENT_PERSISTENCE_NAMESPACE,
321
+ CLIENT_PERSISTENCE_NAMESPACE_GATE_KEY,
322
+ async () => {
323
+ const keys = await adapter.list(CLIENT_PERSISTENCE_NAMESPACE);
324
+ for (const key of keys) {
325
+ await adapter.remove(CLIENT_PERSISTENCE_NAMESPACE, key);
326
+ }
327
+ },
328
+ { exclusive: true }
329
+ );
330
+ } catch (error) {
331
+ throw withContext(error, { phase: "purge" });
332
+ }
333
+ }
334
+ async function persistTransferAllocation(state, params) {
335
+ const record = {
336
+ schemaVersion: CLIENT_PERSISTENCE_SCHEMA_VERSION,
337
+ stage: "allocation",
338
+ swapId: params.swapId,
339
+ amountLamports: params.amountLamports,
340
+ destinationAddress: destinationFromPayoutSpec(params.payoutSpec),
341
+ policySnapshot: params.policySnapshot,
342
+ status: "awaiting_dkg",
343
+ updatedAtMs: Date.now(),
344
+ coordinatorTimestampMs: params.coordinatorTimestampMs,
345
+ syncSecret: new Uint8Array(params.syncSecret)
346
+ };
347
+ const nonTerminalWrite = beginNonTerminalWriteForState(state, params.swapId);
348
+ if (nonTerminalWrite === null) return;
349
+ try {
350
+ await writeRecord(state, record, "allocation", true, null, nonTerminalWrite);
351
+ } finally {
352
+ nonTerminalWrite();
353
+ }
354
+ }
355
+ async function persistTransferDkgMetadata(state, params) {
356
+ if (!state.enabled) return;
357
+ const adapter = requireAdapter(state, "allocation", params.swapId, true);
358
+ const nonTerminalWrite = beginNonTerminalWriteForState(state, params.swapId);
359
+ if (nonTerminalWrite === null) return;
360
+ try {
361
+ await withNamespaceWriteGate(adapter, CLIENT_PERSISTENCE_NAMESPACE, params.swapId, async () => {
362
+ const record = await readRequiredRecord(state, params.swapId, "allocation", true);
363
+ if (record.stage !== "allocation") return;
364
+ await writeRecord(
365
+ state,
366
+ {
367
+ ...record,
368
+ depositAddress: params.depositAddress,
369
+ depositExpiresAtMs: params.depositExpiresAtMs,
370
+ coordinatorTimestampMs: params.coordinatorTimestampMs,
371
+ updatedAtMs: Date.now()
372
+ },
373
+ "allocation",
374
+ true,
375
+ null,
376
+ nonTerminalWrite,
377
+ { namespaceGateHeld: true }
378
+ );
379
+ });
380
+ } finally {
381
+ nonTerminalWrite();
382
+ }
383
+ }
384
+ async function persistTransferRecoveryCode(state, swapId, recoveryCode) {
385
+ if (!state.enabled) return;
386
+ const adapter = requireAdapter(state, "recovery-code", swapId, true);
387
+ const nonTerminalWrite = beginNonTerminalWriteForState(state, swapId);
388
+ if (nonTerminalWrite === null) return;
389
+ try {
390
+ await withNamespaceWriteGate(adapter, CLIENT_PERSISTENCE_NAMESPACE, swapId, async () => {
391
+ const record = await readOptionalRecord(state, swapId, "recovery-code", true);
392
+ if (record === null) return;
393
+ await writeRecord(
394
+ state,
395
+ { ...record, recoveryCode: new Uint8Array(recoveryCode), updatedAtMs: Date.now() },
396
+ "recovery-code",
397
+ true,
398
+ null,
399
+ nonTerminalWrite,
400
+ { namespaceGateHeld: true }
401
+ );
402
+ });
403
+ } finally {
404
+ nonTerminalWrite();
405
+ }
406
+ }
407
+ async function readPersistedTransferForResume(state, swapId) {
408
+ return readOptionalRecord(state, swapId, "ready", false);
409
+ }
410
+ async function persistTransferReady(state, params) {
411
+ if (!state.enabled) return;
412
+ const adapter = requireAdapter(state, "ready", params.swapId, true);
413
+ const nonTerminalWrite = beginNonTerminalWriteForState(state, params.swapId);
414
+ if (nonTerminalWrite === null) return;
415
+ try {
416
+ await withNamespaceWriteGate(adapter, CLIENT_PERSISTENCE_NAMESPACE, params.swapId, async () => {
417
+ const record = await readRequiredRecord(state, params.swapId, "ready", true);
418
+ await writeRecord(
419
+ state,
420
+ {
421
+ ...record,
422
+ stage: "ready",
423
+ status: "awaiting_deposit",
424
+ depositAddress: params.depositAddress,
425
+ depositExpiresAtMs: params.depositExpiresAtMs,
426
+ delegatedAtMs: params.delegatedAtMs,
427
+ coordinatorTimestampMs: params.coordinatorTimestampMs,
428
+ updatedAtMs: Date.now()
429
+ },
430
+ "ready",
431
+ true,
432
+ null,
433
+ nonTerminalWrite,
434
+ { namespaceGateHeld: true }
435
+ );
436
+ });
437
+ } finally {
438
+ nonTerminalWrite();
439
+ }
440
+ }
441
+ async function persistTransferStatus(state, swapId, status, coordinatorTimestampMs) {
442
+ if (!state.enabled) return;
443
+ const isTerminal = CLIENT_SWAP_STATE_VALUES.has(status.state) && isTerminalState(status.state);
444
+ const terminalWrite = isTerminal ? beginTerminalWrite(state, swapId) : null;
445
+ const nonTerminalWrite = isTerminal ? null : beginNonTerminalWriteForState(state, swapId);
446
+ if (!isTerminal && nonTerminalWrite === null) return;
447
+ const adapter = requireAdapter(state, "sync", swapId, true);
448
+ try {
449
+ if (terminalWrite) await terminalWrite.waitForNonTerminalWrites;
450
+ await withNamespaceWriteGate(
451
+ adapter,
452
+ CLIENT_PERSISTENCE_NAMESPACE,
453
+ swapId,
454
+ async () => {
455
+ const record = await readOptionalRecord(state, swapId, "sync", true);
456
+ if (record === null || record.stage === "terminal-cleaned" && !isTerminal) return;
457
+ const next = isTerminal ? {
458
+ ...record,
459
+ stage: "terminal-cleaned",
460
+ status: status.state,
461
+ ...status.actor_sync === void 0 ? {} : { stateVersion: status.actor_sync.snapshot.state_version },
462
+ updatedAtMs: Date.now(),
463
+ coordinatorTimestampMs,
464
+ syncSecret: void 0,
465
+ recoveryCode: void 0
466
+ } : {
467
+ ...record,
468
+ status: status.state,
469
+ ...status.actor_sync === void 0 ? {} : { stateVersion: status.actor_sync.snapshot.state_version },
470
+ updatedAtMs: Date.now(),
471
+ coordinatorTimestampMs
472
+ };
473
+ await writeRecord(state, next, "sync", true, terminalWrite, nonTerminalWrite, {
474
+ namespaceGateHeld: true
475
+ });
476
+ },
477
+ { exclusive: isTerminal }
478
+ );
479
+ } finally {
480
+ terminalWrite?.release();
481
+ nonTerminalWrite?.();
482
+ }
483
+ }
484
+ async function persistTransferTerminal(state, swapId, status, coordinatorTimestampMs, phase = "refund") {
485
+ if (!state.enabled) return;
486
+ const terminalWrite = beginTerminalWrite(state, swapId);
487
+ const adapter = requireAdapter(state, phase, swapId, true);
488
+ try {
489
+ await terminalWrite?.waitForNonTerminalWrites;
490
+ await withNamespaceWriteGate(
491
+ adapter,
492
+ CLIENT_PERSISTENCE_NAMESPACE,
493
+ swapId,
494
+ async () => {
495
+ const record = await readOptionalRecord(state, swapId, phase, true);
496
+ if (record === null) return;
497
+ await writeRecord(
498
+ state,
499
+ {
500
+ ...record,
501
+ stage: "terminal-cleaned",
502
+ status,
503
+ updatedAtMs: Date.now(),
504
+ coordinatorTimestampMs,
505
+ syncSecret: void 0,
506
+ recoveryCode: void 0
507
+ },
508
+ phase,
509
+ true,
510
+ terminalWrite,
511
+ void 0,
512
+ { namespaceGateHeld: true }
513
+ );
514
+ },
515
+ { exclusive: true }
516
+ );
517
+ } finally {
518
+ terminalWrite?.release();
519
+ }
520
+ }
521
+ function automaticStorageAdapter() {
522
+ const global = globalThis;
523
+ const chromeStorage = global.chrome?.storage?.local;
524
+ const extensionContext = global.chrome?.runtime?.id !== void 0 || global.browser?.runtime?.id !== void 0;
525
+ if (isExtensionStorageArea(chromeStorage)) {
526
+ return automaticAdapterFor(chromeStorage, () => extensionStorage({ storage: chromeStorage }));
527
+ }
528
+ if (extensionContext) {
529
+ throw new StorageError(
530
+ "STORAGE_NOT_AVAILABLE",
531
+ "Chrome extension storage.local is unavailable",
532
+ { phase: "session-init" }
533
+ );
534
+ }
535
+ let localStorage;
536
+ try {
537
+ localStorage = globalThis.localStorage;
538
+ } catch {
539
+ return { backend: null, adapter: browserStorage() };
540
+ }
541
+ if (typeof localStorage === "object" && localStorage !== null) {
542
+ return automaticAdapterFor(
543
+ localStorage,
544
+ () => browserStorage({ storage: localStorage })
545
+ );
546
+ }
547
+ return { backend: null, adapter: browserStorage() };
548
+ }
549
+ function automaticAdapterFor(backend, create) {
550
+ const existing = automaticStorageAdapters.get(backend);
551
+ if (existing !== void 0) return { backend, adapter: existing };
552
+ const adapter = create();
553
+ automaticStorageAdapters.set(backend, adapter);
554
+ return { backend, adapter };
555
+ }
556
+ function isExtensionStorageArea(value) {
557
+ return typeof value === "object" && value !== null && isFunction(value.get) && isFunction(value.set) && isFunction(value.remove);
558
+ }
559
+ function validateStorageAdapter(value) {
560
+ if (typeof value !== "object" || value === null || !isStorageKind(value.kind) || !isFunction(value.put) || !isFunction(value.get) || !isFunction(value.list) || !isFunction(value.remove)) {
561
+ throw new StorageError(
562
+ "STORAGE_INVALID_ADAPTER",
563
+ "client persistence storage must implement the StorageAdapter contract",
564
+ { phase: "session-init" }
565
+ );
566
+ }
567
+ return value;
568
+ }
569
+ async function probeStorage(adapter) {
570
+ const probeKey = `${CLIENT_PERSISTENCE_PROBE_KEY_PREFIX}${Date.now()}-${clientPersistenceProbeSequence}`;
571
+ clientPersistenceProbeSequence += 1;
572
+ const probeBytes = new Uint8Array(CLIENT_PERSISTENCE_PROBE_BYTES);
573
+ try {
574
+ await withNamespaceWriteGate(
575
+ adapter,
576
+ CLIENT_PERSISTENCE_NAMESPACE,
577
+ probeKey,
578
+ async () => {
579
+ let probeMayExist = false;
580
+ try {
581
+ await adapter.list(CLIENT_PERSISTENCE_NAMESPACE);
582
+ probeMayExist = true;
583
+ await adapter.put(CLIENT_PERSISTENCE_NAMESPACE, probeKey, probeBytes);
584
+ const storedProbe = await adapter.get(CLIENT_PERSISTENCE_NAMESPACE, probeKey);
585
+ if (!(storedProbe instanceof Uint8Array) || storedProbe.byteLength !== probeBytes.byteLength || storedProbe.some((byte, index) => byte !== probeBytes[index])) {
586
+ throw new StorageError(
587
+ "STORAGE_NOT_AVAILABLE",
588
+ "client persistence storage probe readback failed",
589
+ { phase: "session-init" }
590
+ );
591
+ }
592
+ await adapter.remove(CLIENT_PERSISTENCE_NAMESPACE, probeKey);
593
+ const remainingProbe = await adapter.get(CLIENT_PERSISTENCE_NAMESPACE, probeKey);
594
+ if (remainingProbe !== null) {
595
+ throw new StorageError(
596
+ "STORAGE_NOT_AVAILABLE",
597
+ "client persistence storage probe removal readback failed",
598
+ { phase: "session-init" }
599
+ );
600
+ }
601
+ probeMayExist = false;
602
+ } catch (error) {
603
+ if (probeMayExist) {
604
+ try {
605
+ await adapter.remove(CLIENT_PERSISTENCE_NAMESPACE, probeKey);
606
+ } catch {
607
+ }
608
+ }
609
+ throw error;
610
+ }
611
+ },
612
+ { exclusive: true }
613
+ );
614
+ } catch (error) {
615
+ throw withContext(error, { phase: "session-init" });
616
+ }
617
+ }
618
+ async function writeRecord(state, record, phase, remoteAccepted, terminalWrite, nonTerminalWrite, options = {}) {
619
+ if (!state.enabled) return;
620
+ const adapter = requireAdapter(state, phase, record.swapId, remoteAccepted);
621
+ const terminalRecord = record.stage === "terminal-cleaned";
622
+ const ownedTerminalWrite = terminalRecord && terminalWrite === void 0 ? beginTerminalWrite(state, record.swapId) : null;
623
+ const activeTerminalWrite = terminalWrite ?? ownedTerminalWrite;
624
+ const releaseNonTerminalWrite = terminalRecord ? null : nonTerminalWrite ?? beginNonTerminalWrite(adapter, record.swapId);
625
+ if (!terminalRecord && releaseNonTerminalWrite === null) return;
626
+ try {
627
+ if (activeTerminalWrite) await activeTerminalWrite.waitForNonTerminalWrites;
628
+ const bytes = serializeRecord(record);
629
+ const write = async () => {
630
+ const keys = (await adapter.list(CLIENT_PERSISTENCE_NAMESPACE)).filter(
631
+ (key) => !isReservedPersistenceKey(key)
632
+ );
633
+ const values = /* @__PURE__ */ new Map();
634
+ let namespaceBytes = 0;
635
+ for (const key of keys) {
636
+ const value = await adapter.get(CLIENT_PERSISTENCE_NAMESPACE, key);
637
+ if (value !== null) {
638
+ values.set(key, value);
639
+ namespaceBytes += value.byteLength;
640
+ }
641
+ }
642
+ const previous = values.get(record.swapId);
643
+ const nextCount = previous === void 0 ? keys.length + 1 : keys.length;
644
+ if (previous === void 0 && nextCount > CLIENT_PERSISTENCE_MAX_RECORD_COUNT) {
645
+ throw namespaceLimitError("namespace record limit exceeded");
646
+ }
647
+ const nextBytes = namespaceBytes - (previous?.byteLength ?? 0) + bytes.byteLength;
648
+ if (nextBytes > CLIENT_PERSISTENCE_MAX_NAMESPACE_BYTES && (previous === void 0 || bytes.byteLength > previous.byteLength)) {
649
+ throw namespaceLimitError("namespace byte limit exceeded");
650
+ }
651
+ await adapter.put(CLIENT_PERSISTENCE_NAMESPACE, record.swapId, bytes);
652
+ };
653
+ if (options.namespaceGateHeld) {
654
+ await write();
655
+ } else {
656
+ await withNamespaceWriteGate(adapter, CLIENT_PERSISTENCE_NAMESPACE, record.swapId, write);
657
+ }
658
+ } catch (error) {
659
+ throw withContext(error, { phase, swapId: record.swapId, remoteAccepted });
660
+ } finally {
661
+ releaseNonTerminalWrite?.();
662
+ ownedTerminalWrite?.release();
663
+ }
664
+ }
665
+ function beginNonTerminalWriteForState(state, swapId) {
666
+ if (!state.enabled || state.adapter === null) return null;
667
+ return beginNonTerminalWrite(state.adapter, swapId);
668
+ }
669
+ async function withNamespaceWriteGate(adapter, namespace, key, write, options = {}) {
670
+ const release = await acquireNamespaceWriteGate(adapter, namespace, key, options);
671
+ try {
672
+ await write();
673
+ } finally {
674
+ release();
675
+ }
676
+ }
677
+ async function acquireNamespaceWriteGate(adapter, namespace, key, options = {}) {
678
+ const gate = getNamespaceWriteGate(adapter, namespace);
679
+ const exclusive = options.exclusive === true;
680
+ let granted = false;
681
+ const shouldWait = () => {
682
+ if (gate.activeCount === 0) return gate.waiters.length !== 0;
683
+ if (exclusive || gate.exclusive) return true;
684
+ return gate.activeKey !== key || gate.waiters.length !== 0;
685
+ };
686
+ while (!granted && shouldWait()) {
687
+ await new Promise(
688
+ (resolve) => gate.waiters.push({
689
+ key,
690
+ resolve: () => {
691
+ granted = true;
692
+ resolve();
693
+ }
694
+ })
695
+ );
696
+ }
697
+ gate.activeKey = key;
698
+ gate.activeCount += 1;
699
+ gate.exclusive = exclusive;
700
+ let released = false;
701
+ return () => {
702
+ if (released) return;
703
+ released = true;
704
+ gate.activeCount -= 1;
705
+ if (gate.activeCount !== 0) return;
706
+ gate.activeKey = null;
707
+ gate.exclusive = false;
708
+ const next = gate.waiters.shift();
709
+ if (next) {
710
+ next.resolve();
711
+ return;
712
+ }
713
+ cleanupNamespaceWriteGate(adapter, namespace, gate);
714
+ };
715
+ }
716
+ function getNamespaceWriteGate(adapter, namespace) {
717
+ const backing = getStorageBacking(adapter);
718
+ let gates = namespaceWriteGates.get(backing);
719
+ if (!gates) {
720
+ gates = /* @__PURE__ */ new Map();
721
+ namespaceWriteGates.set(backing, gates);
722
+ }
723
+ let gate = gates.get(namespace);
724
+ if (!gate) {
725
+ gate = { activeKey: null, activeCount: 0, exclusive: false, waiters: [] };
726
+ gates.set(namespace, gate);
727
+ }
728
+ return gate;
729
+ }
730
+ function cleanupNamespaceWriteGate(adapter, namespace, gate) {
731
+ if (gate.activeKey !== null || gate.waiters.length !== 0) return;
732
+ namespaceWriteGates.get(getStorageBacking(adapter))?.delete(namespace);
733
+ }
734
+ function beginTerminalWrite(state, swapId) {
735
+ if (!state.enabled || state.adapter === null) return null;
736
+ const barrier = getTerminalWriteBarrier(state.adapter, swapId);
737
+ barrier.activeTerminalWrites += 1;
738
+ barrier.terminalRequested = true;
739
+ const waitForNonTerminalWrites = barrier.activeNonTerminalWrites === 0 ? Promise.resolve() : new Promise((resolve) => barrier.nonTerminalWaiters.push(resolve));
740
+ let released = false;
741
+ return {
742
+ waitForNonTerminalWrites,
743
+ release() {
744
+ if (released) return;
745
+ released = true;
746
+ barrier.activeTerminalWrites -= 1;
747
+ if (barrier.activeTerminalWrites === 0) barrier.terminalRequested = false;
748
+ cleanupTerminalWriteBarrier(state.adapter, swapId, barrier);
749
+ }
750
+ };
751
+ }
752
+ function beginNonTerminalWrite(adapter, swapId) {
753
+ const barrier = getTerminalWriteBarrier(adapter, swapId);
754
+ if (barrier.terminalRequested) return null;
755
+ barrier.activeNonTerminalWrites += 1;
756
+ let released = false;
757
+ return () => {
758
+ if (released) return;
759
+ released = true;
760
+ barrier.activeNonTerminalWrites -= 1;
761
+ if (barrier.activeNonTerminalWrites === 0) {
762
+ const waiters = barrier.nonTerminalWaiters.splice(0);
763
+ for (const resolve of waiters) resolve();
764
+ }
765
+ cleanupTerminalWriteBarrier(adapter, swapId, barrier);
766
+ };
767
+ }
768
+ function getTerminalWriteBarrier(adapter, swapId) {
769
+ const backing = getStorageBacking(adapter);
770
+ let barriers = terminalWriteBarriers.get(backing);
771
+ if (!barriers) {
772
+ barriers = /* @__PURE__ */ new Map();
773
+ terminalWriteBarriers.set(backing, barriers);
774
+ }
775
+ let barrier = barriers.get(swapId);
776
+ if (!barrier) {
777
+ barrier = {
778
+ activeNonTerminalWrites: 0,
779
+ activeTerminalWrites: 0,
780
+ terminalRequested: false,
781
+ nonTerminalWaiters: []
782
+ };
783
+ barriers.set(swapId, barrier);
784
+ }
785
+ return barrier;
786
+ }
787
+ function cleanupTerminalWriteBarrier(adapter, swapId, barrier) {
788
+ if (barrier.activeNonTerminalWrites !== 0 || barrier.activeTerminalWrites !== 0 || barrier.terminalRequested || barrier.nonTerminalWaiters.length !== 0) {
789
+ return;
790
+ }
791
+ terminalWriteBarriers.get(getStorageBacking(adapter))?.delete(swapId);
792
+ }
793
+ async function readAllRecords(state, phase) {
794
+ const adapter = requireAdapter(state, phase);
795
+ try {
796
+ const keys = (await adapter.list(CLIENT_PERSISTENCE_NAMESPACE)).filter(
797
+ (key) => !isReservedPersistenceKey(key)
798
+ );
799
+ if (keys.length > CLIENT_PERSISTENCE_MAX_RECORD_COUNT) {
800
+ throw namespaceLimitError("namespace contains too many records");
801
+ }
802
+ const records = [];
803
+ let namespaceBytes = 0;
804
+ for (const key of keys) {
805
+ const value = await adapter.get(CLIENT_PERSISTENCE_NAMESPACE, key);
806
+ if (value === null) {
807
+ throw new StorageError(
808
+ "STORAGE_KEY_MISSING",
809
+ "persisted transfer key disappeared during read"
810
+ );
811
+ }
812
+ namespaceBytes += value.byteLength;
813
+ if (namespaceBytes > CLIENT_PERSISTENCE_MAX_NAMESPACE_BYTES) {
814
+ throw namespaceLimitError("namespace byte limit exceeded");
815
+ }
816
+ records.push(parseRecord(value, key));
817
+ }
818
+ return records;
819
+ } catch (error) {
820
+ throw withContext(error, { phase });
821
+ }
822
+ }
823
+ async function readOptionalRecord(state, swapId, phase, remoteAccepted = false) {
824
+ if (!state.enabled) return null;
825
+ assertSwapId(swapId, phase);
826
+ const value = await readValue(state, swapId, phase, remoteAccepted);
827
+ if (value === null) return null;
828
+ try {
829
+ return parseRecord(value, swapId);
830
+ } catch (error) {
831
+ throw withContext(error, { phase, swapId, remoteAccepted });
832
+ }
833
+ }
834
+ async function readRequiredRecord(state, swapId, phase, remoteAccepted = true) {
835
+ const record = await readOptionalRecord(state, swapId, phase, remoteAccepted);
836
+ if (record === null) {
837
+ throw new StorageError("STORAGE_KEY_MISSING", `persisted transfer ${swapId} is missing`, {
838
+ phase,
839
+ swapId,
840
+ remoteAccepted
841
+ });
842
+ }
843
+ return record;
844
+ }
845
+ async function readValue(state, swapId, phase, remoteAccepted = false) {
846
+ const adapter = requireAdapter(state, phase, swapId, remoteAccepted);
847
+ try {
848
+ return await adapter.get(CLIENT_PERSISTENCE_NAMESPACE, swapId);
849
+ } catch (error) {
850
+ throw withContext(error, { phase, swapId, remoteAccepted });
851
+ }
852
+ }
853
+ function serializeRecord(record) {
854
+ assertRecord(record);
855
+ const policySnapshot = normalizePolicySnapshot(record.policySnapshot);
856
+ const serialized = {
857
+ schemaVersion: record.schemaVersion,
858
+ stage: record.stage,
859
+ swapId: record.swapId,
860
+ amountLamports: record.amountLamports,
861
+ destinationAddress: record.destinationAddress,
862
+ policySnapshot,
863
+ status: record.status,
864
+ ...record.stateVersion !== void 0 && { stateVersion: record.stateVersion },
865
+ updatedAtMs: record.updatedAtMs,
866
+ ...record.coordinatorTimestampMs !== void 0 && {
867
+ coordinatorTimestampMs: record.coordinatorTimestampMs
868
+ },
869
+ ...record.depositAddress !== void 0 && { depositAddress: record.depositAddress },
870
+ ...record.depositExpiresAtMs !== void 0 && {
871
+ depositExpiresAtMs: record.depositExpiresAtMs
872
+ },
873
+ ...record.delegatedAtMs !== void 0 && { delegatedAtMs: record.delegatedAtMs },
874
+ syncSecretHex: record.syncSecret === void 0 ? void 0 : bytesToHex(record.syncSecret),
875
+ recoveryCodeHex: record.recoveryCode === void 0 ? void 0 : bytesToHex(record.recoveryCode)
876
+ };
877
+ const json = JSON.stringify(serialized);
878
+ const bytes = new TextEncoder().encode(json);
879
+ if (bytes.byteLength > CLIENT_PERSISTENCE_MAX_RECORD_BYTES) {
880
+ throw namespaceLimitError("persisted transfer record exceeds the byte limit");
881
+ }
882
+ return bytes;
883
+ }
884
+ function parseRecord(bytes, expectedSwapId) {
885
+ if (bytes.byteLength > CLIENT_PERSISTENCE_MAX_RECORD_BYTES) {
886
+ throw new StorageError(
887
+ "STORAGE_QUOTA_EXCEEDED",
888
+ "persisted transfer record exceeds the byte limit"
889
+ );
890
+ }
891
+ let value;
892
+ try {
893
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
894
+ } catch (cause) {
895
+ throw new StorageError(
896
+ "STORAGE_DECRYPT_FAILED",
897
+ "persisted transfer record is not valid JSON",
898
+ {
899
+ cause
900
+ }
901
+ );
902
+ }
903
+ if (!isRecord(value)) {
904
+ throw corruptRecordError("persisted transfer record is not an object");
905
+ }
906
+ const keys = Object.keys(value);
907
+ if (keys.some((key) => !SERIALIZED_RECORD_KEYS.has(key))) {
908
+ throw corruptRecordError("persisted transfer record contains an unknown field");
909
+ }
910
+ if (value.schemaVersion !== CLIENT_PERSISTENCE_SCHEMA_VERSION) {
911
+ throw corruptRecordError("persisted transfer record has an unsupported schema version");
912
+ }
913
+ if (value.swapId !== expectedSwapId) {
914
+ throw corruptRecordError("persisted transfer key does not match its swap id");
915
+ }
916
+ const record = value;
917
+ if (!isPersistedStage(record.stage) || typeof record.swapId !== "string" || typeof record.amountLamports !== "number" || !Number.isSafeInteger(record.amountLamports) || record.amountLamports < 0 || typeof record.destinationAddress !== "string" || typeof record.status !== "string" || !CLIENT_SWAP_STATE_VALUES.has(record.status) || typeof record.updatedAtMs !== "number" || !Number.isSafeInteger(record.updatedAtMs) || record.updatedAtMs < 0 || !isRecord(record.policySnapshot)) {
918
+ throw corruptRecordError("persisted transfer record has invalid required fields");
919
+ }
920
+ assertBoundedString(record.swapId, MAX_IDENTIFIER_LENGTH, "swap id");
921
+ assertBoundedString(record.destinationAddress, MAX_ADDRESS_LENGTH, "destination address");
922
+ if (record.syncSecretHex !== void 0) {
923
+ if (typeof record.syncSecretHex !== "string") {
924
+ throw corruptRecordError("persisted transfer sync secret is invalid");
925
+ }
926
+ assertHex(record.syncSecretHex, "sync secret");
927
+ }
928
+ if (record.recoveryCodeHex !== void 0) {
929
+ if (typeof record.recoveryCodeHex !== "string") {
930
+ throw corruptRecordError("persisted transfer recovery code is invalid");
931
+ }
932
+ assertHex(record.recoveryCodeHex, "recovery code");
933
+ }
934
+ assertOptionalTimestamp(record.stateVersion, "state version");
935
+ assertOptionalTimestamp(record.coordinatorTimestampMs, "coordinator timestamp");
936
+ assertOptionalTimestamp(record.depositExpiresAtMs, "deposit expiry");
937
+ assertOptionalTimestamp(record.delegatedAtMs, "delegated timestamp");
938
+ if (record.depositAddress !== void 0) {
939
+ if (typeof record.depositAddress !== "string") {
940
+ throw corruptRecordError("persisted transfer deposit address is invalid");
941
+ }
942
+ assertBoundedString(record.depositAddress, MAX_ADDRESS_LENGTH, "deposit address");
943
+ }
944
+ if (record.stage === "ready") {
945
+ if (record.depositAddress === void 0 || record.depositExpiresAtMs === void 0 || record.delegatedAtMs === void 0) {
946
+ throw corruptRecordError("ready persisted transfer is missing deposit metadata");
947
+ }
948
+ }
949
+ if (record.stage === "terminal-cleaned") {
950
+ if (record.syncSecretHex !== void 0 || record.recoveryCodeHex !== void 0) {
951
+ throw corruptRecordError("terminal persisted transfer contains bearer material");
952
+ }
953
+ if (!isTerminalState(record.status)) {
954
+ throw corruptRecordError("terminal persisted transfer has a non-terminal status");
955
+ }
956
+ } else if (record.syncSecretHex === void 0 || isTerminalState(record.status)) {
957
+ throw corruptRecordError("non-terminal persisted transfer has invalid bearer state");
958
+ }
959
+ const policySnapshot = normalizePolicySnapshot(record.policySnapshot);
960
+ if (record.amountLamports !== policySnapshot.entry_amount_lamports || record.destinationAddress !== policySnapshot.payout_schedule[0]?.destination_address) {
961
+ throw corruptRecordError("persisted transfer metadata does not match its policy snapshot");
962
+ }
963
+ return {
964
+ schemaVersion: CLIENT_PERSISTENCE_SCHEMA_VERSION,
965
+ stage: record.stage,
966
+ swapId: record.swapId,
967
+ amountLamports: record.amountLamports,
968
+ destinationAddress: record.destinationAddress,
969
+ policySnapshot,
970
+ status: record.status,
971
+ ...record.stateVersion !== void 0 && { stateVersion: record.stateVersion },
972
+ updatedAtMs: record.updatedAtMs,
973
+ ...record.coordinatorTimestampMs !== void 0 && {
974
+ coordinatorTimestampMs: record.coordinatorTimestampMs
975
+ },
976
+ ...record.depositAddress !== void 0 && { depositAddress: record.depositAddress },
977
+ ...record.depositExpiresAtMs !== void 0 && {
978
+ depositExpiresAtMs: record.depositExpiresAtMs
979
+ },
980
+ ...record.delegatedAtMs !== void 0 && { delegatedAtMs: record.delegatedAtMs },
981
+ ...record.syncSecretHex !== void 0 && { syncSecret: hexToBytes(record.syncSecretHex) },
982
+ ...record.recoveryCodeHex !== void 0 && {
983
+ recoveryCode: hexToBytes(record.recoveryCodeHex)
984
+ }
985
+ };
986
+ }
987
+ function assertRecord(record) {
988
+ assertSwapId(record.swapId, "write");
989
+ if (record.schemaVersion !== CLIENT_PERSISTENCE_SCHEMA_VERSION) {
990
+ throw corruptRecordError("persisted transfer has an unsupported schema version");
991
+ }
992
+ if (!PERSISTED_STAGE_VALUES.has(record.stage)) {
993
+ throw corruptRecordError("persisted transfer has an invalid stage");
994
+ }
995
+ if (!Number.isSafeInteger(record.amountLamports) || record.amountLamports < 0) {
996
+ throw corruptRecordError("persisted transfer has an invalid amount");
997
+ }
998
+ assertBoundedString(record.destinationAddress, MAX_ADDRESS_LENGTH, "destination address");
999
+ if (!CLIENT_SWAP_STATE_VALUES.has(record.status)) {
1000
+ throw corruptRecordError("persisted transfer has an invalid status");
1001
+ }
1002
+ if (!Number.isSafeInteger(record.updatedAtMs) || record.updatedAtMs < 0) {
1003
+ throw corruptRecordError("persisted transfer has an invalid update timestamp");
1004
+ }
1005
+ assertOptionalTimestamp(record.stateVersion, "state version");
1006
+ if (record.syncSecret !== void 0 && (!(record.syncSecret instanceof Uint8Array) || record.syncSecret.length !== HEX_BYTE_LENGTH)) {
1007
+ throw corruptRecordError("persisted transfer sync secret has an invalid length");
1008
+ }
1009
+ if (record.recoveryCode !== void 0 && (!(record.recoveryCode instanceof Uint8Array) || record.recoveryCode.length !== HEX_BYTE_LENGTH)) {
1010
+ throw corruptRecordError("persisted transfer recovery code has an invalid length");
1011
+ }
1012
+ const policySnapshot = normalizePolicySnapshot(record.policySnapshot);
1013
+ if (record.amountLamports !== policySnapshot.entry_amount_lamports || record.destinationAddress !== policySnapshot.payout_schedule[0]?.destination_address) {
1014
+ throw corruptRecordError("persisted transfer metadata does not match its policy snapshot");
1015
+ }
1016
+ assertOptionalTimestamp(record.coordinatorTimestampMs, "coordinator timestamp");
1017
+ assertOptionalTimestamp(record.depositExpiresAtMs, "deposit expiry");
1018
+ assertOptionalTimestamp(record.delegatedAtMs, "delegated timestamp");
1019
+ if (record.depositAddress !== void 0) {
1020
+ assertBoundedString(record.depositAddress, MAX_ADDRESS_LENGTH, "deposit address");
1021
+ }
1022
+ if (record.stage === "ready") {
1023
+ if (record.depositAddress === void 0 || record.depositExpiresAtMs === void 0 || record.delegatedAtMs === void 0) {
1024
+ throw corruptRecordError("ready persisted transfer is missing deposit metadata");
1025
+ }
1026
+ }
1027
+ if (record.stage === "terminal-cleaned") {
1028
+ if (record.syncSecret !== void 0 || record.recoveryCode !== void 0) {
1029
+ throw corruptRecordError("terminal persisted transfer contains bearer material");
1030
+ }
1031
+ if (!isTerminalState(record.status)) {
1032
+ throw corruptRecordError("terminal persisted transfer has a non-terminal status");
1033
+ }
1034
+ } else if (record.syncSecret === void 0 || isTerminalState(record.status)) {
1035
+ throw corruptRecordError("non-terminal persisted transfer has invalid bearer state");
1036
+ }
1037
+ const policyBytes = new TextEncoder().encode(JSON.stringify(policySnapshot));
1038
+ if (policyBytes.byteLength > MAX_POLICY_JSON_BYTES) {
1039
+ throw namespaceLimitError("persisted transfer policy snapshot exceeds the byte limit");
1040
+ }
1041
+ }
1042
+ function toPublicRecord(record) {
1043
+ const hasDurableDelegation = record.stage !== "allocation" && record.depositAddress !== void 0 && record.depositExpiresAtMs !== void 0 && record.delegatedAtMs !== void 0;
1044
+ return {
1045
+ schemaVersion: record.schemaVersion,
1046
+ stage: record.stage,
1047
+ swapId: record.swapId,
1048
+ amountLamports: record.amountLamports,
1049
+ destinationAddress: record.destinationAddress,
1050
+ policySnapshot: record.policySnapshot,
1051
+ status: record.status,
1052
+ ...record.stateVersion !== void 0 && { stateVersion: record.stateVersion },
1053
+ updatedAtMs: record.updatedAtMs,
1054
+ ...record.coordinatorTimestampMs !== void 0 && {
1055
+ coordinatorTimestampMs: record.coordinatorTimestampMs
1056
+ },
1057
+ ...hasDurableDelegation && { depositAddress: record.depositAddress },
1058
+ ...hasDurableDelegation && { depositExpiresAtMs: record.depositExpiresAtMs },
1059
+ ...hasDurableDelegation && { delegatedAtMs: record.delegatedAtMs },
1060
+ ...record.syncSecret !== void 0 && { syncSecret: new Uint8Array(record.syncSecret) },
1061
+ ...record.recoveryCode !== void 0 && {
1062
+ recoveryCode: new Uint8Array(record.recoveryCode)
1063
+ }
1064
+ };
1065
+ }
1066
+ function destinationFromPayoutSpec(payoutSpec) {
1067
+ return payoutSpec.mode === "instant" ? payoutSpec.destination_address : payoutSpec.destination_addresses[0] ?? "";
1068
+ }
1069
+ function requireAdapter(state, phase, swapId, remoteAccepted) {
1070
+ if (!state.enabled || state.adapter === null) {
1071
+ throw new StorageError("STORAGE_NOT_AVAILABLE", "client persistence is disabled", {
1072
+ phase,
1073
+ ...swapId === void 0 ? {} : { swapId },
1074
+ ...remoteAccepted === void 0 ? {} : { remoteAccepted }
1075
+ });
1076
+ }
1077
+ return state.adapter;
1078
+ }
1079
+ function withContext(error, context) {
1080
+ const code = error instanceof StorageError ? error.code === "STORAGE_NOT_AVAILABLE" && isQuotaError(error) ? "STORAGE_QUOTA_EXCEEDED" : error.code : isQuotaError(error) ? "STORAGE_QUOTA_EXCEEDED" : "STORAGE_NOT_AVAILABLE";
1081
+ const message = error instanceof Error ? error.message : "storage operation failed";
1082
+ return new StorageError(code, message, {
1083
+ cause: error,
1084
+ swapId: context.swapId ?? (error instanceof StorageError ? error.swapId : void 0),
1085
+ phase: context.phase,
1086
+ remoteAccepted: context.remoteAccepted ?? (error instanceof StorageError ? error.remoteAccepted : void 0)
1087
+ });
1088
+ }
1089
+ function namespaceLimitError(message) {
1090
+ return new StorageError("STORAGE_QUOTA_EXCEEDED", message);
1091
+ }
1092
+ function corruptRecordError(message) {
1093
+ return new StorageError("STORAGE_DECRYPT_FAILED", message);
1094
+ }
1095
+ function assertSwapId(swapId, phase) {
1096
+ if (typeof swapId !== "string" || swapId.length === 0 || swapId.length > MAX_IDENTIFIER_LENGTH) {
1097
+ throw new StorageError("STORAGE_KEY_MISSING", "swapId is invalid", { phase });
1098
+ }
1099
+ }
1100
+ function isReservedPersistenceKey(key) {
1101
+ return key.startsWith(CLIENT_PERSISTENCE_PROBE_KEY_PREFIX);
1102
+ }
1103
+ function assertBoundedString(value, maxLength, label) {
1104
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
1105
+ throw corruptRecordError(`${label} is outside the supported length`);
1106
+ }
1107
+ }
1108
+ function assertHex(value, label) {
1109
+ if (value.length !== HEX_LENGTH || !HEX_PATTERN.test(value)) {
1110
+ throw corruptRecordError(`${label} is not a 32-byte hexadecimal value`);
1111
+ }
1112
+ }
1113
+ function assertOptionalTimestamp(value, label) {
1114
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) {
1115
+ throw corruptRecordError(`${label} is invalid`);
1116
+ }
1117
+ }
1118
+ function normalizePolicySnapshot(value) {
1119
+ if (!isRecord(value)) throw corruptRecordError("persisted transfer policy snapshot is invalid");
1120
+ if (Object.keys(value).some((key) => !POLICY_SNAPSHOT_KEYS.has(key))) {
1121
+ throw corruptRecordError("persisted transfer policy snapshot contains an unknown field");
1122
+ }
1123
+ if (value.asset !== "SOL") throw corruptRecordError("persisted transfer policy asset is invalid");
1124
+ const integerFields = [
1125
+ "entry_amount_lamports",
1126
+ "total_committed_lamports",
1127
+ "payout_deadline_ms",
1128
+ "fragment_count",
1129
+ "min_fee_bps",
1130
+ "fee_bps",
1131
+ "fee_lamports",
1132
+ "net_payout_lamports"
1133
+ ];
1134
+ for (const field of integerFields) assertNonNegativeInteger(value[field], field);
1135
+ if (value.payout_window_ms !== void 0) {
1136
+ assertNonNegativeInteger(value.payout_window_ms, "payout_window_ms");
1137
+ }
1138
+ if (value.payout_mode !== "instant" && value.payout_mode !== "scheduled") {
1139
+ throw corruptRecordError("persisted transfer payout mode is invalid");
1140
+ }
1141
+ if (value.matching_mode !== "exact_1_to_1") {
1142
+ throw corruptRecordError("persisted transfer matching mode is invalid");
1143
+ }
1144
+ if (value.fragmentation_allowed !== "payout_side_only") {
1145
+ throw corruptRecordError("persisted transfer fragmentation mode is invalid");
1146
+ }
1147
+ if (!Array.isArray(value.payout_schedule) || value.payout_schedule.length === 0) {
1148
+ throw corruptRecordError("persisted transfer payout schedule is invalid");
1149
+ }
1150
+ if (value.payout_schedule.length > MAX_POLICY_DESTINATION_COUNT) {
1151
+ throw corruptRecordError("persisted transfer payout schedule is too large");
1152
+ }
1153
+ const payoutSchedule = Array.from(value.payout_schedule, (entry) => {
1154
+ if (!isRecord(entry) || Object.keys(entry).some((key) => !POLICY_DESTINATION_KEYS.has(key))) {
1155
+ throw corruptRecordError("persisted transfer payout schedule entry is invalid");
1156
+ }
1157
+ if (typeof entry.destination_address !== "string") {
1158
+ throw corruptRecordError("persisted transfer payout destination is invalid");
1159
+ }
1160
+ assertBoundedString(entry.destination_address, MAX_ADDRESS_LENGTH, "payout destination");
1161
+ return { destination_address: entry.destination_address };
1162
+ });
1163
+ return {
1164
+ asset: "SOL",
1165
+ entry_amount_lamports: value.entry_amount_lamports,
1166
+ total_committed_lamports: value.total_committed_lamports,
1167
+ payout_deadline_ms: value.payout_deadline_ms,
1168
+ ...value.payout_window_ms !== void 0 && {
1169
+ payout_window_ms: value.payout_window_ms
1170
+ },
1171
+ payout_schedule: payoutSchedule,
1172
+ fragment_count: value.fragment_count,
1173
+ payout_mode: value.payout_mode,
1174
+ matching_mode: "exact_1_to_1",
1175
+ fragmentation_allowed: "payout_side_only",
1176
+ min_fee_bps: value.min_fee_bps,
1177
+ fee_bps: value.fee_bps,
1178
+ fee_lamports: value.fee_lamports,
1179
+ net_payout_lamports: value.net_payout_lamports
1180
+ };
1181
+ }
1182
+ function assertNonNegativeInteger(value, label) {
1183
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
1184
+ throw corruptRecordError(`persisted transfer policy ${label} is invalid`);
1185
+ }
1186
+ }
1187
+ function isQuotaError(error) {
1188
+ if (error instanceof StorageError) {
1189
+ return isQuotaError(error.cause);
1190
+ }
1191
+ if (error instanceof Error && /quota/i.test(error.name)) return true;
1192
+ if (error instanceof Error && /quota/i.test(error.message)) return true;
1193
+ if (isRecord(error)) {
1194
+ const name = typeof error.name === "string" ? error.name : "";
1195
+ const message = typeof error.message === "string" ? error.message : "";
1196
+ return /quota/i.test(name) || /quota/i.test(message);
1197
+ }
1198
+ return false;
1199
+ }
1200
+ function isPersistedStage(value) {
1201
+ return typeof value === "string" && PERSISTED_STAGE_VALUES.has(value);
1202
+ }
1203
+ function isStorageKind(value) {
1204
+ return value === "browser" || value === "extension" || value === "server-hkdf" || value === "server-custom" || value === "memory";
1205
+ }
1206
+ function isFunction(value) {
1207
+ return typeof value === "function";
1208
+ }
1209
+ function isRecord(value) {
1210
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1211
+ }
1212
+ function isTerminalState(state) {
1213
+ return state === "completed" || state === "refunded" || state === "failed";
1214
+ }
1215
+ function bytesToHex(bytes) {
1216
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1217
+ }
1218
+ function hexToBytes(value) {
1219
+ const bytes = new Uint8Array(value.length / 2);
1220
+ for (let index = 0; index < bytes.length; index += 1) {
1221
+ bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
1222
+ }
1223
+ return bytes;
1224
+ }
1225
+ var DEFAULT_AZURE_JWKS_TIMEOUT_MS = 3e4;
1226
+ var TPM_GENERATED_VALUE = 4283712327;
1227
+ var TPM_ST_ATTEST_QUOTE = 32792;
1228
+ function assertAttestResponse(value) {
1229
+ if (typeof value !== "object" || value === null) {
1230
+ throw err("QUOTE_DECODE", "attestation response is not a JSON object");
1231
+ }
1232
+ const v = value;
1233
+ const fields = ["quote", "nonce", "pubkey", "binary_hash", "mode"];
1234
+ for (const field of fields) {
1235
+ if (typeof v[field] !== "string") {
1236
+ throw err(
1237
+ "QUOTE_DECODE",
1238
+ `attestation response missing or non-string field: "${field}" (got ${typeof v[field]})`
1239
+ );
1240
+ }
1241
+ }
1242
+ if (v["mode"] !== "dev" && v["mode"] !== "prod") {
1243
+ throw err("QUOTE_DECODE", `attestation response has unknown mode: "${v["mode"]}"`);
1244
+ }
1245
+ if (v["runtime_profile"] !== void 0 && v["runtime_profile"] !== "prod-hardened" && v["runtime_profile"] !== "prod-devnet-simplified" && v["runtime_profile"] !== "prod-devnet-simplified-locked" && v["runtime_profile"] !== "azure-tdx-v0-agentless") {
1246
+ throw err(
1247
+ "QUOTE_DECODE",
1248
+ `attestation response has unknown runtime profile: "${v["runtime_profile"]}"`
1249
+ );
1250
+ }
1251
+ if (v["local_attestation"] !== void 0 && typeof v["local_attestation"] !== "boolean") {
1252
+ throw err(
1253
+ "QUOTE_DECODE",
1254
+ `attestation response has non-boolean local_attestation: ${typeof v["local_attestation"]}`
1255
+ );
1256
+ }
1257
+ }
1258
+ var INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256 = "44a0196b2b99f889b8e149e95b807a350e7424964399e885a7cbb8ccfab674d3";
1259
+ var REASON_TO_CODE = {
1260
+ // Structural / chain / quote-signature failures -> the cert-chain bucket.
1261
+ QUOTE_DECODE: "CERT_CHAIN_INVALID",
1262
+ QUOTE_STRUCTURE: "CERT_CHAIN_INVALID",
1263
+ UNSUPPORTED_PLATFORM: "CERT_CHAIN_INVALID",
1264
+ PCK_CHAIN_MISSING: "CERT_CHAIN_INVALID",
1265
+ PCK_CHAIN_PARSE: "CERT_CHAIN_INVALID",
1266
+ ROOT_FINGERPRINT_MISMATCH: "CERT_CHAIN_INVALID",
1267
+ PCK_CHAIN_SIGNATURE: "CERT_CHAIN_INVALID",
1268
+ DCAP_COLLATERAL_SIGNATURE: "CERT_CHAIN_INVALID",
1269
+ DCAP_COLLATERAL_MISSING: "TCB_REJECTED",
1270
+ DCAP_COLLATERAL_PARSE: "TCB_REJECTED",
1271
+ DCAP_COLLATERAL_REVOKED: "TCB_REJECTED",
1272
+ TCB_STATUS_REJECTED: "TCB_REJECTED",
1273
+ QE_IDENTITY_REJECTED: "TCB_REJECTED",
1274
+ QUOTE_SIGNATURE: "CERT_CHAIN_INVALID",
1275
+ QE_REPORT_BINDING: "CERT_CHAIN_INVALID",
1276
+ QE_REPORT_SIGNATURE: "CERT_CHAIN_INVALID",
1277
+ // Freshness.
1278
+ NONCE_MISMATCH: "FRESHNESS_NONCE_MISMATCH",
1279
+ NONCE_FIELD_MISMATCH: "FRESHNESS_NONCE_MISMATCH",
1280
+ TPM_NONCE_MISMATCH: "AZURE_TPM_QUALIFYING_DATA_MISMATCH",
1281
+ // Noise channel binding (pubkey / report-data binding / bound JSON fields).
1282
+ CHANNEL_BINDING_MISMATCH: "NOISE_BINDING_MISMATCH",
1283
+ PUBKEY_FIELD_MISMATCH: "NOISE_BINDING_MISMATCH",
1284
+ BINARY_HASH_FORMAT: "NOISE_BINDING_MISMATCH",
1285
+ BINARY_HASH_MISMATCH: "NOISE_BINDING_MISMATCH",
1286
+ // Debug bit.
1287
+ DEBUG_BIT_SET: "DEBUG_BIT_SET",
1288
+ MAA_DEBUG_BIT_SET: "DEBUG_BIT_SET",
1289
+ // Mode / measurement pinning.
1290
+ MODE_MISMATCH: "MRTD_MISMATCH",
1291
+ MRTD_MISMATCH: "MRTD_MISMATCH",
1292
+ RUNTIME_MEASUREMENTS_MISMATCH: "RUNTIME_MEASUREMENTS_MISMATCH",
1293
+ RUNTIME_PROFILE_MISMATCH: "RUNTIME_PROFILE_MISMATCH",
1294
+ // Local fallback disabled -> not attested.
1295
+ LOCAL_ATTESTATION_DISABLED: "NOT_ATTESTED",
1296
+ // Azure MAA token issuer / JWKS / policy / signature / compliance / binding.
1297
+ MAA_TOKEN_MISSING: "AZURE_MAA_JWKS_INVALID",
1298
+ MAA_TOKEN_PARSE: "AZURE_MAA_JWKS_INVALID",
1299
+ MAA_TOKEN_EXPIRED: "AZURE_MAA_JWKS_INVALID",
1300
+ MAA_TOKEN_SIGNATURE: "AZURE_MAA_JWKS_INVALID",
1301
+ MAA_TRUST_POLICY_MISSING: "AZURE_MAA_ISSUER_MISMATCH",
1302
+ MAA_ISSUER_MISMATCH: "AZURE_MAA_ISSUER_MISMATCH",
1303
+ MAA_JWKS_MISMATCH: "AZURE_MAA_JWKS_INVALID",
1304
+ MAA_POLICY_HASH_MISMATCH: "AZURE_MAA_POLICY_HASH_MISMATCH",
1305
+ MAA_COMPLIANCE_MISMATCH: "AZURE_MAA_NOT_COMPLIANT",
1306
+ MAA_REPORTDATA_MISMATCH: "AZURE_MAA_DCAP_MISMATCH",
1307
+ // Azure HCL key blob hash binding.
1308
+ HCL_KEYS_MISSING: "AZURE_HCL_KEYS_MISMATCH",
1309
+ HCL_KEYS_HASH_MISMATCH: "AZURE_HCL_KEYS_MISMATCH",
1310
+ HCL_KEYS_PARSE: "AZURE_AK_PUB_INVALID",
1311
+ // Azure TPM AK quote.
1312
+ TPM_QUOTE_MISSING: "AZURE_TPM_AK_QUOTE_INVALID",
1313
+ TPM_QUOTE_SIGNATURE: "AZURE_TPM_AK_QUOTE_INVALID"
1314
+ };
1315
+ function err(reason, message) {
1316
+ return new AttestationError(REASON_TO_CODE[reason], `[${reason}] ${message}`);
1317
+ }
1318
+ var MEASUREMENT_LEN = 48;
1319
+ var HEADER_SIZE = 48;
1320
+ var HEADER_VERSION_OFF = 0;
1321
+ var HEADER_ATT_KEY_OFF = 2;
1322
+ var HEADER_TEE_TYPE_OFF = 4;
1323
+ var TDX_TEE_TYPE = 129;
1324
+ var ECDSA_P256_KEY_TYPE = 2;
1325
+ var BODY_OFFSET = 48;
1326
+ var BODY_SIZE = 584;
1327
+ var BODY_TEE_TCB_SVN_OFF = 0;
1328
+ var BODY_MRSIGNERSEAM_OFF = 64;
1329
+ var BODY_SEAM_ATTRIBUTES_OFF = 112;
1330
+ var BODY_TD_ATTR_OFF = 120;
1331
+ var BODY_MRTD_OFF = 136;
1332
+ var BODY_RTMR0_OFF = 328;
1333
+ var BODY_RTMR1_OFF = 376;
1334
+ var BODY_RTMR2_OFF = 424;
1335
+ var BODY_RTMR3_OFF = 472;
1336
+ var BODY_REPORT_DATA_OFF = 520;
1337
+ var BODY_REPORT_DATA_LEN = 64;
1338
+ var SIG_DATA_LEN_OFFSET = BODY_OFFSET + BODY_SIZE;
1339
+ var SIG_QUOTE_SIG_OFF = 0;
1340
+ var SIG_ATT_KEY_OFF = 64;
1341
+ var SIG_QE_CERT_DATA_OFF = 128;
1342
+ var SIG_QE_CERT_DATA_HEADER_SIZE = 6;
1343
+ var SIG_QE_REPORT_SIZE = 384;
1344
+ var SIG_QE_REPORT_SIG_SIZE = 64;
1345
+ var SGX_REPORT_MISCSELECT_OFF = 16;
1346
+ var SGX_REPORT_ATTRIBUTES_OFF = 48;
1347
+ var SGX_REPORT_MRSIGNER_OFF = 128;
1348
+ var SGX_REPORT_ISV_PROD_ID_OFF = 256;
1349
+ var SGX_REPORT_ISV_SVN_OFF = 258;
1350
+ var SGX_REPORT_DATA_OFF = 320;
1351
+ var QE_CERT_TYPE_PCK_CHAIN = 5;
1352
+ var QE_CERT_TYPE_QE_REPORT_CERT_DATA = 6;
1353
+ var INTEL_PCK_SGX_EXTENSIONS_OID = "1.2.840.113741.1.13.1";
1354
+ var INTEL_PCK_EXTENSION_TCB_OID = "1.2.840.113741.1.13.1.2";
1355
+ var INTEL_PCK_EXTENSION_PCE_ID_OID = "1.2.840.113741.1.13.1.3";
1356
+ var INTEL_PCK_EXTENSION_FMSPC_OID = "1.2.840.113741.1.13.1.4";
1357
+ var INTEL_PCK_EXTENSION_PCESVN_OID = "1.2.840.113741.1.13.1.2.17";
1358
+ var INTEL_PCK_EXTENSION_CPUSVN_OID = "1.2.840.113741.1.13.1.2.18";
1359
+ var INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX = "1.2.840.113741.1.13.1.2.";
1360
+ var INTEL_TCB_SIGNING_SUBJECT = "CN=Intel SGX TCB Signing, O=Intel Corporation, L=Santa Clara, ST=CA, C=US";
1361
+ var TEST_TCB_SIGNING_SUBJECT = "CN=Test SGX and TDX TCB Signing";
1362
+ var ACCEPTED_TCB_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
1363
+ var ACCEPTED_TDX_MODULE_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
1364
+ var ACCEPTED_QE_IDENTITY_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
1365
+ async function verifyAttestation(params) {
1366
+ const { response, expectedNonce, expectedNoiseStaticPub, policy, timeoutMs } = params;
1367
+ if (policy.expectedRuntimeProfile !== void 0 && response.runtime_profile !== policy.expectedRuntimeProfile) {
1368
+ throw err(
1369
+ "RUNTIME_PROFILE_MISMATCH",
1370
+ `runtime profile ${String(response.runtime_profile)} does not match the approved release profile ${policy.expectedRuntimeProfile}`
1371
+ );
1372
+ }
1373
+ if (response.runtime_profile === "azure-tdx-v0-agentless" && policy.expectedRuntimeMeasurements === void 0) {
1374
+ throw err(
1375
+ "RUNTIME_MEASUREMENTS_MISMATCH",
1376
+ "agentless attestation requires pinned RTMR0..RTMR3 measurements"
1377
+ );
1378
+ }
1379
+ if (response.runtime_profile === "azure-tdx-v0-agentless" && response.local_attestation === true) {
1380
+ throw err(
1381
+ "LOCAL_ATTESTATION_DISABLED",
1382
+ "the agentless Azure TDX profile requires a hardware attestation quote"
1383
+ );
1384
+ }
1385
+ if (response.runtime_profile === "azure-tdx-v0-agentless" && response.platform !== "azure-tdx-dcap") {
1386
+ throw err(
1387
+ "UNSUPPORTED_PLATFORM",
1388
+ "the agentless Azure TDX profile requires an Azure DCAP attestation response"
1389
+ );
1390
+ }
1391
+ if (response.local_attestation === true) {
1392
+ return verifyLocalAttestation({
1393
+ response,
1394
+ expectedNonce,
1395
+ expectedNoiseStaticPub,
1396
+ policy
1397
+ });
1398
+ }
1399
+ const raw = decodeQuoteB64(response.quote);
1400
+ ensureStructure(raw);
1401
+ if (response.platform === "azure-tdx-dcap") {
1402
+ return verifyAzureTdxAttestation({
1403
+ raw,
1404
+ response,
1405
+ expectedNonce,
1406
+ expectedNoiseStaticPub,
1407
+ policy,
1408
+ timeoutMs
1409
+ });
1410
+ }
1411
+ if (response.platform && response.platform !== "tdx-dcap") {
1412
+ throw err(
1413
+ "UNSUPPORTED_PLATFORM",
1414
+ `attestation not supported for platform "${response.platform}"`
1415
+ );
1416
+ }
1417
+ return verifyGcpTdxAttestation({
1418
+ raw,
1419
+ response,
1420
+ expectedNonce,
1421
+ expectedNoiseStaticPub,
1422
+ policy
1423
+ });
1424
+ }
1425
+ async function verifyGcpTdxAttestation(params) {
1426
+ const { raw, response, expectedNonce, expectedNoiseStaticPub, policy } = params;
1427
+ const body = raw.subarray(BODY_OFFSET, BODY_OFFSET + BODY_SIZE);
1428
+ const reportData = body.subarray(
1429
+ BODY_REPORT_DATA_OFF,
1430
+ BODY_REPORT_DATA_OFF + BODY_REPORT_DATA_LEN
1431
+ );
1432
+ if (!constantTimeEq(reportData.subarray(0, 32), expectedNonce)) {
1433
+ throw err(
1434
+ "NONCE_MISMATCH",
1435
+ "REPORT_DATA[0:32] does not match verifier nonce - possible replay"
1436
+ );
1437
+ }
1438
+ if (response.nonce.toLowerCase() !== bytesToHex2(expectedNonce)) {
1439
+ throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
1440
+ }
1441
+ if (!constantTimeEq(reportData.subarray(32, 64), expectedNoiseStaticPub)) {
1442
+ throw err(
1443
+ "CHANNEL_BINDING_MISMATCH",
1444
+ "REPORT_DATA[32:64] does not match Noise static pubkey from handshake"
1445
+ );
1446
+ }
1447
+ const expectedPubHex = bytesToHex2(expectedNoiseStaticPub);
1448
+ if (response.pubkey.toLowerCase() !== expectedPubHex) {
1449
+ throw err(
1450
+ "PUBKEY_FIELD_MISMATCH",
1451
+ "attestation pubkey field does not match Noise static pubkey from handshake"
1452
+ );
1453
+ }
1454
+ if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
1455
+ throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
1456
+ }
1457
+ if (policy.expectedBinaryHash !== void 0 && response.binary_hash.toLowerCase() !== policy.expectedBinaryHash.toLowerCase()) {
1458
+ throw err(
1459
+ "BINARY_HASH_MISMATCH",
1460
+ `binary hash ${response.binary_hash.toLowerCase()} does not match the approved release pin`
1461
+ );
1462
+ }
1463
+ await verifyDcapQuoteAndCollateral({ raw, body, response, policy });
1464
+ assertDebugBitClear(body);
1465
+ const measurements = parseMeasurements(body);
1466
+ assertRuntimeMeasurementsMatch(measurements, policy);
1467
+ if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
1468
+ throw err(
1469
+ "MODE_MISMATCH",
1470
+ `attestation mode ${response.mode} does not match required ${policy.requiredMode}`
1471
+ );
1472
+ }
1473
+ if (response.mode === "prod") {
1474
+ if ((policy.expectedMrtd === void 0 || policy.expectedMrtd === null) && policy.allowUnpinnedProdMrtdForRelease !== true) {
1475
+ throw err(
1476
+ "MRTD_MISMATCH",
1477
+ "production attestation requires a pinned MRTD but none was configured"
1478
+ );
1479
+ }
1480
+ if (policy.expectedMrtd !== void 0 && policy.expectedMrtd !== null && measurements.mrtd !== policy.expectedMrtd.toLowerCase()) {
1481
+ throw err(
1482
+ "MRTD_MISMATCH",
1483
+ `MRTD ${measurements.mrtd} does not match pinned ${policy.expectedMrtd.toLowerCase()}`
1484
+ );
1485
+ }
1486
+ }
1487
+ return {
1488
+ mode: response.mode,
1489
+ binaryHash: response.binary_hash,
1490
+ runtimeProfile: response.runtime_profile,
1491
+ mrtd: measurements.mrtd,
1492
+ rtmr0: measurements.rtmr0,
1493
+ rtmr1: measurements.rtmr1,
1494
+ rtmr2: measurements.rtmr2,
1495
+ rtmr3: measurements.rtmr3,
1496
+ pubkeyHex: response.pubkey.toLowerCase(),
1497
+ localAttestation: false
1498
+ };
1499
+ }
1500
+ function assertRuntimeMeasurementsMatch(measurements, policy) {
1501
+ const expected = policy.expectedRuntimeMeasurements;
1502
+ if (expected === void 0) return;
1503
+ for (const field of ["rtmr0", "rtmr1", "rtmr2", "rtmr3"]) {
1504
+ if (!/^[0-9a-f]{96}$/i.test(expected[field])) {
1505
+ throw err(
1506
+ "RUNTIME_MEASUREMENTS_MISMATCH",
1507
+ `approved ${field.toUpperCase()} pin is not a 48-byte hex digest`
1508
+ );
1509
+ }
1510
+ if (measurements[field] !== expected[field].toLowerCase()) {
1511
+ throw err(
1512
+ "RUNTIME_MEASUREMENTS_MISMATCH",
1513
+ `${field.toUpperCase()} does not match the approved runtime measurement`
1514
+ );
1515
+ }
1516
+ }
1517
+ }
1518
+ async function verifyDcapQuoteAndCollateral(params) {
1519
+ const { raw, body, response, policy } = params;
1520
+ const sigData = readSigData(raw);
1521
+ const qeCertData = parseQeCertificationData(sigData);
1522
+ const pckChain = extractPckChain(qeCertData.qeCertificationData);
1523
+ const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
1524
+ const rootFp = await sha256Hex(new Uint8Array(pckChain[pckChain.length - 1].rawData));
1525
+ if (rootFp !== expectedRootFp) {
1526
+ throw err(
1527
+ "ROOT_FINGERPRINT_MISMATCH",
1528
+ `PCK root fingerprint ${rootFp} does not match pinned ${expectedRootFp}`
1529
+ );
1530
+ }
1531
+ await verifyChainSignatures(pckChain);
1532
+ const quoteSig = sigData.subarray(SIG_QUOTE_SIG_OFF, SIG_QUOTE_SIG_OFF + 64);
1533
+ const attKey = sigData.subarray(SIG_ATT_KEY_OFF, SIG_ATT_KEY_OFF + 64);
1534
+ const headerAndBody = raw.subarray(0, BODY_OFFSET + BODY_SIZE);
1535
+ await verifyP256Signature(
1536
+ rawXyToSpki(attKey),
1537
+ quoteSig,
1538
+ headerAndBody,
1539
+ "QUOTE_SIGNATURE",
1540
+ "quote header||body signature"
1541
+ );
1542
+ const { qeReport, qeReportSig, qeAuthData } = qeCertData;
1543
+ const expectedBinding = await sha256(concat(attKey, qeAuthData));
1544
+ const qeReportData = qeReport.subarray(SGX_REPORT_DATA_OFF, SGX_REPORT_DATA_OFF + 32);
1545
+ if (!constantTimeEq(qeReportData, expectedBinding)) {
1546
+ throw err(
1547
+ "QE_REPORT_BINDING",
1548
+ "QE report does not bind the in-quote attestation key (REPORT_DATA mismatch)"
1549
+ );
1550
+ }
1551
+ const pckLeafSpki = await leafSpki(pckChain[0]);
1552
+ await verifyP256Signature(
1553
+ pckLeafSpki,
1554
+ qeReportSig,
1555
+ qeReport,
1556
+ "QE_REPORT_SIGNATURE",
1557
+ "QE report signature"
1558
+ );
1559
+ await verifyDcapCollateral({
1560
+ collateral: response.dcap_collateral,
1561
+ pckChain,
1562
+ body,
1563
+ qeReport,
1564
+ mode: response.mode,
1565
+ policy
1566
+ });
1567
+ }
1568
+ async function verifyDcapCollateral(params) {
1569
+ const { collateral, pckChain, body, qeReport, mode, policy } = params;
1570
+ if (!collateral) {
1571
+ if (policy.allowMissingDcapCollateral === true && mode === "dev" && policy.requiredMode === "dev") {
1572
+ return;
1573
+ }
1574
+ throw err("DCAP_COLLATERAL_MISSING", "TDX quote is missing Intel DCAP collateral");
1575
+ }
1576
+ const now = /* @__PURE__ */ new Date();
1577
+ const rootCert = pckChain[pckChain.length - 1];
1578
+ const rootCrl = parseCrl(collateral.rootCaCrlPem, "root_ca_crl");
1579
+ await verifyCrl(rootCrl, rootCert, "root_ca_crl", now);
1580
+ const pckCrlIssuerChain = await verifyCollateralIssuerChain(
1581
+ parsePemCertificateChain(collateral.pckCrlIssuerChainPem, "pck_crl_issuer_chain", 2),
1582
+ policy,
1583
+ "pck_crl_issuer_chain"
1584
+ );
1585
+ await assertNotRevokedByRoot(rootCrl, pckCrlIssuerChain[0], "PCK CRL issuer");
1586
+ await assertSameCertificate(pckCrlIssuerChain[0], pckChain[1], "PCK CRL issuer");
1587
+ const pckCrl = parseCrl(collateral.pckCrlPem, "pck_crl");
1588
+ await verifyCrl(pckCrl, pckCrlIssuerChain[0], "pck_crl", now);
1589
+ if (pckCrl.findRevoked(pckChain[0])) {
1590
+ throw err("DCAP_COLLATERAL_REVOKED", "PCK leaf certificate is revoked by PCK CRL");
1591
+ }
1592
+ const tcbInfo = await verifySignedDcapJson({
1593
+ signed: collateral.tcbInfo,
1594
+ wrapperField: "tcbInfo",
1595
+ policy,
1596
+ rootCrl,
1597
+ label: "tcb_info"
1598
+ });
1599
+ const qeIdentity = await verifySignedDcapJson({
1600
+ signed: collateral.qeIdentity,
1601
+ wrapperField: "enclaveIdentity",
1602
+ policy,
1603
+ rootCrl,
1604
+ label: "qe_identity"
1605
+ });
1606
+ assertMatchingTcbEvaluationDataNumber(tcbInfo, qeIdentity);
1607
+ verifyTcbInfo(tcbInfo, pckChain[0], body, now);
1608
+ verifyQeIdentity(qeIdentity, qeReport, now);
1609
+ }
1610
+ function assertDebugBitClear(body) {
1611
+ if ((body[BODY_TD_ATTR_OFF] & 1) !== 0) {
1612
+ throw err("DEBUG_BIT_SET", "TD_ATTRIBUTES DEBUG bit set - refusing to attest a debuggable TD");
1613
+ }
1614
+ }
1615
+ async function verifySignedDcapJson(params) {
1616
+ const { signed, wrapperField, policy, rootCrl, label } = params;
1617
+ if (!signed || typeof signed.body !== "string" || typeof signed.signature !== "string" || typeof signed.issuerChainPem !== "string") {
1618
+ throw err("DCAP_COLLATERAL_MISSING", `${label} collateral is incomplete`);
1619
+ }
1620
+ let parsed;
1621
+ try {
1622
+ parsed = JSON.parse(signed.body);
1623
+ } catch (e) {
1624
+ throw err("DCAP_COLLATERAL_PARSE", `${label} body is not valid JSON: ${e}`);
1625
+ }
1626
+ const signedBody = exactSignedDcapJsonBody(signed.body, parsed, wrapperField, label);
1627
+ const body = signedBody.parsed;
1628
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1629
+ throw err("DCAP_COLLATERAL_PARSE", `${label} body is not a JSON object`);
1630
+ }
1631
+ const issuerChain = await verifyCollateralIssuerChain(
1632
+ parsePemCertificateChain(signed.issuerChainPem, `${label}_issuer_chain`, 2),
1633
+ policy,
1634
+ `${label}_issuer_chain`
1635
+ );
1636
+ await assertNotRevokedByRoot(rootCrl, issuerChain[0], `${label} signing certificate`);
1637
+ assertDcapJsonSignerIdentity(issuerChain[0], issuerChain[issuerChain.length - 1], policy, label);
1638
+ if (!/^[0-9a-f]{128}$/i.test(signed.signature)) {
1639
+ throw err("DCAP_COLLATERAL_PARSE", `${label} signature is not a 64-byte hex ECDSA signature`);
1640
+ }
1641
+ await verifyP256Signature(
1642
+ await leafSpki(issuerChain[0]),
1643
+ hexToBytes2(signed.signature),
1644
+ new TextEncoder().encode(signedBody.body),
1645
+ "DCAP_COLLATERAL_SIGNATURE",
1646
+ `${label} signature`
1647
+ );
1648
+ return body;
1649
+ }
1650
+ function exactSignedDcapJsonBody(raw, parsed, wrapperField, label) {
1651
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1652
+ return { body: raw, parsed };
1653
+ }
1654
+ if (!(wrapperField in parsed)) {
1655
+ return { body: raw, parsed };
1656
+ }
1657
+ const body = extractTopLevelJsonProperty(raw, wrapperField);
1658
+ if (body === null) {
1659
+ throw err(
1660
+ "DCAP_COLLATERAL_PARSE",
1661
+ `${label} PCS wrapper contains ${wrapperField} but exact signed body bytes could not be extracted`
1662
+ );
1663
+ }
1664
+ try {
1665
+ return { body, parsed: JSON.parse(body) };
1666
+ } catch (e) {
1667
+ throw err("DCAP_COLLATERAL_PARSE", `${label} exact signed body is not valid JSON: ${e}`);
1668
+ }
1669
+ }
1670
+ function extractTopLevelJsonProperty(raw, property) {
1671
+ let offset = skipJsonWhitespace(raw, 0);
1672
+ if (raw[offset] !== "{") return null;
1673
+ offset++;
1674
+ while (offset < raw.length) {
1675
+ offset = skipJsonWhitespace(raw, offset);
1676
+ if (raw[offset] === "}") return null;
1677
+ if (raw[offset] === ",") {
1678
+ offset++;
1679
+ continue;
1680
+ }
1681
+ const key = readJsonString(raw, offset);
1682
+ if (key === null) return null;
1683
+ offset = skipJsonWhitespace(raw, key.end);
1684
+ if (raw[offset] !== ":") return null;
1685
+ offset = skipJsonWhitespace(raw, offset + 1);
1686
+ const valueStart = offset;
1687
+ const valueEnd = findJsonValueEnd(raw, valueStart);
1688
+ if (valueEnd === null) return null;
1689
+ if (key.value === property) return raw.slice(valueStart, valueEnd);
1690
+ offset = valueEnd;
1691
+ }
1692
+ return null;
1693
+ }
1694
+ function skipJsonWhitespace(raw, offset) {
1695
+ while (offset < raw.length && /[\t\n\r ]/u.test(raw[offset])) offset++;
1696
+ return offset;
1697
+ }
1698
+ function readJsonString(raw, offset) {
1699
+ if (raw[offset] !== '"') return null;
1700
+ let cursor = offset + 1;
1701
+ let escaped = false;
1702
+ while (cursor < raw.length) {
1703
+ const char = raw[cursor];
1704
+ if (escaped) {
1705
+ escaped = false;
1706
+ } else if (char === "\\") {
1707
+ escaped = true;
1708
+ } else if (char === '"') {
1709
+ const token = raw.slice(offset, cursor + 1);
1710
+ try {
1711
+ return { value: JSON.parse(token), end: cursor + 1 };
1712
+ } catch {
1713
+ return null;
1714
+ }
1715
+ }
1716
+ cursor++;
1717
+ }
1718
+ return null;
1719
+ }
1720
+ function findJsonValueEnd(raw, offset) {
1721
+ if (offset >= raw.length) return null;
1722
+ if (raw[offset] === '"') return readJsonString(raw, offset)?.end ?? null;
1723
+ if (raw[offset] === "{" || raw[offset] === "[") {
1724
+ const stack = [raw[offset]];
1725
+ let cursor2 = offset + 1;
1726
+ while (cursor2 < raw.length) {
1727
+ const char = raw[cursor2];
1728
+ if (char === '"') {
1729
+ const stringToken = readJsonString(raw, cursor2);
1730
+ if (stringToken === null) return null;
1731
+ cursor2 = stringToken.end;
1732
+ continue;
1733
+ }
1734
+ if (char === "{" || char === "[") {
1735
+ stack.push(char);
1736
+ } else if (char === "}" || char === "]") {
1737
+ const open = stack.pop();
1738
+ if (char === "}" && open !== "{" || char === "]" && open !== "[") return null;
1739
+ if (stack.length === 0) return cursor2 + 1;
1740
+ }
1741
+ cursor2++;
1742
+ }
1743
+ return null;
1744
+ }
1745
+ let cursor = offset;
1746
+ while (cursor < raw.length && !/[\t\n\r ,}\]]/u.test(raw[cursor])) cursor++;
1747
+ return cursor > offset ? cursor : null;
1748
+ }
1749
+ function assertMatchingTcbEvaluationDataNumber(tcbInfo, qeIdentity) {
1750
+ const tcbNumber = tcbInfo.tcbEvaluationDataNumber;
1751
+ const qeNumber = qeIdentity.tcbEvaluationDataNumber;
1752
+ if (!isTcbEvaluationDataNumber(tcbNumber) || !isTcbEvaluationDataNumber(qeNumber)) {
1753
+ throw err(
1754
+ "TCB_STATUS_REJECTED",
1755
+ "TCB Info and QE Identity must include valid tcbEvaluationDataNumber values"
1756
+ );
1757
+ }
1758
+ if (tcbNumber !== qeNumber) {
1759
+ throw err(
1760
+ "TCB_STATUS_REJECTED",
1761
+ `TCB Info tcbEvaluationDataNumber ${tcbNumber} does not match QE Identity ${qeNumber}`
1762
+ );
1763
+ }
1764
+ }
1765
+ function isTcbEvaluationDataNumber(value) {
1766
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1767
+ }
1768
+ async function verifyCollateralIssuerChain(certs, policy, label) {
1769
+ const candidates = [certs, [...certs].reverse()];
1770
+ let lastError;
1771
+ for (const chain of candidates) {
1772
+ try {
1773
+ const rootFp = await sha256Hex(new Uint8Array(chain[chain.length - 1].rawData));
1774
+ const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
1775
+ if (rootFp !== expectedRootFp) {
1776
+ throw err(
1777
+ "ROOT_FINGERPRINT_MISMATCH",
1778
+ `${label} root fingerprint ${rootFp} does not match pinned ${expectedRootFp}`
1779
+ );
1780
+ }
1781
+ await verifyChainSignatures(chain);
1782
+ return chain;
1783
+ } catch (e) {
1784
+ lastError = e;
1785
+ }
1786
+ }
1787
+ if (lastError instanceof AttestationError) throw lastError;
1788
+ throw err("DCAP_COLLATERAL_SIGNATURE", `${label} certificate chain failed verification`);
1789
+ }
1790
+ function assertDcapJsonSignerIdentity(leaf, root, policy, label) {
1791
+ const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
1792
+ const allowsSyntheticRoot = expectedRootFp !== INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
1793
+ const allowedSubjects = allowsSyntheticRoot ? [INTEL_TCB_SIGNING_SUBJECT, TEST_TCB_SIGNING_SUBJECT] : [INTEL_TCB_SIGNING_SUBJECT];
1794
+ if (!allowedSubjects.includes(leaf.subject)) {
1795
+ throw err(
1796
+ "DCAP_COLLATERAL_SIGNATURE",
1797
+ `${label} signing certificate subject ${leaf.subject} is not an Intel PCS TCB signing identity`
1798
+ );
1799
+ }
1800
+ if (leaf.issuer !== root.subject) {
1801
+ throw err(
1802
+ "DCAP_COLLATERAL_SIGNATURE",
1803
+ `${label} signing certificate issuer ${leaf.issuer} does not match root ${root.subject}`
1804
+ );
1805
+ }
1806
+ const basicConstraints = leaf.getExtension(x509.BasicConstraintsExtension);
1807
+ if (basicConstraints?.ca) {
1808
+ throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signing certificate must not be a CA`);
1809
+ }
1810
+ }
1811
+ function verifyTcbInfo(tcbInfo, pckLeaf, body, now) {
1812
+ if (tcbInfo.id !== "TDX") {
1813
+ throw err("TCB_STATUS_REJECTED", `TCB Info id ${String(tcbInfo.id)} is not TDX`);
1814
+ }
1815
+ if (tcbInfo.version !== 3 || tcbInfo.tcbType !== 0) {
1816
+ throw err(
1817
+ "TCB_STATUS_REJECTED",
1818
+ `unsupported TCB Info version=${String(tcbInfo.version)} tcbType=${String(tcbInfo.tcbType)}`
1819
+ );
1820
+ }
1821
+ assertFreshDcapJsonWindow(tcbInfo, "TCB Info", now);
1822
+ const pckTcb = extractPckTcb(pckLeaf);
1823
+ if (typeof tcbInfo.fmspc !== "string" || tcbInfo.fmspc.toLowerCase() !== pckTcb.fmspc) {
1824
+ throw err(
1825
+ "TCB_STATUS_REJECTED",
1826
+ `TCB Info FMSPC ${String(tcbInfo.fmspc)} does not match PCK FMSPC ${pckTcb.fmspc}`
1827
+ );
1828
+ }
1829
+ if (typeof tcbInfo.pceId !== "string" || tcbInfo.pceId.toLowerCase() !== pckTcb.pceId) {
1830
+ throw err(
1831
+ "TCB_STATUS_REJECTED",
1832
+ `TCB Info PCE ID ${String(tcbInfo.pceId)} does not match PCK PCE ID ${pckTcb.pceId}`
1833
+ );
1834
+ }
1835
+ if (!Array.isArray(tcbInfo.tcbLevels)) {
1836
+ throw err("TCB_STATUS_REJECTED", "TCB Info has no tcbLevels array");
1837
+ }
1838
+ const teeTcbSvn = body.subarray(BODY_TEE_TCB_SVN_OFF, BODY_TEE_TCB_SVN_OFF + 16);
1839
+ const matched = tcbInfo.tcbLevels.find((level) => tdxTcbLevelMatches(level, pckTcb, teeTcbSvn));
1840
+ if (!matched || typeof matched !== "object") {
1841
+ throw err("TCB_STATUS_REJECTED", "no TCB Info level matches PCK SGX TCB and quote TEE_TCB_SVN");
1842
+ }
1843
+ const status = matched["tcbStatus"];
1844
+ if (typeof status !== "string" || !ACCEPTED_TCB_STATUSES.has(status)) {
1845
+ throw err("TCB_STATUS_REJECTED", `TCB status ${String(status)} is not accepted`);
1846
+ }
1847
+ verifyTdxModuleIdentity(tcbInfo, body, teeTcbSvn);
1848
+ }
1849
+ function verifyQeIdentity(identity, qeReport, now) {
1850
+ if (identity.id !== "TD_QE") {
1851
+ throw err("QE_IDENTITY_REJECTED", `QE Identity id ${String(identity.id)} is not TD_QE`);
1852
+ }
1853
+ if (identity.version !== 2) {
1854
+ throw err(
1855
+ "QE_IDENTITY_REJECTED",
1856
+ `unsupported QE Identity version ${String(identity.version)}`
1857
+ );
1858
+ }
1859
+ assertFreshDcapJsonWindow(identity, "QE Identity", now);
1860
+ assertMaskedHexMatches({
1861
+ actual: qeReport.subarray(SGX_REPORT_MISCSELECT_OFF, SGX_REPORT_MISCSELECT_OFF + 4),
1862
+ expectedHex: identity.miscselect,
1863
+ maskHex: identity.miscselectMask,
1864
+ label: "QE Identity MISCSELECT",
1865
+ reason: "QE_IDENTITY_REJECTED"
1866
+ });
1867
+ assertMaskedHexMatches({
1868
+ actual: qeReport.subarray(SGX_REPORT_ATTRIBUTES_OFF, SGX_REPORT_ATTRIBUTES_OFF + 16),
1869
+ expectedHex: identity.attributes,
1870
+ maskHex: identity.attributesMask,
1871
+ label: "QE Identity ATTRIBUTES",
1872
+ reason: "QE_IDENTITY_REJECTED"
1873
+ });
1874
+ const expectedMrSigner = bytesToHex2(
1875
+ qeReport.subarray(SGX_REPORT_MRSIGNER_OFF, SGX_REPORT_MRSIGNER_OFF + 32)
1876
+ );
1877
+ if (typeof identity.mrsigner !== "string" || identity.mrsigner.toLowerCase() !== expectedMrSigner) {
1878
+ throw err(
1879
+ "QE_IDENTITY_REJECTED",
1880
+ `QE Identity MRSIGNER ${String(identity.mrsigner)} does not match QE report ${expectedMrSigner}`
1881
+ );
1882
+ }
1883
+ const isvProdId = readU16LE(qeReport, SGX_REPORT_ISV_PROD_ID_OFF);
1884
+ if (identity.isvprodid !== isvProdId) {
1885
+ throw err(
1886
+ "QE_IDENTITY_REJECTED",
1887
+ `QE Identity ISVProdID ${String(identity.isvprodid)} does not match QE report ${isvProdId}`
1888
+ );
1889
+ }
1890
+ if (!Array.isArray(identity.tcbLevels)) {
1891
+ throw err("QE_IDENTITY_REJECTED", "QE Identity has no tcbLevels array");
1892
+ }
1893
+ const isvSvn = readU16LE(qeReport, SGX_REPORT_ISV_SVN_OFF);
1894
+ const matched = identity.tcbLevels.find((level) => qeIdentityLevelMatches(level, isvSvn));
1895
+ if (!matched || typeof matched !== "object") {
1896
+ throw err("QE_IDENTITY_REJECTED", "no QE Identity level matches QE report ISVSVN");
1897
+ }
1898
+ const status = matched["tcbStatus"];
1899
+ if (typeof status !== "string" || !ACCEPTED_QE_IDENTITY_STATUSES.has(status)) {
1900
+ throw err("QE_IDENTITY_REJECTED", `QE Identity status ${String(status)} is not accepted`);
1901
+ }
1902
+ }
1903
+ function tdxTcbLevelMatches(level, pckTcb, teeTcbSvn) {
1904
+ if (!level || typeof level !== "object") return false;
1905
+ const tcb = level["tcb"];
1906
+ if (!tcb || typeof tcb !== "object") return false;
1907
+ const record = tcb;
1908
+ const sgxComponents = record["sgxtcbcomponents"];
1909
+ if (!tcbComponentsMatch(sgxComponents, pckTcb.sgxTcbSvns, 0)) return false;
1910
+ const pceSvn = record["pcesvn"];
1911
+ if (typeof pceSvn !== "number" || pckTcb.pceSvn < pceSvn) return false;
1912
+ const tdxComponents = record["tdxtcbcomponents"];
1913
+ const tdxStartIndex = teeTcbSvn[1] === 0 ? 0 : 2;
1914
+ return tcbComponentsMatch(tdxComponents, Array.from(teeTcbSvn), tdxStartIndex);
1915
+ }
1916
+ function tcbComponentsMatch(components, actualSvns, startIndex) {
1917
+ if (!Array.isArray(components) || components.length < 16) return false;
1918
+ for (let index = startIndex; index < 16; index++) {
1919
+ const component = components[index];
1920
+ if (!component || typeof component !== "object") return false;
1921
+ const svn = component["svn"];
1922
+ if (typeof svn !== "number" || actualSvns[index] < svn) return false;
1923
+ }
1924
+ return true;
1925
+ }
1926
+ function verifyTdxModuleIdentity(tcbInfo, body, teeTcbSvn) {
1927
+ const moduleVersion = teeTcbSvn[1];
1928
+ const identity = getTdxModuleIdentity(tcbInfo, moduleVersion);
1929
+ const expectedMrSigner = bytesToHex2(
1930
+ body.subarray(BODY_MRSIGNERSEAM_OFF, BODY_MRSIGNERSEAM_OFF + 48)
1931
+ );
1932
+ if (typeof identity["mrsigner"] !== "string" || identity["mrsigner"].toLowerCase() !== expectedMrSigner) {
1933
+ throw err(
1934
+ "TCB_STATUS_REJECTED",
1935
+ `TDX Module Identity MRSIGNER ${String(identity["mrsigner"])} does not match quote ${expectedMrSigner}`
1936
+ );
1937
+ }
1938
+ assertMaskedHexMatches({
1939
+ actual: body.subarray(BODY_SEAM_ATTRIBUTES_OFF, BODY_SEAM_ATTRIBUTES_OFF + 8),
1940
+ expectedHex: identity["attributes"],
1941
+ maskHex: identity["attributesMask"],
1942
+ label: "TDX Module Identity ATTRIBUTES",
1943
+ reason: "TCB_STATUS_REJECTED"
1944
+ });
1945
+ if (moduleVersion < 1) return;
1946
+ const tcbLevels = identity["tcbLevels"];
1947
+ if (!Array.isArray(tcbLevels)) {
1948
+ throw err("TCB_STATUS_REJECTED", "TDX Module Identity has no tcbLevels array");
1949
+ }
1950
+ const matched = tcbLevels.find((level) => tdxModuleTcbLevelMatches(level, teeTcbSvn[0]));
1951
+ if (!matched || typeof matched !== "object") {
1952
+ throw err("TCB_STATUS_REJECTED", "no TDX Module Identity level matches TEE_TCB_SVN ISVSVN");
1953
+ }
1954
+ const status = matched["tcbStatus"];
1955
+ if (typeof status !== "string" || !ACCEPTED_TDX_MODULE_STATUSES.has(status)) {
1956
+ throw err("TCB_STATUS_REJECTED", `TDX Module status ${String(status)} is not accepted`);
1957
+ }
1958
+ }
1959
+ function getTdxModuleIdentity(tcbInfo, moduleVersion) {
1960
+ if (moduleVersion < 1) {
1961
+ const module = tcbInfo.tdxModule;
1962
+ if (!module || typeof module !== "object" || Array.isArray(module)) {
1963
+ throw err("TCB_STATUS_REJECTED", "TCB Info has no tdxModule object");
1964
+ }
1965
+ return module;
1966
+ }
1967
+ const identities = tcbInfo.tdxModuleIdentities;
1968
+ if (!Array.isArray(identities)) {
1969
+ throw err("TCB_STATUS_REJECTED", "TCB Info has no tdxModuleIdentities array");
1970
+ }
1971
+ const moduleIdentity = identities.find((candidate) => {
1972
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return false;
1973
+ const id = candidate["id"];
1974
+ return typeof id === "string" && tdxModuleIdMatches(id, moduleVersion);
1975
+ });
1976
+ if (!moduleIdentity || typeof moduleIdentity !== "object") {
1977
+ throw err(
1978
+ "TCB_STATUS_REJECTED",
1979
+ `no TDX Module Identity matches TEE_TCB_SVN module version ${moduleVersion}`
1980
+ );
1981
+ }
1982
+ return moduleIdentity;
1983
+ }
1984
+ function tdxModuleIdMatches(id, moduleVersion) {
1985
+ const match = /^TDX_(\d+)$/.exec(id);
1986
+ return match !== null && Number.parseInt(match[1], 10) === moduleVersion;
1987
+ }
1988
+ function tdxModuleTcbLevelMatches(level, isvSvn) {
1989
+ if (!level || typeof level !== "object") return false;
1990
+ const tcb = level["tcb"];
1991
+ if (!tcb || typeof tcb !== "object") return false;
1992
+ const record = tcb;
1993
+ const required = typeof record["isvsvn"] === "number" ? record["isvsvn"] : typeof record["isvnsvn"] === "number" ? record["isvnsvn"] : void 0;
1994
+ return typeof required === "number" && isvSvn >= required;
1995
+ }
1996
+ function qeIdentityLevelMatches(level, isvSvn) {
1997
+ if (!level || typeof level !== "object") return false;
1998
+ const tcb = level["tcb"];
1999
+ if (!tcb || typeof tcb !== "object") return false;
2000
+ const requiredIsvSvn = tcb["isvsvn"];
2001
+ return typeof requiredIsvSvn === "number" && isvSvn >= requiredIsvSvn;
2002
+ }
2003
+ function assertMaskedHexMatches(params) {
2004
+ const { actual, expectedHex, maskHex, label, reason } = params;
2005
+ if (typeof expectedHex !== "string" || !isHexByteLength(expectedHex, actual.length)) {
2006
+ throw err(reason, `${label} is missing or not ${actual.length} bytes of hex`);
2007
+ }
2008
+ if (typeof maskHex !== "string" || !isHexByteLength(maskHex, actual.length)) {
2009
+ throw err(reason, `${label} mask is missing or not ${actual.length} bytes of hex`);
2010
+ }
2011
+ const expected = hexToBytes2(expectedHex);
2012
+ const mask = hexToBytes2(maskHex);
2013
+ for (let index = 0; index < actual.length; index++) {
2014
+ if ((actual[index] & mask[index]) !== (expected[index] & mask[index])) {
2015
+ throw err(reason, `${label} masked byte ${index} does not match signed collateral policy`);
2016
+ }
2017
+ }
2018
+ }
2019
+ function isHexByteLength(value, byteLength) {
2020
+ return value.length === byteLength * 2 && /^[0-9a-f]+$/i.test(value);
2021
+ }
2022
+ function assertFreshDcapJsonWindow(value, label, now) {
2023
+ if (typeof value.issueDate !== "string" || typeof value.nextUpdate !== "string") {
2024
+ throw err("DCAP_COLLATERAL_PARSE", `${label} is missing issueDate or nextUpdate`);
2025
+ }
2026
+ const issueDate = new Date(value.issueDate);
2027
+ const nextUpdate = new Date(value.nextUpdate);
2028
+ if (Number.isNaN(issueDate.getTime()) || Number.isNaN(nextUpdate.getTime())) {
2029
+ throw err("DCAP_COLLATERAL_PARSE", `${label} has invalid issueDate or nextUpdate`);
2030
+ }
2031
+ if (now < issueDate) {
2032
+ throw err("TCB_STATUS_REJECTED", `${label} is not yet valid`);
2033
+ }
2034
+ if (now > nextUpdate) {
2035
+ throw err("TCB_STATUS_REJECTED", `${label} is stale past nextUpdate`);
2036
+ }
2037
+ }
2038
+ function parseCrl(pem, label) {
2039
+ if (typeof pem !== "string" || pem.length === 0) {
2040
+ throw err("DCAP_COLLATERAL_MISSING", `${label} is missing`);
2041
+ }
2042
+ try {
2043
+ return new x509.X509Crl(pem);
2044
+ } catch (e) {
2045
+ throw err("DCAP_COLLATERAL_PARSE", `${label} is not a parseable X.509 CRL: ${e}`);
2046
+ }
2047
+ }
2048
+ async function verifyCrl(crl, issuer, label, now) {
2049
+ if (crl.issuer !== issuer.subject) {
2050
+ throw err(
2051
+ "DCAP_COLLATERAL_SIGNATURE",
2052
+ `${label} issuer ${crl.issuer} does not match expected ${issuer.subject}`
2053
+ );
2054
+ }
2055
+ if (now < crl.thisUpdate) {
2056
+ throw err("TCB_STATUS_REJECTED", `${label} is not yet valid`);
2057
+ }
2058
+ if (!crl.nextUpdate || now > crl.nextUpdate) {
2059
+ throw err("TCB_STATUS_REJECTED", `${label} is stale past nextUpdate`);
2060
+ }
2061
+ let ok;
2062
+ try {
2063
+ ok = await crl.verify({ publicKey: issuer });
2064
+ } catch (e) {
2065
+ throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signature verification threw: ${e}`);
2066
+ }
2067
+ if (!ok) {
2068
+ throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signature verification failed`);
2069
+ }
2070
+ }
2071
+ async function assertNotRevokedByRoot(rootCrl, cert, label) {
2072
+ if (rootCrl.findRevoked(cert)) {
2073
+ throw err("DCAP_COLLATERAL_REVOKED", `${label} is revoked by Intel root CRL`);
2074
+ }
2075
+ }
2076
+ async function assertSameCertificate(actual, expected, label) {
2077
+ const actualFp = await sha256Hex(new Uint8Array(actual.rawData));
2078
+ const expectedFp = await sha256Hex(new Uint8Array(expected.rawData));
2079
+ if (actualFp !== expectedFp) {
2080
+ throw err(
2081
+ "DCAP_COLLATERAL_SIGNATURE",
2082
+ `${label} fingerprint ${actualFp} does not match quote chain issuer ${expectedFp}`
2083
+ );
2084
+ }
2085
+ }
2086
+ function parsePemCertificateChain(pem, label, minCerts) {
2087
+ if (typeof pem !== "string" || pem.length === 0) {
2088
+ throw err("DCAP_COLLATERAL_MISSING", `${label} is missing`);
2089
+ }
2090
+ const matches = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? [];
2091
+ if (matches.length < minCerts) {
2092
+ throw err(
2093
+ "DCAP_COLLATERAL_PARSE",
2094
+ `${label} has ${matches.length} cert(s), expected at least ${minCerts}`
2095
+ );
2096
+ }
2097
+ return matches.map((certPem, index) => {
2098
+ try {
2099
+ return new x509.X509Certificate(certPem);
2100
+ } catch (e) {
2101
+ throw err("DCAP_COLLATERAL_PARSE", `${label} cert[${index}] is not parseable: ${e}`);
2102
+ }
2103
+ });
2104
+ }
2105
+ function extractPckTcb(cert) {
2106
+ const sgxExtensions = cert.getExtension(INTEL_PCK_SGX_EXTENSIONS_OID);
2107
+ if (sgxExtensions) {
2108
+ return parsePckSgxExtensions(new Uint8Array(sgxExtensions.value));
2109
+ }
2110
+ return {
2111
+ fmspc: extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_FMSPC_OID, 6, "FMSPC"),
2112
+ pceId: extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_PCE_ID_OID, 2, "PCE ID"),
2113
+ sgxTcbSvns: Array.from(
2114
+ { length: 16 },
2115
+ (_, index) => extractPckIntegerExtension(
2116
+ cert,
2117
+ `${INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX}${index + 1}`,
2118
+ `SGX TCB component ${index + 1}`
2119
+ )
2120
+ ),
2121
+ pceSvn: extractPckIntegerExtension(cert, INTEL_PCK_EXTENSION_PCESVN_OID, "PCESVN"),
2122
+ cpuSvn: hexToBytes2(extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_CPUSVN_OID, 16, "CPUSVN"))
2123
+ };
2124
+ }
2125
+ function parsePckSgxExtensions(bytes) {
2126
+ const topLevel = unwrapDerOctetStringIfPresent(bytes);
2127
+ const extensions = derChildren(expectDerTag(readDerTlv(topLevel, 0), 48, "SGX extensions"));
2128
+ const parsed = {};
2129
+ for (const extension of extensions) {
2130
+ const contextTag = contextSpecificTagNumber(extension);
2131
+ if (contextTag === 2) {
2132
+ parsed.pceId = bytesToHex2(readContextOctetString(extension, "PCE ID"));
2133
+ continue;
2134
+ }
2135
+ if (contextTag === 3) {
2136
+ parsed.fmspc = bytesToHex2(readContextOctetString(extension, "FMSPC"));
2137
+ continue;
2138
+ }
2139
+ if (contextTag === 1) {
2140
+ const tcb = parsePckTcbExtension(extension);
2141
+ parsed.sgxTcbSvns = tcb.sgxTcbSvns;
2142
+ parsed.pceSvn = tcb.pceSvn;
2143
+ parsed.cpuSvn = tcb.cpuSvn;
2144
+ continue;
2145
+ }
2146
+ const children = derChildren(expectDerTag(extension, 48, "SGX extension item"));
2147
+ if (children.length !== 2) {
2148
+ throw err("TCB_STATUS_REJECTED", "PCK SGX extension item does not contain OID and value");
2149
+ }
2150
+ const oid = readDerOid(children[0]);
2151
+ const value = children[1];
2152
+ if (oid === INTEL_PCK_EXTENSION_PCE_ID_OID) {
2153
+ parsed.pceId = bytesToHex2(readPckChoiceOctetString(value, 2, "PCE ID"));
2154
+ } else if (oid === INTEL_PCK_EXTENSION_FMSPC_OID) {
2155
+ parsed.fmspc = bytesToHex2(readPckChoiceOctetString(value, 3, "FMSPC"));
2156
+ } else if (oid === INTEL_PCK_EXTENSION_TCB_OID) {
2157
+ const tcb = parsePckTcbExtension(value);
2158
+ parsed.sgxTcbSvns = tcb.sgxTcbSvns;
2159
+ parsed.pceSvn = tcb.pceSvn;
2160
+ parsed.cpuSvn = tcb.cpuSvn;
2161
+ }
2162
+ }
2163
+ if (parsed.fmspc === void 0 || parsed.pceId === void 0 || parsed.sgxTcbSvns === void 0 || parsed.pceSvn === void 0 || parsed.cpuSvn === void 0) {
2164
+ throw err("TCB_STATUS_REJECTED", "PCK SGX extension is missing FMSPC, PCE ID, or TCB fields");
2165
+ }
2166
+ return parsed;
2167
+ }
2168
+ function parsePckTcbExtension(value) {
2169
+ const tcbItems = pckTcbChildren(value);
2170
+ const sgxTcbSvns = [];
2171
+ let pceSvn;
2172
+ let cpuSvn;
2173
+ for (const item of tcbItems) {
2174
+ const contextTag = contextSpecificTagNumber(item);
2175
+ if (contextTag !== null) {
2176
+ if (contextTag === 16) {
2177
+ pceSvn = readContextInteger(item, "PCESVN");
2178
+ } else if (contextTag === 17) {
2179
+ cpuSvn = readContextOctetString(item, "CPUSVN");
2180
+ } else if (contextTag >= 0 && contextTag <= 15) {
2181
+ sgxTcbSvns[contextTag] = readContextInteger(item, `SGX TCB component ${contextTag + 1}`);
2182
+ }
2183
+ continue;
2184
+ }
2185
+ const children = derChildren(expectDerTag(item, 48, "PCK TCB item"));
2186
+ if (children.length !== 2) {
2187
+ throw err("TCB_STATUS_REJECTED", "PCK TCB item does not contain OID and value");
2188
+ }
2189
+ const oid = readDerOid(children[0]);
2190
+ const valueTlv = children[1];
2191
+ if (oid === INTEL_PCK_EXTENSION_PCESVN_OID) {
2192
+ pceSvn = readPckChoiceInteger(valueTlv, 16, "PCESVN");
2193
+ } else if (oid === INTEL_PCK_EXTENSION_CPUSVN_OID) {
2194
+ cpuSvn = readPckChoiceOctetString(valueTlv, 17, "CPUSVN");
2195
+ } else if (oid.startsWith(INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX)) {
2196
+ const index = Number.parseInt(
2197
+ oid.slice(INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX.length),
2198
+ 10
2199
+ );
2200
+ if (!Number.isInteger(index) || index < 1 || index > 16) {
2201
+ throw err("TCB_STATUS_REJECTED", `PCK TCB component OID ${oid} is out of range`);
2202
+ }
2203
+ sgxTcbSvns[index - 1] = readPckChoiceInteger(
2204
+ valueTlv,
2205
+ index - 1,
2206
+ `SGX TCB component ${index}`
2207
+ );
2208
+ }
2209
+ }
2210
+ if (sgxTcbSvns.length !== 16 || Array.from({ length: 16 }, (_, index) => sgxTcbSvns[index]).some((svn) => svn === void 0)) {
2211
+ throw err("TCB_STATUS_REJECTED", "PCK TCB extension is missing SGX TCB components");
2212
+ }
2213
+ if (pceSvn === void 0 || cpuSvn === void 0) {
2214
+ throw err("TCB_STATUS_REJECTED", "PCK TCB extension is missing PCESVN or CPUSVN");
2215
+ }
2216
+ if (cpuSvn.length !== 16) {
2217
+ throw err("TCB_STATUS_REJECTED", `PCK CPUSVN has ${cpuSvn.length} bytes, expected 16`);
2218
+ }
2219
+ return { sgxTcbSvns, pceSvn, cpuSvn };
2220
+ }
2221
+ function extractPckExtensionHex(cert, oid, expectedBytes, label) {
2222
+ const ext = cert.getExtension(oid);
2223
+ if (!ext) {
2224
+ throw err("TCB_STATUS_REJECTED", `PCK leaf is missing ${label} extension ${oid}`);
2225
+ }
2226
+ const decoded = unwrapDerOctetStringIfPresent(new Uint8Array(ext.value));
2227
+ if (decoded.length !== expectedBytes) {
2228
+ throw err(
2229
+ "TCB_STATUS_REJECTED",
2230
+ `PCK ${label} extension has ${decoded.length} bytes, expected ${expectedBytes}`
2231
+ );
2232
+ }
2233
+ return bytesToHex2(decoded);
2234
+ }
2235
+ function extractPckIntegerExtension(cert, oid, label) {
2236
+ const ext = cert.getExtension(oid);
2237
+ if (!ext) {
2238
+ throw err("TCB_STATUS_REJECTED", `PCK leaf is missing ${label} extension ${oid}`);
2239
+ }
2240
+ return readDerInteger(readDerTlv(new Uint8Array(ext.value), 0), label);
2241
+ }
2242
+ function readDerTlv(source, start) {
2243
+ if (start + 2 > source.length) {
2244
+ throw err("TCB_STATUS_REJECTED", "truncated DER value");
2245
+ }
2246
+ const tag = source[start];
2247
+ let offset = start + 1;
2248
+ const firstLength = source[offset++];
2249
+ let length;
2250
+ if ((firstLength & 128) === 0) {
2251
+ length = firstLength;
2252
+ } else {
2253
+ const octets = firstLength & 127;
2254
+ if (octets === 0 || octets > 4 || offset + octets > source.length) {
2255
+ throw err("TCB_STATUS_REJECTED", "unsupported DER length");
2256
+ }
2257
+ length = 0;
2258
+ for (let i = 0; i < octets; i++) {
2259
+ length = length << 8 | source[offset++];
2260
+ }
2261
+ }
2262
+ const valueStart = offset;
2263
+ const valueEnd = valueStart + length;
2264
+ if (valueEnd > source.length) {
2265
+ throw err("TCB_STATUS_REJECTED", "DER value length exceeds buffer");
2266
+ }
2267
+ return { tag, start, valueStart, valueEnd, end: valueEnd, source };
2268
+ }
2269
+ function expectDerTag(tlv, tag, label) {
2270
+ if (tlv.tag !== tag) {
2271
+ throw err("TCB_STATUS_REJECTED", `${label} has DER tag 0x${tlv.tag.toString(16)}`);
2272
+ }
2273
+ return tlv;
2274
+ }
2275
+ function derChildren(tlv) {
2276
+ const children = [];
2277
+ let offset = tlv.valueStart;
2278
+ while (offset < tlv.valueEnd) {
2279
+ const child = readDerTlv(tlv.source, offset);
2280
+ if (child.end > tlv.valueEnd) {
2281
+ throw err("TCB_STATUS_REJECTED", "DER child exceeds parent");
2282
+ }
2283
+ children.push(child);
2284
+ offset = child.end;
2285
+ }
2286
+ return children;
2287
+ }
2288
+ function readDerOid(tlv) {
2289
+ expectDerTag(tlv, 6, "OID");
2290
+ const bytes = tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
2291
+ if (bytes.length === 0) {
2292
+ throw err("TCB_STATUS_REJECTED", "empty DER OID");
2293
+ }
2294
+ const parts = [Math.floor(bytes[0] / 40), bytes[0] % 40];
2295
+ let value = 0;
2296
+ for (const byte of bytes.subarray(1)) {
2297
+ value = value << 7 | byte & 127;
2298
+ if ((byte & 128) === 0) {
2299
+ parts.push(value);
2300
+ value = 0;
2301
+ }
2302
+ }
2303
+ if (value !== 0) {
2304
+ throw err("TCB_STATUS_REJECTED", "truncated DER OID");
2305
+ }
2306
+ return parts.join(".");
2307
+ }
2308
+ function readDerInteger(tlv, label) {
2309
+ expectDerTag(tlv, 2, label);
2310
+ const bytes = tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
2311
+ if (bytes.length === 0 || (bytes[0] & 128) !== 0) {
2312
+ throw err("TCB_STATUS_REJECTED", `${label} is not a positive DER integer`);
2313
+ }
2314
+ let value = 0;
2315
+ for (const byte of bytes) value = value << 8 | byte;
2316
+ return value;
2317
+ }
2318
+ function readDerOctetString(tlv, label) {
2319
+ expectDerTag(tlv, 4, label);
2320
+ return tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
2321
+ }
2322
+ function pckTcbChildren(value) {
2323
+ if (contextSpecificTagNumber(value) === 1) {
2324
+ if ((value.tag & 32) === 0) {
2325
+ throw err("TCB_STATUS_REJECTED", "PCK TCB field is not constructed");
2326
+ }
2327
+ const children = derChildren(value);
2328
+ if (children.length === 1 && children[0].tag === 48) {
2329
+ return derChildren(children[0]);
2330
+ }
2331
+ return children;
2332
+ }
2333
+ return derChildren(expectDerTag(value, 48, "PCK TCB extension"));
2334
+ }
2335
+ function readPckChoiceInteger(tlv, expectedTag, label) {
2336
+ const contextTag = contextSpecificTagNumber(tlv);
2337
+ if (contextTag !== null) {
2338
+ if (contextTag !== expectedTag) {
2339
+ throw err(
2340
+ "TCB_STATUS_REJECTED",
2341
+ `${label} uses context tag ${contextTag}, expected ${expectedTag}`
2342
+ );
2343
+ }
2344
+ return readContextInteger(tlv, label);
2345
+ }
2346
+ return readDerInteger(tlv, label);
2347
+ }
2348
+ function readPckChoiceOctetString(tlv, expectedTag, label) {
2349
+ const contextTag = contextSpecificTagNumber(tlv);
2350
+ if (contextTag !== null) {
2351
+ if (contextTag !== expectedTag) {
2352
+ throw err(
2353
+ "TCB_STATUS_REJECTED",
2354
+ `${label} uses context tag ${contextTag}, expected ${expectedTag}`
2355
+ );
2356
+ }
2357
+ return readContextOctetString(tlv, label);
2358
+ }
2359
+ return readDerOctetString(tlv, label);
2360
+ }
2361
+ function contextSpecificTagNumber(tlv) {
2362
+ if ((tlv.tag & 192) !== 128) return null;
2363
+ const tagNumber = tlv.tag & 31;
2364
+ return tagNumber === 31 ? null : tagNumber;
2365
+ }
2366
+ function readContextInner(tlv) {
2367
+ if ((tlv.tag & 32) === 0) {
2368
+ throw err("TCB_STATUS_REJECTED", "PCK context-specific field is not constructed");
2369
+ }
2370
+ const children = derChildren(tlv);
2371
+ if (children.length !== 1) {
2372
+ throw err("TCB_STATUS_REJECTED", "PCK context-specific field has multiple values");
2373
+ }
2374
+ return children[0];
2375
+ }
2376
+ function readContextInteger(tlv, label) {
2377
+ const inner = (tlv.tag & 32) !== 0 ? readContextInner(tlv) : tlv;
2378
+ if ((tlv.tag & 32) !== 0) return readDerInteger(inner, label);
2379
+ return readDerIntegerValue(inner.source.subarray(inner.valueStart, inner.valueEnd), label);
2380
+ }
2381
+ function readContextOctetString(tlv, label) {
2382
+ const inner = (tlv.tag & 32) !== 0 ? readContextInner(tlv) : tlv;
2383
+ if ((tlv.tag & 32) !== 0) return readDerOctetString(inner, label);
2384
+ return inner.source.subarray(inner.valueStart, inner.valueEnd);
2385
+ }
2386
+ function readDerIntegerValue(bytes, label) {
2387
+ if (bytes.length === 0 || (bytes[0] & 128) !== 0) {
2388
+ throw err("TCB_STATUS_REJECTED", `${label} is not a positive DER integer`);
2389
+ }
2390
+ let value = 0;
2391
+ for (const byte of bytes) value = value << 8 | byte;
2392
+ return value;
2393
+ }
2394
+ function unwrapDerOctetStringIfPresent(bytes) {
2395
+ if (bytes.length >= 2 && bytes[0] === 4) {
2396
+ const tlv = readDerTlv(bytes, 0);
2397
+ if (tlv.end === bytes.length) return bytes.subarray(tlv.valueStart, tlv.valueEnd);
2398
+ }
2399
+ return bytes;
2400
+ }
2401
+ function hexToBytes2(hex) {
2402
+ if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex)) {
2403
+ throw err("DCAP_COLLATERAL_PARSE", "invalid hex string");
2404
+ }
2405
+ const out = new Uint8Array(hex.length / 2);
2406
+ for (let i = 0; i < out.length; i++) {
2407
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
2408
+ }
2409
+ return out;
2410
+ }
2411
+ var MAA_JKU_PATTERN = /^https:\/\/[a-z0-9-]+\.[a-z0-9]+\.attest\.azure\.net\/certs$/;
2412
+ function b64urlDecodeStr(s) {
2413
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
2414
+ return atob(padded);
2415
+ }
2416
+ function b64urlDecodeBytes(s) {
2417
+ const decoded = b64urlDecodeStr(s);
2418
+ const out = new Uint8Array(decoded.length);
2419
+ for (let i = 0; i < decoded.length; i++) out[i] = decoded.charCodeAt(i);
2420
+ return out;
2421
+ }
2422
+ async function verifyAzureMaaToken(token, policy, timeoutMs = DEFAULT_AZURE_JWKS_TIMEOUT_MS) {
2423
+ const parts = token.split(".");
2424
+ if (parts.length !== 3) {
2425
+ throw err("MAA_TOKEN_PARSE", `MAA token is not a valid JWT (${parts.length} parts)`);
2426
+ }
2427
+ const [headerB64, payloadB64, sigB64] = parts;
2428
+ let header;
2429
+ let payload;
2430
+ try {
2431
+ header = JSON.parse(b64urlDecodeStr(headerB64));
2432
+ payload = JSON.parse(b64urlDecodeStr(payloadB64));
2433
+ } catch (e) {
2434
+ throw err("MAA_TOKEN_PARSE", `failed to decode MAA JWT header/payload: ${e}`);
2435
+ }
2436
+ const exp = payload["exp"];
2437
+ if (typeof exp === "number" && Math.floor(Date.now() / 1e3) > exp) {
2438
+ throw err("MAA_TOKEN_EXPIRED", `MAA JWT expired at ${new Date(exp * 1e3).toISOString()}`);
2439
+ }
2440
+ const jku = header["jku"];
2441
+ const kid = header["kid"];
2442
+ if (!jku || !kid) {
2443
+ throw err("MAA_TOKEN_PARSE", "MAA JWT header missing jku or kid");
2444
+ }
2445
+ if (payload["iss"] !== policy.expectedIssuer) {
2446
+ throw err(
2447
+ "MAA_ISSUER_MISMATCH",
2448
+ `MAA JWT issuer ${String(payload["iss"])} does not match pinned ${policy.expectedIssuer}`
2449
+ );
2450
+ }
2451
+ if (jku !== policy.expectedJwksUrl) {
2452
+ throw err(
2453
+ "MAA_JWKS_MISMATCH",
2454
+ `MAA JWT jku ${jku} does not match pinned ${policy.expectedJwksUrl}`
2455
+ );
2456
+ }
2457
+ if (!MAA_JKU_PATTERN.test(policy.expectedJwksUrl)) {
2458
+ throw err(
2459
+ "MAA_JWKS_MISMATCH",
2460
+ `pinned MAA JWKS URL does not match Azure attestation domain: ${policy.expectedJwksUrl}`
2461
+ );
2462
+ }
2463
+ if (payload["x-ms-policy-hash"] !== policy.expectedPolicyHash) {
2464
+ throw err(
2465
+ "MAA_POLICY_HASH_MISMATCH",
2466
+ `MAA policy hash ${String(payload["x-ms-policy-hash"])} does not match pinned ${policy.expectedPolicyHash}`
2467
+ );
2468
+ }
2469
+ let jwks;
2470
+ try {
2471
+ jwks = await fetchMaaJwks(policy.expectedJwksUrl, timeoutMs);
2472
+ } catch (e) {
2473
+ throw err(
2474
+ "MAA_TOKEN_SIGNATURE",
2475
+ `failed to fetch MAA JWKS from ${policy.expectedJwksUrl}: ${e}`
2476
+ );
2477
+ }
2478
+ const jwk = jwks.keys.find((k) => k.kid === kid);
2479
+ if (!jwk) {
2480
+ throw err("MAA_TOKEN_SIGNATURE", `no key with kid=${kid} found in MAA JWKS`);
2481
+ }
2482
+ let sigPubKey;
2483
+ try {
2484
+ sigPubKey = await crypto.subtle.importKey(
2485
+ "jwk",
2486
+ { kty: "RSA", n: jwk.n, e: jwk.e, alg: "RS256", ext: true },
2487
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
2488
+ false,
2489
+ ["verify"]
2490
+ );
2491
+ } catch (e) {
2492
+ throw err("MAA_TOKEN_SIGNATURE", `failed to import MAA JWK: ${e}`);
2493
+ }
2494
+ const msg = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
2495
+ const sig = b64urlDecodeBytes(sigB64);
2496
+ const valid = await crypto.subtle.verify(
2497
+ { name: "RSASSA-PKCS1-v1_5" },
2498
+ sigPubKey,
2499
+ sig,
2500
+ msg
2501
+ );
2502
+ if (!valid) {
2503
+ throw err("MAA_TOKEN_SIGNATURE", "MAA JWT RS256 signature verification failed");
2504
+ }
2505
+ if (payload["x-ms-compliance-status"] !== "azure-compliant-cvm") {
2506
+ throw err(
2507
+ "MAA_COMPLIANCE_MISMATCH",
2508
+ `MAA compliance status: "${payload["x-ms-compliance-status"]}" (expected "azure-compliant-cvm")`
2509
+ );
2510
+ }
2511
+ if (payload["tdx_td_attributes_debug"] !== false) {
2512
+ throw err("MAA_DEBUG_BIT_SET", "MAA token reports TDX debug bit set");
2513
+ }
2514
+ const reportData = payload["tdx_report_data"];
2515
+ if (typeof reportData !== "string" || !/^[0-9a-f]{128}$/i.test(reportData)) {
2516
+ throw err("MAA_REPORTDATA_MISMATCH", "MAA token missing valid 64-byte tdx_report_data claim");
2517
+ }
2518
+ return {
2519
+ reportData: reportData.toLowerCase(),
2520
+ mrtd: readMaaMeasurement(payload, "tdx_mrtd"),
2521
+ rtmr0: readMaaMeasurement(payload, "tdx_rtmr0"),
2522
+ rtmr1: readMaaMeasurement(payload, "tdx_rtmr1"),
2523
+ rtmr2: readMaaMeasurement(payload, "tdx_rtmr2"),
2524
+ rtmr3: readMaaMeasurement(payload, "tdx_rtmr3")
2525
+ };
2526
+ }
2527
+ function readMaaMeasurement(payload, claim) {
2528
+ const value = payload[claim];
2529
+ if (typeof value !== "string" || !/^[0-9a-f]{96}$/i.test(value)) {
2530
+ throw err("MAA_REPORTDATA_MISMATCH", `MAA token missing valid ${claim} claim`);
2531
+ }
2532
+ return value.toLowerCase();
2533
+ }
2534
+ function assertMaaMeasurementsMatchDcap(maa, dcap) {
2535
+ const fields = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
2536
+ for (const field of fields) {
2537
+ if (maa[field] !== dcap[field]) {
2538
+ throw err(
2539
+ "MAA_REPORTDATA_MISMATCH",
2540
+ `MAA ${field} ${maa[field]} does not match DCAP quote ${field.toUpperCase()} ${dcap[field]}`
2541
+ );
2542
+ }
2543
+ }
2544
+ }
2545
+ async function fetchMaaJwks(url, timeoutMs) {
2546
+ const boundedTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_AZURE_JWKS_TIMEOUT_MS;
2547
+ const controller = new AbortController();
2548
+ let timeoutId = null;
2549
+ const timeout = new Promise((_, reject) => {
2550
+ timeoutId = setTimeout(() => {
2551
+ controller.abort();
2552
+ reject(new Error(`timed out after ${boundedTimeoutMs}ms`));
2553
+ }, boundedTimeoutMs);
2554
+ });
2555
+ try {
2556
+ return await Promise.race([
2557
+ (async () => {
2558
+ const resp = await fetch(url, { signal: controller.signal });
2559
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
2560
+ return await resp.json();
2561
+ })(),
2562
+ timeout
2563
+ ]);
2564
+ } finally {
2565
+ if (timeoutId !== null) clearTimeout(timeoutId);
2566
+ }
2567
+ }
2568
+ async function verifyTpmQuoteNonce(tpmQuoteB64, akPubKey, expectedNonce, expectedNoiseStaticPub, expectedBinaryHash) {
2569
+ let raw;
2570
+ try {
2571
+ raw = base64ToBytes(tpmQuoteB64);
2572
+ } catch (e) {
2573
+ throw err("TPM_QUOTE_SIGNATURE", `failed to decode tpm_quote: ${e}`);
2574
+ }
2575
+ if (raw.length < 8) {
2576
+ throw err("TPM_QUOTE_SIGNATURE", "tpm_quote too short");
2577
+ }
2578
+ const msgLen = new DataView(raw.buffer, raw.byteOffset).getUint32(
2579
+ 0,
2580
+ true
2581
+ /* little-endian */
2582
+ );
2583
+ if (4 + msgLen > raw.length) {
2584
+ throw err("TPM_QUOTE_SIGNATURE", `tpm_quote msg_len=${msgLen} exceeds buffer`);
2585
+ }
2586
+ const tpmsAttest = raw.slice(4, 4 + msgLen);
2587
+ const sigBlob = raw.slice(4 + msgLen);
2588
+ if (sigBlob.length < 6) {
2589
+ throw err("TPM_QUOTE_SIGNATURE", "TPMT_SIGNATURE too short");
2590
+ }
2591
+ const sigSize = sigBlob[4] << 8 | sigBlob[5];
2592
+ const sigBytes = sigBlob.slice(6, 6 + sigSize);
2593
+ if (sigBytes.length !== sigSize) {
2594
+ throw err(
2595
+ "TPM_QUOTE_SIGNATURE",
2596
+ `TPMT_SIGNATURE truncated: need ${sigSize}, have ${sigBytes.length}`
2597
+ );
2598
+ }
2599
+ const valid = await crypto.subtle.verify(
2600
+ { name: "RSASSA-PKCS1-v1_5" },
2601
+ akPubKey,
2602
+ sigBytes,
2603
+ tpmsAttest
2604
+ );
2605
+ if (!valid) {
2606
+ throw err("TPM_QUOTE_SIGNATURE", "TPM AK RSA signature verification failed");
2607
+ }
2608
+ const qdInput = new Uint8Array(
2609
+ expectedNonce.length + expectedNoiseStaticPub.length + expectedBinaryHash.length
2610
+ );
2611
+ qdInput.set(expectedNonce, 0);
2612
+ qdInput.set(expectedNoiseStaticPub, expectedNonce.length);
2613
+ qdInput.set(expectedBinaryHash, expectedNonce.length + expectedNoiseStaticPub.length);
2614
+ const expectedQd = new Uint8Array(await crypto.subtle.digest("SHA-256", qdInput));
2615
+ const extraData = readTpmsAttestExtraData(tpmsAttest);
2616
+ if (!constantTimeEq(extraData, expectedQd)) {
2617
+ throw err(
2618
+ "TPM_NONCE_MISMATCH",
2619
+ "TPMS_ATTEST extraData does not equal SHA-256(nonce || noise_pubkey || binary_hash)"
2620
+ );
2621
+ }
2622
+ }
2623
+ function readTpmsAttestExtraData(tpmsAttest) {
2624
+ if (tpmsAttest.length < 10) {
2625
+ throw err("TPM_QUOTE_SIGNATURE", "TPMS_ATTEST too short");
2626
+ }
2627
+ const view = new DataView(tpmsAttest.buffer, tpmsAttest.byteOffset, tpmsAttest.byteLength);
2628
+ const magic = view.getUint32(0, false);
2629
+ if (magic !== TPM_GENERATED_VALUE) {
2630
+ throw err(
2631
+ "TPM_QUOTE_SIGNATURE",
2632
+ `TPMS_ATTEST magic ${magic.toString(16)} is not TPM_GENERATED_VALUE`
2633
+ );
2634
+ }
2635
+ const attestType = view.getUint16(4, false);
2636
+ if (attestType !== TPM_ST_ATTEST_QUOTE) {
2637
+ throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST type ${attestType.toString(16)} is not quote`);
2638
+ }
2639
+ let offset = 6;
2640
+ offset = readTpm2b(tpmsAttest, offset, "qualifiedSigner").nextOffset;
2641
+ return readTpm2b(tpmsAttest, offset, "extraData").data;
2642
+ }
2643
+ function readTpm2b(bytes, offset, label) {
2644
+ if (offset + 2 > bytes.length) {
2645
+ throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST truncated before ${label} size`);
2646
+ }
2647
+ const size = bytes[offset] << 8 | bytes[offset + 1];
2648
+ const start = offset + 2;
2649
+ const end = start + size;
2650
+ if (end > bytes.length) {
2651
+ throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST truncated in ${label}`);
2652
+ }
2653
+ return { data: bytes.slice(start, end), nextOffset: end };
2654
+ }
2655
+ function base64ToBytes(b64) {
2656
+ const normalized = b64.replace(/-/g, "+").replace(/_/g, "/");
2657
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2658
+ const binaryStr = atob(padded);
2659
+ const out = new Uint8Array(binaryStr.length);
2660
+ for (let i = 0; i < binaryStr.length; i++) out[i] = binaryStr.charCodeAt(i);
2661
+ return out;
2662
+ }
2663
+ async function verifyAzureTdxAttestation(params) {
2664
+ const { raw, response, expectedNonce, expectedNoiseStaticPub, policy, timeoutMs } = params;
2665
+ const body = raw.subarray(BODY_OFFSET, BODY_OFFSET + BODY_SIZE);
2666
+ const dcapMeasurements = parseMeasurements(body);
2667
+ const reportData = body.subarray(
2668
+ BODY_REPORT_DATA_OFF,
2669
+ BODY_REPORT_DATA_OFF + BODY_REPORT_DATA_LEN
2670
+ );
2671
+ await verifyDcapQuoteAndCollateral({ raw, body, response, policy });
2672
+ assertDebugBitClear(body);
2673
+ if (!response.maa_token) {
2674
+ throw err("MAA_TOKEN_MISSING", "azure-tdx-dcap response missing maa_token");
2675
+ }
2676
+ if (!policy.azureMaa) {
2677
+ throw err(
2678
+ "MAA_TRUST_POLICY_MISSING",
2679
+ "azure-tdx-dcap attestation requires pinned MAA issuer, JWKS URL, and policy hash"
2680
+ );
2681
+ }
2682
+ const measurements = await verifyAzureMaaToken(response.maa_token, policy.azureMaa, timeoutMs);
2683
+ const reportDataHex = bytesToHex2(reportData);
2684
+ if (measurements.reportData !== reportDataHex) {
2685
+ throw err(
2686
+ "MAA_REPORTDATA_MISMATCH",
2687
+ `MAA tdx_report_data ${measurements.reportData} does not match DCAP REPORTDATA ${reportDataHex}`
2688
+ );
2689
+ }
2690
+ assertMaaMeasurementsMatchDcap(measurements, dcapMeasurements);
2691
+ assertRuntimeMeasurementsMatch(dcapMeasurements, policy);
2692
+ if (!response.hcl_keys_json) {
2693
+ throw err("HCL_KEYS_MISSING", "azure-tdx-dcap response missing hcl_keys_json");
2694
+ }
2695
+ let hclKeysBytes;
2696
+ try {
2697
+ hclKeysBytes = base64ToBytes(response.hcl_keys_json);
2698
+ } catch (e) {
2699
+ throw err("HCL_KEYS_PARSE", `failed to decode hcl_keys_json base64: ${e}`);
2700
+ }
2701
+ const reportData0_32 = reportData.subarray(0, 32);
2702
+ const hclKeysHash = new Uint8Array(
2703
+ await crypto.subtle.digest("SHA-256", hclKeysBytes)
2704
+ );
2705
+ if (!constantTimeEq(hclKeysHash, reportData0_32)) {
2706
+ throw err(
2707
+ "HCL_KEYS_HASH_MISMATCH",
2708
+ "SHA-256(hcl_keys_json) does not match DCAP quote REPORTDATA[0:32]"
2709
+ );
2710
+ }
2711
+ let hclKeys;
2712
+ try {
2713
+ hclKeys = JSON.parse(new TextDecoder().decode(hclKeysBytes));
2714
+ } catch (e) {
2715
+ throw err("HCL_KEYS_PARSE", `failed to parse hcl_keys_json: ${e}`);
2716
+ }
2717
+ const keysArray = hclKeys["keys"];
2718
+ const akJwk = Array.isArray(keysArray) ? keysArray.find((k) => k["kid"] === "HCLAkPub") : void 0;
2719
+ if (!akJwk) {
2720
+ throw err("HCL_KEYS_PARSE", 'hcl_keys_json does not contain a key with kid="HCLAkPub"');
2721
+ }
2722
+ let akPubKey;
2723
+ try {
2724
+ akPubKey = await crypto.subtle.importKey(
2725
+ "jwk",
2726
+ { kty: "RSA", n: akJwk["n"], e: akJwk["e"], ext: true },
2727
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
2728
+ false,
2729
+ ["verify"]
2730
+ );
2731
+ } catch (e) {
2732
+ throw err("HCL_KEYS_PARSE", `failed to import HCLAkPub from hcl_keys_json: ${e}`);
2733
+ }
2734
+ if (!response.tpm_quote) {
2735
+ throw err("TPM_QUOTE_MISSING", "azure-tdx-dcap response missing tpm_quote");
2736
+ }
2737
+ if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
2738
+ throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
2739
+ }
2740
+ if (response.mode === "prod" && policy.expectedBinaryHash === void 0) {
2741
+ throw err(
2742
+ "BINARY_HASH_MISMATCH",
2743
+ "production Azure attestation requires a pinned coordinator binary hash"
2744
+ );
2745
+ }
2746
+ if (policy.expectedBinaryHash !== void 0 && response.binary_hash.toLowerCase() !== policy.expectedBinaryHash.toLowerCase()) {
2747
+ throw err(
2748
+ "BINARY_HASH_MISMATCH",
2749
+ `binary hash ${response.binary_hash.toLowerCase()} does not match the approved release pin`
2750
+ );
2751
+ }
2752
+ await verifyTpmQuoteNonce(
2753
+ response.tpm_quote,
2754
+ akPubKey,
2755
+ expectedNonce,
2756
+ expectedNoiseStaticPub,
2757
+ hexToBytes2(response.binary_hash)
2758
+ );
2759
+ if (response.nonce.toLowerCase() !== bytesToHex2(expectedNonce)) {
2760
+ throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
2761
+ }
2762
+ if (response.pubkey.toLowerCase() !== bytesToHex2(expectedNoiseStaticPub)) {
2763
+ throw err(
2764
+ "PUBKEY_FIELD_MISMATCH",
2765
+ "attestation pubkey field does not match Noise static pubkey from handshake"
2766
+ );
2767
+ }
2768
+ if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
2769
+ throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
2770
+ }
2771
+ if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
2772
+ throw err(
2773
+ "MODE_MISMATCH",
2774
+ `attestation mode ${response.mode} does not match required ${policy.requiredMode}`
2775
+ );
2776
+ }
2777
+ if (response.mode === "prod") {
2778
+ if ((policy.expectedMrtd === void 0 || policy.expectedMrtd === null) && policy.allowUnpinnedProdMrtdForRelease !== true) {
2779
+ throw err(
2780
+ "MRTD_MISMATCH",
2781
+ "production attestation requires a pinned MRTD but none was configured"
2782
+ );
2783
+ }
2784
+ if (policy.expectedMrtd !== void 0 && policy.expectedMrtd !== null && dcapMeasurements.mrtd !== policy.expectedMrtd.toLowerCase()) {
2785
+ throw err(
2786
+ "MRTD_MISMATCH",
2787
+ `MRTD ${dcapMeasurements.mrtd} does not match pinned ${policy.expectedMrtd.toLowerCase()}`
2788
+ );
2789
+ }
2790
+ }
2791
+ return {
2792
+ mode: response.mode,
2793
+ binaryHash: response.binary_hash,
2794
+ runtimeProfile: response.runtime_profile,
2795
+ mrtd: dcapMeasurements.mrtd,
2796
+ rtmr0: dcapMeasurements.rtmr0,
2797
+ rtmr1: dcapMeasurements.rtmr1,
2798
+ rtmr2: dcapMeasurements.rtmr2,
2799
+ rtmr3: dcapMeasurements.rtmr3,
2800
+ pubkeyHex: response.pubkey.toLowerCase(),
2801
+ localAttestation: false
2802
+ };
2803
+ }
2804
+ function verifyLocalAttestation(params) {
2805
+ const { response, expectedNonce, expectedNoiseStaticPub, policy } = params;
2806
+ if (policy.allowLocalAttestation !== true) {
2807
+ throw err(
2808
+ "LOCAL_ATTESTATION_DISABLED",
2809
+ "local attestation response received but local attestation is not enabled"
2810
+ );
2811
+ }
2812
+ if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
2813
+ throw err(
2814
+ "MODE_MISMATCH",
2815
+ `attestation mode ${response.mode} does not match required ${policy.requiredMode}`
2816
+ );
2817
+ }
2818
+ if (response.mode !== "dev") {
2819
+ throw err(
2820
+ "LOCAL_ATTESTATION_DISABLED",
2821
+ "local attestation is only accepted for dev mode responses"
2822
+ );
2823
+ }
2824
+ if (response.nonce.toLowerCase() !== bytesToHex2(expectedNonce)) {
2825
+ throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
2826
+ }
2827
+ if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
2828
+ throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
2829
+ }
2830
+ const expectedPubHex = bytesToHex2(expectedNoiseStaticPub);
2831
+ if (response.pubkey.toLowerCase() !== expectedPubHex) {
2832
+ throw err(
2833
+ "PUBKEY_FIELD_MISMATCH",
2834
+ "local attestation pubkey field does not match Noise static pubkey from handshake"
2835
+ );
2836
+ }
2837
+ return {
2838
+ mode: response.mode,
2839
+ binaryHash: response.binary_hash.toLowerCase(),
2840
+ runtimeProfile: response.runtime_profile,
2841
+ mrtd: "local-dev-no-mrtd",
2842
+ rtmr0: "local-dev-no-rtmr0",
2843
+ rtmr1: "local-dev-no-rtmr1",
2844
+ rtmr2: "local-dev-no-rtmr2",
2845
+ rtmr3: "local-dev-no-rtmr3",
2846
+ pubkeyHex: response.pubkey.toLowerCase(),
2847
+ localAttestation: true
2848
+ };
2849
+ }
2850
+ function decodeQuoteB64(b64) {
2851
+ let bin;
2852
+ try {
2853
+ bin = atob(b64);
2854
+ } catch (e) {
2855
+ throw err("QUOTE_DECODE", `quote field is not valid base64: ${e}`);
2856
+ }
2857
+ const out = new Uint8Array(bin.length);
2858
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
2859
+ return out;
2860
+ }
2861
+ function ensureStructure(raw) {
2862
+ if (raw.length < HEADER_SIZE + BODY_SIZE + 4) {
2863
+ throw err(
2864
+ "QUOTE_STRUCTURE",
2865
+ `quote too short: ${raw.length} bytes (need >= ${HEADER_SIZE + BODY_SIZE + 4})`
2866
+ );
2867
+ }
2868
+ const version = readU16LE(raw, HEADER_VERSION_OFF);
2869
+ const attKey = readU16LE(raw, HEADER_ATT_KEY_OFF);
2870
+ const teeType = readU32LE(raw, HEADER_TEE_TYPE_OFF);
2871
+ if (version !== 4) {
2872
+ throw err("QUOTE_STRUCTURE", `unsupported quote version ${version}`);
2873
+ }
2874
+ if (teeType !== TDX_TEE_TYPE) {
2875
+ throw err("QUOTE_STRUCTURE", `not a TDX quote (TEE type 0x${teeType.toString(16)})`);
2876
+ }
2877
+ if (attKey !== ECDSA_P256_KEY_TYPE) {
2878
+ throw err(
2879
+ "QUOTE_STRUCTURE",
2880
+ `unsupported attestation key type ${attKey} (expected ECDSA-P256)`
2881
+ );
2882
+ }
2883
+ }
2884
+ function readSigData(raw) {
2885
+ const sigDataLen = readU32LE(raw, SIG_DATA_LEN_OFFSET);
2886
+ const sigStart = SIG_DATA_LEN_OFFSET + 4;
2887
+ if (raw.length < sigStart + sigDataLen) {
2888
+ throw err("QUOTE_STRUCTURE", "quote truncated in signature data");
2889
+ }
2890
+ return raw.subarray(sigStart, sigStart + sigDataLen);
2891
+ }
2892
+ function parseMeasurements(body) {
2893
+ const m = (off) => bytesToHex2(body.subarray(off, off + MEASUREMENT_LEN));
2894
+ return {
2895
+ mrtd: m(BODY_MRTD_OFF),
2896
+ rtmr0: m(BODY_RTMR0_OFF),
2897
+ rtmr1: m(BODY_RTMR1_OFF),
2898
+ rtmr2: m(BODY_RTMR2_OFF),
2899
+ rtmr3: m(BODY_RTMR3_OFF),
2900
+ tdAttr: bytesToHex2(body.subarray(BODY_TD_ATTR_OFF, BODY_TD_ATTR_OFF + 8))
2901
+ };
2902
+ }
2903
+ var PEM_BEGIN = "-----BEGIN CERTIFICATE-----";
2904
+ var PEM_END = "-----END CERTIFICATE-----";
2905
+ var PEM_BEGIN_BYTES = new TextEncoder().encode(PEM_BEGIN);
2906
+ var PEM_END_BYTES = new TextEncoder().encode(PEM_END);
2907
+ function findBytes(hay, needle, from = 0) {
2908
+ if (needle.length === 0) return from;
2909
+ const last = hay.length - needle.length;
2910
+ outer: for (let i = from; i <= last; i++) {
2911
+ for (let j = 0; j < needle.length; j++) {
2912
+ if (hay[i + j] !== needle[j]) continue outer;
2913
+ }
2914
+ return i;
2915
+ }
2916
+ return -1;
2917
+ }
2918
+ function parseQeCertificationData(sigData) {
2919
+ if (sigData.length < SIG_QE_CERT_DATA_OFF + SIG_QE_CERT_DATA_HEADER_SIZE) {
2920
+ throw err("QUOTE_STRUCTURE", "signature data truncated before QE certification data header");
2921
+ }
2922
+ const outerCertType = readU16LE(sigData, SIG_QE_CERT_DATA_OFF);
2923
+ const outerCertSize = readU32LE(sigData, SIG_QE_CERT_DATA_OFF + 2);
2924
+ if (outerCertType !== QE_CERT_TYPE_QE_REPORT_CERT_DATA) {
2925
+ throw err(
2926
+ "QUOTE_STRUCTURE",
2927
+ `unexpected QE certification data type ${outerCertType} (expected ${QE_CERT_TYPE_QE_REPORT_CERT_DATA})`
2928
+ );
2929
+ }
2930
+ const outerDataStart = SIG_QE_CERT_DATA_OFF + SIG_QE_CERT_DATA_HEADER_SIZE;
2931
+ const outerDataEnd = outerDataStart + outerCertSize;
2932
+ if (sigData.length < outerDataEnd) {
2933
+ throw err("QUOTE_STRUCTURE", "signature data truncated in outer QE certification data");
2934
+ }
2935
+ const outerData = sigData.subarray(outerDataStart, outerDataEnd);
2936
+ const qeReportSigOff = SIG_QE_REPORT_SIZE;
2937
+ const qeAuthSizeOff = qeReportSigOff + SIG_QE_REPORT_SIG_SIZE;
2938
+ const qeAuthDataOff = qeAuthSizeOff + 2;
2939
+ if (outerData.length < qeAuthDataOff + SIG_QE_CERT_DATA_HEADER_SIZE) {
2940
+ throw err(
2941
+ "QUOTE_STRUCTURE",
2942
+ "signature data truncated before QE auth_data and nested certification data"
2943
+ );
2944
+ }
2945
+ const qeReport = outerData.subarray(0, SIG_QE_REPORT_SIZE);
2946
+ const qeReportSig = outerData.subarray(qeReportSigOff, qeReportSigOff + SIG_QE_REPORT_SIG_SIZE);
2947
+ const authSize = readU16LE(outerData, qeAuthSizeOff);
2948
+ const qeAuthDataEnd = qeAuthDataOff + authSize;
2949
+ if (outerData.length < qeAuthDataEnd + SIG_QE_CERT_DATA_HEADER_SIZE) {
2950
+ throw err("QUOTE_STRUCTURE", "signature data truncated in QE auth_data");
2951
+ }
2952
+ const qeAuthData = outerData.subarray(qeAuthDataOff, qeAuthDataEnd);
2953
+ const nestedCertType = readU16LE(outerData, qeAuthDataEnd);
2954
+ const nestedCertSize = readU32LE(outerData, qeAuthDataEnd + 2);
2955
+ if (nestedCertType !== QE_CERT_TYPE_PCK_CHAIN) {
2956
+ throw err(
2957
+ "PCK_CHAIN_MISSING",
2958
+ `cert data type ${nestedCertType} is not PCK chain (expected ${QE_CERT_TYPE_PCK_CHAIN})`
2959
+ );
2960
+ }
2961
+ const nestedDataEnd = qeAuthDataEnd + SIG_QE_CERT_DATA_HEADER_SIZE + nestedCertSize;
2962
+ if (outerData.length < nestedDataEnd) {
2963
+ throw err("QUOTE_STRUCTURE", "signature data truncated in nested QE certification data");
2964
+ }
2965
+ return {
2966
+ qeReport,
2967
+ qeReportSig,
2968
+ qeAuthData,
2969
+ qeCertificationData: outerData.subarray(qeAuthDataEnd, nestedDataEnd)
2970
+ };
2971
+ }
2972
+ function extractPckChain(qeCertificationData) {
2973
+ if (qeCertificationData.length < SIG_QE_CERT_DATA_HEADER_SIZE) {
2974
+ throw err("PCK_CHAIN_PARSE", "nested QE certification data header truncated");
2975
+ }
2976
+ const certType = readU16LE(qeCertificationData, 0);
2977
+ const certSize = readU32LE(qeCertificationData, 2);
2978
+ if (certType !== QE_CERT_TYPE_PCK_CHAIN) {
2979
+ throw err(
2980
+ "PCK_CHAIN_MISSING",
2981
+ `cert data type ${certType} is not PCK chain (expected ${QE_CERT_TYPE_PCK_CHAIN})`
2982
+ );
2983
+ }
2984
+ if (qeCertificationData.length < SIG_QE_CERT_DATA_HEADER_SIZE + certSize) {
2985
+ throw err("PCK_CHAIN_PARSE", "nested QE certification data truncated");
2986
+ }
2987
+ const certBytes = qeCertificationData.subarray(
2988
+ SIG_QE_CERT_DATA_HEADER_SIZE,
2989
+ SIG_QE_CERT_DATA_HEADER_SIZE + certSize
2990
+ );
2991
+ const certs = [];
2992
+ let cursor = 0;
2993
+ while (cursor < certBytes.length) {
2994
+ const begin = findBytes(certBytes, PEM_BEGIN_BYTES, cursor);
2995
+ if (begin === -1) break;
2996
+ const endTag = findBytes(certBytes, PEM_END_BYTES, begin);
2997
+ if (endTag === -1) {
2998
+ throw err("PCK_CHAIN_PARSE", "unterminated PEM block");
2999
+ }
3000
+ const end = endTag + PEM_END_BYTES.length;
3001
+ const pem = new TextDecoder("utf-8").decode(certBytes.subarray(begin, end));
3002
+ try {
3003
+ certs.push(new x509.X509Certificate(pem));
3004
+ } catch (e) {
3005
+ throw err("PCK_CHAIN_PARSE", `failed to parse PEM cert: ${e}`);
3006
+ }
3007
+ cursor = end;
3008
+ }
3009
+ if (certs.length === 0) {
3010
+ throw err("PCK_CHAIN_MISSING", "no parseable certs in PCK chain");
3011
+ }
3012
+ if (certs.length < 3) {
3013
+ throw err(
3014
+ "PCK_CHAIN_MISSING",
3015
+ `PCK chain has only ${certs.length} cert(s); Intel DCAP requires leaf + CA + Root (min 3)`
3016
+ );
3017
+ }
3018
+ return certs;
3019
+ }
3020
+ async function verifyChainSignatures(chain) {
3021
+ const now = /* @__PURE__ */ new Date();
3022
+ for (let i = 0; i < chain.length - 1; i++) {
3023
+ const cert = chain[i];
3024
+ if (now < cert.notBefore) {
3025
+ throw err(
3026
+ "PCK_CHAIN_SIGNATURE",
3027
+ `cert[${i}] (${cert.subject}) is not yet valid (notBefore=${cert.notBefore.toISOString()})`
3028
+ );
3029
+ }
3030
+ if (now > cert.notAfter) {
3031
+ throw err(
3032
+ "PCK_CHAIN_SIGNATURE",
3033
+ `cert[${i}] (${cert.subject}) has expired (notAfter=${cert.notAfter.toISOString()})`
3034
+ );
3035
+ }
3036
+ let ok;
3037
+ try {
3038
+ ok = await chain[i].verify({ publicKey: chain[i + 1].publicKey });
3039
+ } catch (e) {
3040
+ throw err(
3041
+ "PCK_CHAIN_SIGNATURE",
3042
+ `cert[${i}] (${chain[i].subject}) signature verification threw: ${e}`
3043
+ );
3044
+ }
3045
+ if (!ok) {
3046
+ throw err(
3047
+ "PCK_CHAIN_SIGNATURE",
3048
+ `cert[${i}] (${chain[i].subject}) signature does not verify against issuer cert[${i + 1}]`
3049
+ );
3050
+ }
3051
+ }
3052
+ const root = chain[chain.length - 1];
3053
+ if (now < root.notBefore) {
3054
+ throw err(
3055
+ "PCK_CHAIN_SIGNATURE",
3056
+ `root cert (${root.subject}) is not yet valid (notBefore=${root.notBefore.toISOString()})`
3057
+ );
3058
+ }
3059
+ if (now > root.notAfter) {
3060
+ throw err(
3061
+ "PCK_CHAIN_SIGNATURE",
3062
+ `root cert (${root.subject}) has expired (notAfter=${root.notAfter.toISOString()})`
3063
+ );
3064
+ }
3065
+ let rootOk;
3066
+ try {
3067
+ rootOk = await root.verify({ publicKey: root.publicKey });
3068
+ } catch (e) {
3069
+ throw err(
3070
+ "PCK_CHAIN_SIGNATURE",
3071
+ `root cert (${root.subject}) self-signature verification threw: ${e}`
3072
+ );
3073
+ }
3074
+ if (!rootOk) {
3075
+ throw err("PCK_CHAIN_SIGNATURE", `root cert (${root.subject}) is not self-signed`);
3076
+ }
3077
+ for (let i = 1; i < chain.length; i++) {
3078
+ const bc = chain[i].getExtension(x509.BasicConstraintsExtension);
3079
+ if (!bc || !bc.ca) {
3080
+ throw err(
3081
+ "PCK_CHAIN_SIGNATURE",
3082
+ `cert[${i}] (${chain[i].subject}) is not a CA cert (BasicConstraints.cA is not true)`
3083
+ );
3084
+ }
3085
+ }
3086
+ }
3087
+ async function leafSpki(leaf) {
3088
+ const key = await leaf.publicKey.export();
3089
+ const spki = await crypto.subtle.exportKey("spki", key);
3090
+ return new Uint8Array(spki);
3091
+ }
3092
+ function rawXyToSpki(xy) {
3093
+ if (xy.length !== 64) {
3094
+ throw err("QUOTE_STRUCTURE", `attestation pubkey must be 64 bytes (got ${xy.length})`);
3095
+ }
3096
+ const uncompressed = new Uint8Array(65);
3097
+ uncompressed[0] = 4;
3098
+ uncompressed.set(xy, 1);
3099
+ const prefix = new Uint8Array([
3100
+ 48,
3101
+ 89,
3102
+ 48,
3103
+ 19,
3104
+ 6,
3105
+ 7,
3106
+ 42,
3107
+ 134,
3108
+ 72,
3109
+ 206,
3110
+ 61,
3111
+ 2,
3112
+ 1,
3113
+ 6,
3114
+ 8,
3115
+ 42,
3116
+ 134,
3117
+ 72,
3118
+ 206,
3119
+ 61,
3120
+ 3,
3121
+ 1,
3122
+ 7,
3123
+ 3,
3124
+ 66,
3125
+ 0
3126
+ ]);
3127
+ return concat(prefix, uncompressed);
3128
+ }
3129
+ async function verifyP256Signature(spki, rsSig, data, failureReason, what) {
3130
+ if (rsSig.length !== 64) {
3131
+ throw err(failureReason, `${what}: signature must be 64 bytes (r||s)`);
3132
+ }
3133
+ let key;
3134
+ try {
3135
+ key = await crypto.subtle.importKey(
3136
+ "spki",
3137
+ spki,
3138
+ { name: "ECDSA", namedCurve: "P-256" },
3139
+ false,
3140
+ ["verify"]
3141
+ );
3142
+ } catch (e) {
3143
+ throw err(failureReason, `${what}: failed to import key: ${e}`);
3144
+ }
3145
+ const ok = await crypto.subtle.verify(
3146
+ { name: "ECDSA", hash: "SHA-256" },
3147
+ key,
3148
+ rsSig,
3149
+ data
3150
+ );
3151
+ if (!ok) {
3152
+ throw err(failureReason, `${what}: ECDSA verification failed`);
3153
+ }
3154
+ }
3155
+ async function sha256(data) {
3156
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
3157
+ }
3158
+ async function sha256Hex(data) {
3159
+ return bytesToHex2(await sha256(data));
3160
+ }
3161
+ function readU16LE(buf, off) {
3162
+ return buf[off] | buf[off + 1] << 8;
3163
+ }
3164
+ function readU32LE(buf, off) {
3165
+ return (buf[off] | buf[off + 1] << 8 | buf[off + 2] << 16 | buf[off + 3] << 24) >>> 0;
3166
+ }
3167
+ function constantTimeEq(a, b) {
3168
+ if (a.length !== b.length) return false;
3169
+ let diff = 0;
3170
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
3171
+ return diff === 0;
3172
+ }
3173
+ function concat(a, b) {
3174
+ const out = new Uint8Array(a.length + b.length);
3175
+ out.set(a, 0);
3176
+ out.set(b, a.length);
3177
+ return out;
3178
+ }
3179
+ function bytesToHex2(b) {
3180
+ let s = "";
3181
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
3182
+ return s;
3183
+ }
3184
+ var PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256";
3185
+ function noiseNonce(n) {
3186
+ const buf = new Uint8Array(12);
3187
+ const view = new DataView(buf.buffer);
3188
+ view.setUint32(4, n & 4294967295, true);
3189
+ view.setUint32(8, Math.floor(n / 4294967296) & 4294967295, true);
3190
+ return buf;
3191
+ }
3192
+ function hkdf2(ck, ikm) {
3193
+ const mac1 = new HMAC(SHA256, ck);
3194
+ mac1.update(ikm);
3195
+ const temp = mac1.digest();
3196
+ const mac2 = new HMAC(SHA256, temp);
3197
+ mac2.update(new Uint8Array([1]));
3198
+ const out1 = mac2.digest();
3199
+ const mac3 = new HMAC(SHA256, temp);
3200
+ mac3.update(out1);
3201
+ mac3.update(new Uint8Array([2]));
3202
+ const out2 = mac3.digest();
3203
+ return [out1, out2];
3204
+ }
3205
+ function createSymState() {
3206
+ const proto = new TextEncoder().encode(PROTOCOL_NAME);
3207
+ const h0 = new Uint8Array(32);
3208
+ h0.set(proto);
3209
+ const ck0 = new Uint8Array(32);
3210
+ ck0.set(h0);
3211
+ const ss = {
3212
+ h: h0,
3213
+ ck: ck0,
3214
+ k: null,
3215
+ n: 0,
3216
+ mixHash(data) {
3217
+ const combined = new Uint8Array(this.h.length + data.length);
3218
+ combined.set(this.h);
3219
+ combined.set(data, this.h.length);
3220
+ this.h = hash(combined);
3221
+ },
3222
+ mixKey(ikm) {
3223
+ const [newCk, newK] = hkdf2(this.ck, ikm);
3224
+ this.ck = newCk;
3225
+ this.k = newK;
3226
+ this.n = 0;
3227
+ },
3228
+ encryptAndHash(plaintext) {
3229
+ if (!this.k) {
3230
+ this.mixHash(plaintext);
3231
+ return new Uint8Array(plaintext);
3232
+ }
3233
+ const aead = new ChaCha20Poly1305(this.k);
3234
+ const ct = aead.seal(noiseNonce(this.n), plaintext, this.h);
3235
+ this.n++;
3236
+ this.mixHash(ct);
3237
+ return ct;
3238
+ },
3239
+ decryptAndHash(ciphertext) {
3240
+ if (!this.k) {
3241
+ this.mixHash(ciphertext);
3242
+ return new Uint8Array(ciphertext);
3243
+ }
3244
+ const aead = new ChaCha20Poly1305(this.k);
3245
+ const pt = aead.open(noiseNonce(this.n), ciphertext, this.h);
3246
+ if (!pt) throw new Error("Noise: AEAD decryption failed");
3247
+ this.n++;
3248
+ this.mixHash(ciphertext);
3249
+ return pt;
3250
+ },
3251
+ split() {
3252
+ const [k1, k2] = hkdf2(this.ck, new Uint8Array(0));
3253
+ return [k1, k2];
3254
+ }
3255
+ };
3256
+ ss.mixHash(new Uint8Array(0));
3257
+ return ss;
3258
+ }
3259
+ function createNoiseTransport(sendKey, recvKey) {
3260
+ let sendN = 0;
3261
+ let recvN = 0;
3262
+ return {
3263
+ encrypt(plaintext) {
3264
+ const aead = new ChaCha20Poly1305(sendKey);
3265
+ const ct = aead.seal(noiseNonce(sendN), plaintext, void 0);
3266
+ sendN++;
3267
+ return ct;
3268
+ },
3269
+ decrypt(ciphertext) {
3270
+ const aead = new ChaCha20Poly1305(recvKey);
3271
+ const pt = aead.open(noiseNonce(recvN), ciphertext, void 0);
3272
+ if (!pt) throw new Error("Noise: transport decryption failed");
3273
+ recvN++;
3274
+ return pt;
3275
+ }
3276
+ };
3277
+ }
3278
+ function createNoiseXXInitiator() {
3279
+ const staticKeyPair = generateKeyPair();
3280
+ const ephKeyPair = generateKeyPair();
3281
+ const ss = createSymState();
3282
+ let remoteEphPub = null;
3283
+ let remoteStaticPub = null;
3284
+ return {
3285
+ get remoteStaticPublicKey() {
3286
+ return remoteStaticPub;
3287
+ },
3288
+ writeMessage1() {
3289
+ ss.mixHash(ephKeyPair.publicKey);
3290
+ return new Uint8Array(ephKeyPair.publicKey);
3291
+ },
3292
+ readMessage2(msg) {
3293
+ if (msg.length < 96) {
3294
+ throw new Error(`noise msg2: expected >=96 bytes, got ${msg.length}`);
3295
+ }
3296
+ remoteEphPub = msg.slice(0, 32);
3297
+ ss.mixHash(remoteEphPub);
3298
+ const dhEE = sharedKey(ephKeyPair.secretKey, remoteEphPub);
3299
+ ss.mixKey(dhEE);
3300
+ remoteStaticPub = ss.decryptAndHash(msg.slice(32, 80));
3301
+ const dhES = sharedKey(ephKeyPair.secretKey, remoteStaticPub);
3302
+ ss.mixKey(dhES);
3303
+ if (msg.length > 80) {
3304
+ ss.decryptAndHash(msg.slice(80));
3305
+ }
3306
+ },
3307
+ writeMessage3() {
3308
+ if (!remoteEphPub) {
3309
+ throw new Error("noise msg3: must call readMessage2 first");
3310
+ }
3311
+ const encS = ss.encryptAndHash(staticKeyPair.publicKey);
3312
+ const dhSE = sharedKey(staticKeyPair.secretKey, remoteEphPub);
3313
+ ss.mixKey(dhSE);
3314
+ const encPayload = ss.encryptAndHash(new Uint8Array(0));
3315
+ const message = new Uint8Array(encS.length + encPayload.length);
3316
+ message.set(encS);
3317
+ message.set(encPayload, encS.length);
3318
+ const [sendKey, recvKey] = ss.split();
3319
+ return { message, transport: createNoiseTransport(sendKey, recvKey) };
3320
+ }
3321
+ };
3322
+ }
3323
+
3324
+ // src/session/secureChannel.ts
3325
+ var ATTEST_REQUEST_PREFIX = new TextEncoder().encode("invisible-attest-v1\0");
3326
+ var DEFAULT_CHANNEL_TIMEOUT_MS = 3e4;
3327
+ async function establishSecureChannel(options) {
3328
+ const { transport, endpoint, nonce } = options;
3329
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CHANNEL_TIMEOUT_MS;
3330
+ if (transport.state() !== "open") {
3331
+ throw new TransportError("CONNECTION_LOST", "secure channel requires an open transport");
3332
+ }
3333
+ const inbound = createFrameReader(transport);
3334
+ try {
3335
+ const initiator = createNoiseXXInitiator();
3336
+ transport.send(initiator.writeMessage1());
3337
+ const msg2 = await inbound.next(timeoutMs);
3338
+ try {
3339
+ initiator.readMessage2(msg2);
3340
+ } catch (cause) {
3341
+ throw new TransportError("NOISE_HANDSHAKE_FAILED", `readMessage2 failed: ${String(cause)}`, {
3342
+ cause
3343
+ });
3344
+ }
3345
+ const remoteStaticPublicKey = initiator.remoteStaticPublicKey;
3346
+ if (!remoteStaticPublicKey) {
3347
+ throw new TransportError(
3348
+ "NOISE_HANDSHAKE_FAILED",
3349
+ "missing responder static pubkey after readMessage2"
3350
+ );
3351
+ }
3352
+ let pending;
3353
+ try {
3354
+ const result = initiator.writeMessage3();
3355
+ transport.send(result.message);
3356
+ pending = result.transport;
3357
+ } catch (cause) {
3358
+ throw new TransportError("NOISE_HANDSHAKE_FAILED", `writeMessage3 failed: ${String(cause)}`, {
3359
+ cause
3360
+ });
3361
+ }
3362
+ const nonceHex = bytesToHex3(nonce);
3363
+ transport.send(pending.encrypt(encodeAttestationRequest(nonceHex)));
3364
+ const responseFrame = await inbound.next(timeoutMs);
3365
+ let response;
3366
+ try {
3367
+ response = decodeAttestationResponse(pending.decrypt(responseFrame));
3368
+ } catch (cause) {
3369
+ if (cause instanceof AttestationError) throw cause;
3370
+ throw new AttestationError(
3371
+ "NOT_ATTESTED",
3372
+ `failed to decode attestation response: ${String(cause)}`,
3373
+ { cause }
3374
+ );
3375
+ }
3376
+ const policy = toAttestationPolicy(endpoint);
3377
+ const attestation = await verifyAttestation({
3378
+ response,
3379
+ expectedNonce: nonce,
3380
+ expectedNoiseStaticPub: remoteStaticPublicKey,
3381
+ policy,
3382
+ timeoutMs
3383
+ });
3384
+ inbound.detach();
3385
+ return promoteChannel({ transport, cipher: pending, attestation, remoteStaticPublicKey });
3386
+ } catch (err2) {
3387
+ inbound.detach();
3388
+ throw err2;
3389
+ }
3390
+ }
3391
+ function toAttestationPolicy(endpoint) {
3392
+ const pin = endpoint.releasePin;
3393
+ const loopback = isLoopbackUrl(endpoint.wsUrl);
3394
+ const allowLocalAttestation = endpoint.allowLocalAttestation === true && loopback && endpoint.requiredMode !== "prod";
3395
+ const policy = {
3396
+ ...pin.intelRootFingerprint ? { intelRootFingerprint: pin.intelRootFingerprint } : {},
3397
+ // "auto" leaves the mode unconstrained; "prod"/"dev" are passed through.
3398
+ ...endpoint.requiredMode !== "auto" ? { requiredMode: endpoint.requiredMode } : {},
3399
+ // Keep the release pin active in prod and auto mode. Dev local attestations
3400
+ // ignore the MRTD pin inside the verifier.
3401
+ expectedMrtd: pin.mrtd,
3402
+ ...pin.binaryHash ? { expectedBinaryHash: pin.binaryHash } : {},
3403
+ ...pin.runtimeProfile ? { expectedRuntimeProfile: pin.runtimeProfile } : {},
3404
+ ...pin.runtimeMeasurements ? { expectedRuntimeMeasurements: pin.runtimeMeasurements } : {},
3405
+ allowMissingDcapCollateral: pin.allowMissingDcapCollateral === true,
3406
+ allowLocalAttestation
3407
+ };
3408
+ if (pin.azureMaa) {
3409
+ policy.azureMaa = {
3410
+ expectedIssuer: pin.azureMaa.issuer,
3411
+ expectedJwksUrl: pin.azureMaa.jwksUrl,
3412
+ expectedPolicyHash: pin.azureMaa.policyHash
3413
+ };
3414
+ }
3415
+ return policy;
3416
+ }
3417
+ function promoteChannel(params) {
3418
+ const { transport, cipher, attestation, remoteStaticPublicKey } = params;
3419
+ const handlers = /* @__PURE__ */ new Set();
3420
+ const fatalHandlers = /* @__PURE__ */ new Set();
3421
+ let attestationWaiter = null;
3422
+ let closed = false;
3423
+ const unsubscribeRaw = transport.onMessage((frame) => {
3424
+ if (closed) return;
3425
+ let plaintext;
3426
+ try {
3427
+ plaintext = cipher.decrypt(frame);
3428
+ } catch (cause) {
3429
+ const error = new TransportError(
3430
+ "NOISE_HANDSHAKE_FAILED",
3431
+ `secure channel decrypt failed: ${String(cause)}`,
3432
+ { cause }
3433
+ );
3434
+ fail(error);
3435
+ return;
3436
+ }
3437
+ if (attestationWaiter !== null) {
3438
+ const decoded = decodeAttestationControlFrame(plaintext);
3439
+ if (decoded.kind === "response") {
3440
+ const waiter = attestationWaiter;
3441
+ attestationWaiter = null;
3442
+ clearTimeout(waiter.timer);
3443
+ waiter.resolve(decoded.response);
3444
+ return;
3445
+ }
3446
+ if (decoded.kind === "invalid") {
3447
+ const waiter = attestationWaiter;
3448
+ attestationWaiter = null;
3449
+ clearTimeout(waiter.timer);
3450
+ waiter.reject(decoded.error);
3451
+ return;
3452
+ }
3453
+ }
3454
+ for (const handler of handlers) handler(plaintext);
3455
+ });
3456
+ const unsubscribeState = transport.onStateChange((state) => {
3457
+ if (closed) return;
3458
+ if (state === "closing" || state === "closed") {
3459
+ fail(new TransportError("CONNECTION_LOST", "secure channel transport closed"));
3460
+ }
3461
+ });
3462
+ function close() {
3463
+ if (closed) return;
3464
+ closed = true;
3465
+ handlers.clear();
3466
+ fatalHandlers.clear();
3467
+ if (attestationWaiter !== null) {
3468
+ const waiter = attestationWaiter;
3469
+ attestationWaiter = null;
3470
+ clearTimeout(waiter.timer);
3471
+ waiter.reject(new TransportError("CONNECTION_LOST", "secure channel is closed"));
3472
+ }
3473
+ unsubscribeRaw();
3474
+ unsubscribeState();
3475
+ }
3476
+ function fail(error) {
3477
+ if (closed) return;
3478
+ const notifyHandlers = [...fatalHandlers];
3479
+ for (const handler of notifyHandlers) handler(error);
3480
+ if (!closed) {
3481
+ close();
3482
+ transport.close();
3483
+ }
3484
+ }
3485
+ return {
3486
+ attestation,
3487
+ remoteStaticPublicKey,
3488
+ requestAttestation(nonceHex, timeoutMs) {
3489
+ if (closed) {
3490
+ return Promise.reject(new TransportError("CONNECTION_LOST", "secure channel is closed"));
3491
+ }
3492
+ if (attestationWaiter !== null) {
3493
+ return Promise.reject(
3494
+ new AttestationError("NOT_ATTESTED", "re-attestation is already in flight")
3495
+ );
3496
+ }
3497
+ return new Promise((resolve, reject) => {
3498
+ const timer = setTimeout(() => {
3499
+ if (attestationWaiter === null) return;
3500
+ attestationWaiter = null;
3501
+ reject(
3502
+ new AttestationError("NOT_ATTESTED", `re-attestation timed out after ${timeoutMs}ms`)
3503
+ );
3504
+ }, timeoutMs);
3505
+ attestationWaiter = { resolve, reject, timer };
3506
+ try {
3507
+ transport.send(cipher.encrypt(encodeAttestationRequest(nonceHex)));
3508
+ } catch (cause) {
3509
+ if (attestationWaiter === null) return;
3510
+ attestationWaiter = null;
3511
+ clearTimeout(timer);
3512
+ reject(cause instanceof Error ? cause : new Error(String(cause)));
3513
+ }
3514
+ });
3515
+ },
3516
+ send(plaintext) {
3517
+ if (closed) {
3518
+ throw new TransportError("CONNECTION_LOST", "secure channel is closed");
3519
+ }
3520
+ transport.send(cipher.encrypt(plaintext));
3521
+ },
3522
+ onMessage(handler) {
3523
+ handlers.add(handler);
3524
+ return () => handlers.delete(handler);
3525
+ },
3526
+ onFatal(handler) {
3527
+ fatalHandlers.add(handler);
3528
+ return () => fatalHandlers.delete(handler);
3529
+ },
3530
+ close
3531
+ };
3532
+ }
3533
+ function createFrameReader(transport) {
3534
+ const queue = [];
3535
+ let waiter = null;
3536
+ let detached = false;
3537
+ const unsubscribeMessage = transport.onMessage((frame) => {
3538
+ if (detached) return;
3539
+ if (waiter) {
3540
+ const w = waiter;
3541
+ waiter = null;
3542
+ w.resolve(frame);
3543
+ } else {
3544
+ queue.push(frame);
3545
+ }
3546
+ });
3547
+ const unsubscribeState = transport.onStateChange((state) => {
3548
+ if (detached) return;
3549
+ if (state === "closing" || state === "closed") {
3550
+ const w = waiter;
3551
+ waiter = null;
3552
+ w?.reject(
3553
+ new TransportError("CONNECTION_LOST", "transport closed during handshake/attestation")
3554
+ );
3555
+ }
3556
+ });
3557
+ function detach() {
3558
+ if (detached) return;
3559
+ detached = true;
3560
+ unsubscribeMessage();
3561
+ unsubscribeState();
3562
+ queue.length = 0;
3563
+ }
3564
+ return {
3565
+ next(timeoutMs) {
3566
+ if (detached) {
3567
+ return Promise.reject(new TransportError("CONNECTION_LOST", "frame reader detached"));
3568
+ }
3569
+ const queued = queue.shift();
3570
+ if (queued) return Promise.resolve(queued);
3571
+ return new Promise((resolve, reject) => {
3572
+ const timer = setTimeout(() => {
3573
+ if (waiter) {
3574
+ waiter = null;
3575
+ reject(
3576
+ new TransportError(
3577
+ "NOISE_HANDSHAKE_FAILED",
3578
+ `timed out waiting for a handshake frame after ${timeoutMs}ms`
3579
+ )
3580
+ );
3581
+ }
3582
+ }, timeoutMs);
3583
+ waiter = {
3584
+ resolve: (f) => {
3585
+ clearTimeout(timer);
3586
+ resolve(f);
3587
+ },
3588
+ reject: (e) => {
3589
+ clearTimeout(timer);
3590
+ reject(e);
3591
+ }
3592
+ };
3593
+ });
3594
+ },
3595
+ detach
3596
+ };
3597
+ }
3598
+ function decodeAttestationControlFrame(plaintext) {
3599
+ const text = new TextDecoder().decode(plaintext);
3600
+ let body;
3601
+ try {
3602
+ body = JSON.parse(text);
3603
+ } catch {
3604
+ return { kind: "app" };
3605
+ }
3606
+ try {
3607
+ assertAttestResponse(body);
3608
+ return { kind: "response", response: body };
3609
+ } catch (cause) {
3610
+ if (looksLikeAttestationResponse(body)) {
3611
+ return {
3612
+ kind: "invalid",
3613
+ error: cause instanceof AttestationError ? cause : new AttestationError("NOT_ATTESTED", String(cause), { cause })
3614
+ };
3615
+ }
3616
+ return { kind: "app" };
3617
+ }
3618
+ }
3619
+ function looksLikeAttestationResponse(value) {
3620
+ if (typeof value !== "object" || value === null) return false;
3621
+ const body = value;
3622
+ return "quote" in body || "nonce" in body || "pubkey" in body || "binary_hash" in body || "local_attestation" in body || "maa_token" in body || "tpm_quote" in body || "hcl_keys_json" in body;
3623
+ }
3624
+ function encodeAttestationRequest(nonceHex) {
3625
+ const nonce = new TextEncoder().encode(nonceHex);
3626
+ const out = new Uint8Array(ATTEST_REQUEST_PREFIX.length + nonce.length);
3627
+ out.set(ATTEST_REQUEST_PREFIX);
3628
+ out.set(nonce, ATTEST_REQUEST_PREFIX.length);
3629
+ return out;
3630
+ }
3631
+ function decodeAttestationResponse(plaintext) {
3632
+ const text = new TextDecoder().decode(plaintext);
3633
+ let body;
3634
+ try {
3635
+ body = JSON.parse(text);
3636
+ } catch (cause) {
3637
+ throw new AttestationError(
3638
+ "NOT_ATTESTED",
3639
+ `attestation response is not valid JSON: ${String(cause)}`,
3640
+ { cause }
3641
+ );
3642
+ }
3643
+ assertAttestResponse(body);
3644
+ return body;
3645
+ }
3646
+ function isLoopbackUrl(value) {
3647
+ try {
3648
+ return isLoopbackHostname(new URL(value).hostname);
3649
+ } catch {
3650
+ return false;
3651
+ }
3652
+ }
3653
+ function isLoopbackHostname(hostname) {
3654
+ const normalized = hostname.toLowerCase();
3655
+ if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") {
3656
+ return true;
3657
+ }
3658
+ if (isLoopbackIpv4(normalized)) {
3659
+ return true;
3660
+ }
3661
+ if (!normalized.startsWith("[::ffff:") || !normalized.endsWith("]")) {
3662
+ return false;
3663
+ }
3664
+ const mapped = normalized.slice("[::ffff:".length, -1);
3665
+ if (isLoopbackIpv4(mapped)) {
3666
+ return true;
3667
+ }
3668
+ const parts = mapped.split(":");
3669
+ if (parts.length !== 2) {
3670
+ return false;
3671
+ }
3672
+ const high = Number.parseInt(parts[0] ?? "", 16);
3673
+ const low = Number.parseInt(parts[1] ?? "", 16);
3674
+ if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 65535 || low < 0 || low > 65535) {
3675
+ return false;
3676
+ }
3677
+ return (high << 16 | low) >>> 24 === 127;
3678
+ }
3679
+ function isLoopbackIpv4(hostname) {
3680
+ const parts = hostname.split(".");
3681
+ return parts.length === 4 && parts.every((part) => /^\d+$/.test(part)) && Number(parts[0]) === 127 && parts.every((part) => Number(part) >= 0 && Number(part) <= 255);
3682
+ }
3683
+ function bytesToHex3(b) {
3684
+ let s = "";
3685
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
3686
+ return s;
3687
+ }
3688
+
3689
+ // src/transport/handshake.ts
3690
+ async function establishAttestedChannel(params) {
3691
+ const { transport, endpoint, nonce, timeoutMs } = params;
3692
+ const channel = await establishSecureChannel({ transport, endpoint, nonce, timeoutMs });
3693
+ const handshake = {
3694
+ noiseStaticPublicKey: channel.remoteStaticPublicKey,
3695
+ channel
3696
+ };
3697
+ const attestation = {
3698
+ mrtd: channel.attestation.mrtd,
3699
+ mode: channel.attestation.mode,
3700
+ binaryHash: channel.attestation.binaryHash,
3701
+ runtimeProfile: channel.attestation.runtimeProfile,
3702
+ pubkeyHex: channel.attestation.pubkeyHex,
3703
+ localAttestation: channel.attestation.localAttestation,
3704
+ rtmr0: channel.attestation.rtmr0,
3705
+ rtmr1: channel.attestation.rtmr1,
3706
+ rtmr2: channel.attestation.rtmr2,
3707
+ rtmr3: channel.attestation.rtmr3,
3708
+ verifiedAtMs: Date.now()
3709
+ };
3710
+ void toAttestationPolicy(endpoint);
3711
+ return { channel, handshake, attestation };
3712
+ }
3713
+
3714
+ // src/schemas/payoutPolicy.schema.json
3715
+ var payoutPolicy_schema_default = {
3716
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3717
+ $id: "https://invisible.exchange/schemas/sdk/payoutPolicy.schema.json",
3718
+ title: "PayoutPolicy",
3719
+ description: "Where and how a contract's output is delivered. V0 normal-user requests accept exactly one destination. Validated locally before submission (spec sections 4.3.1 and 10.1).",
3720
+ type: "object",
3721
+ additionalProperties: false,
3722
+ required: ["destinations"],
3723
+ properties: {
3724
+ destinations: {
3725
+ type: "array",
3726
+ minItems: 1,
3727
+ description: "V0 normal-user requests accept exactly one destination. Multi-address receive is not supported yet.",
3728
+ items: { $ref: "#/$defs/payoutDestination" }
3729
+ },
3730
+ totalDeadlineMs: {
3731
+ type: "integer",
3732
+ minimum: 0,
3733
+ description: "Payout-window selector in milliseconds: 0 for instant, or a currently supported scheduled window duration."
3734
+ },
3735
+ batchExclusionId: {
3736
+ type: "string",
3737
+ description: "Not supported in V0 normal-user requests. Reserved for V1 campaign-scoped batch exclusion; never a stable actor identity."
3738
+ }
3739
+ },
3740
+ $defs: {
3741
+ payoutDestination: {
3742
+ title: "PayoutDestination",
3743
+ type: "object",
3744
+ additionalProperties: false,
3745
+ required: ["address", "sharePercent"],
3746
+ properties: {
3747
+ address: {
3748
+ type: "string",
3749
+ description: "Base58 Solana account address."
3750
+ },
3751
+ sharePercent: {
3752
+ type: "number",
3753
+ minimum: 0,
3754
+ maximum: 100,
3755
+ description: "Share of the payout. Sums to 100 across destinations."
3756
+ },
3757
+ minDelayMs: {
3758
+ type: "integer",
3759
+ minimum: 0,
3760
+ description: "Not supported in V0 normal-user requests; the SDK rejects this field because the current runtime does not honor per-destination delay bounds."
3761
+ },
3762
+ maxDelayMs: {
3763
+ type: "integer",
3764
+ minimum: 0,
3765
+ description: "Not supported in V0 normal-user requests; the SDK rejects this field because the current runtime does not honor per-destination delay bounds."
3766
+ },
3767
+ minLamports: { type: "integer", minimum: 0 }
3768
+ }
3769
+ }
3770
+ }
3771
+ };
3772
+
3773
+ // src/schemas/coordinatorPoolConfig.schema.json
3774
+ var coordinatorPoolConfig_schema_default = {
3775
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3776
+ $id: "https://invisible.exchange/schemas/sdk/coordinatorPoolConfig.schema.json",
3777
+ title: "CoordinatorPoolConfig",
3778
+ description: "The coordinator endpoint pool a session connects through (spec section 6.1). No hostname is baked in; callers supply endpoints (or opt into a preset).",
3779
+ type: "object",
3780
+ additionalProperties: false,
3781
+ required: ["endpoints"],
3782
+ properties: {
3783
+ endpoints: {
3784
+ type: "array",
3785
+ minItems: 1,
3786
+ items: { $ref: "#/$defs/coordinatorEndpoint" }
3787
+ },
3788
+ reattestEveryMs: { type: "integer", minimum: 1 },
3789
+ failoverDebounceMs: { type: "integer", minimum: 0 },
3790
+ maxReconnectAttempts: { type: "integer", minimum: 0 },
3791
+ noiseTransportTimeoutMs: { type: "integer", minimum: 1 },
3792
+ preferLeader: { type: "boolean" },
3793
+ allowedRoles: {
3794
+ type: "array",
3795
+ items: { enum: ["leader", "watcher"] }
3796
+ }
3797
+ },
3798
+ $defs: {
3799
+ coordinatorEndpoint: {
3800
+ title: "CoordinatorEndpoint",
3801
+ type: "object",
3802
+ additionalProperties: false,
3803
+ required: ["wsUrl", "expectedHostname", "releasePin", "requiredMode"],
3804
+ allOf: [
3805
+ {
3806
+ if: {
3807
+ properties: {
3808
+ releasePin: {
3809
+ type: "object",
3810
+ required: ["allowMissingDcapCollateral"],
3811
+ properties: {
3812
+ allowMissingDcapCollateral: { const: true }
3813
+ }
3814
+ }
3815
+ },
3816
+ required: ["releasePin"]
3817
+ },
3818
+ then: {
3819
+ properties: {
3820
+ requiredMode: { const: "dev" }
3821
+ }
3822
+ }
3823
+ }
3824
+ ],
3825
+ properties: {
3826
+ wsUrl: { type: "string", description: "wss://<host>/ws-noise" },
3827
+ expectedHostname: {
3828
+ type: "string",
3829
+ description: "Must match the SNI / TLS certificate host."
3830
+ },
3831
+ releasePin: { $ref: "#/$defs/releasePin" },
3832
+ requiredMode: { enum: ["prod", "dev", "auto"] },
3833
+ allowLocalAttestation: {
3834
+ type: "boolean",
3835
+ description: "Explicit local-dev fallback opt-in. Honored only for loopback non-prod endpoints."
3836
+ },
3837
+ roleHint: { enum: ["leader", "watcher"] },
3838
+ weight: { type: "number", minimum: 0 }
3839
+ }
3840
+ },
3841
+ releasePin: {
3842
+ title: "CoordinatorReleasePin",
3843
+ type: "object",
3844
+ additionalProperties: false,
3845
+ required: ["mrtd"],
3846
+ properties: {
3847
+ mrtd: { type: "string", description: "Hex MRTD release pin." },
3848
+ binaryHash: {
3849
+ type: "string",
3850
+ pattern: "^[0-9a-fA-F]{64}$",
3851
+ description: "Optional SHA-256 hash of the approved coordinator binary; required for production Azure attestation."
3852
+ },
3853
+ runtimeProfile: {
3854
+ enum: [
3855
+ "prod-hardened",
3856
+ "prod-devnet-simplified",
3857
+ "prod-devnet-simplified-locked",
3858
+ "azure-tdx-v0-agentless"
3859
+ ],
3860
+ description: "Optional compile-time coordinator runtime profile; production simplified deployments must pin it."
3861
+ },
3862
+ runtimeMeasurements: {
3863
+ $ref: "#/$defs/runtimeMeasurements",
3864
+ description: "Approved RTMR0..RTMR3 values. Required for the agentless Azure TDX V0 profile."
3865
+ },
3866
+ intelRootFingerprint: {
3867
+ type: "string",
3868
+ description: "Hex SHA-256 of the Intel root cert."
3869
+ },
3870
+ allowMissingDcapCollateral: {
3871
+ type: "boolean",
3872
+ description: "Temporary non-prod legacy escape hatch for coordinators that do not yet emit Intel DCAP collateral."
3873
+ },
3874
+ azureMaa: { $ref: "#/$defs/azureMaaPin" }
3875
+ }
3876
+ },
3877
+ runtimeMeasurements: {
3878
+ title: "RuntimeMeasurements",
3879
+ type: "object",
3880
+ additionalProperties: false,
3881
+ required: ["rtmr0", "rtmr1", "rtmr2", "rtmr3"],
3882
+ properties: {
3883
+ rtmr0: { type: "string", pattern: "^[0-9a-fA-F]{96}$" },
3884
+ rtmr1: { type: "string", pattern: "^[0-9a-fA-F]{96}$" },
3885
+ rtmr2: { type: "string", pattern: "^[0-9a-fA-F]{96}$" },
3886
+ rtmr3: { type: "string", pattern: "^[0-9a-fA-F]{96}$" }
3887
+ }
3888
+ },
3889
+ azureMaaPin: {
3890
+ title: "AzureMaaPin",
3891
+ type: "object",
3892
+ additionalProperties: false,
3893
+ required: ["issuer", "jwksUrl", "policyHash"],
3894
+ properties: {
3895
+ issuer: { type: "string" },
3896
+ jwksUrl: { type: "string" },
3897
+ policyHash: { type: "string" }
3898
+ }
3899
+ }
3900
+ }
3901
+ };
3902
+
3903
+ // src/schemas/lpPositionView.schema.json
3904
+ var lpPositionView_schema_default = {
3905
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3906
+ $id: "https://invisible.exchange/schemas/sdk/lpPositionView.schema.json",
3907
+ title: "LpPositionView",
3908
+ description: "The actor-safe LP position projection (spec section 4.4.1). Earned fees are per-position, never aggregated globally.",
3909
+ type: "object",
3910
+ additionalProperties: false,
3911
+ required: [
3912
+ "id",
3913
+ "status",
3914
+ "targetShardCount",
3915
+ "authPublicKey",
3916
+ "refillPublicKey",
3917
+ "withdrawalCommitment",
3918
+ "committedLamports",
3919
+ "earnedLamports",
3920
+ "refillThreshold",
3921
+ "shards",
3922
+ "createdAt"
3923
+ ],
3924
+ properties: {
3925
+ id: { type: "string", description: "lp_position_id" },
3926
+ status: { enum: ["dkg_pending", "active", "withdrawing", "closed"] },
3927
+ targetShardCount: { type: "integer", minimum: 0, maximum: 300 },
3928
+ authPublicKey: {
3929
+ type: "string",
3930
+ description: "Derived from the LP Position Code; signs LP commands."
3931
+ },
3932
+ refillPublicKey: { type: "string", description: "Ed25519, 32 bytes." },
3933
+ withdrawalCommitment: {
3934
+ type: "string",
3935
+ description: "SHA256(withdrawal_secret)."
3936
+ },
3937
+ committedLamports: { type: "integer", minimum: 0 },
3938
+ earnedLamports: { type: "integer", minimum: 0 },
3939
+ refillThreshold: { type: "integer", minimum: 0 },
3940
+ shards: {
3941
+ type: "array",
3942
+ description: "LpInventoryShard[] from the protocol package; opaque to the SDK skeleton.",
3943
+ items: { type: "object" }
3944
+ },
3945
+ createdAt: { type: "integer", minimum: 0 },
3946
+ redemptionPermission: {
3947
+ type: "object",
3948
+ additionalProperties: false,
3949
+ required: ["state", "grantRevision", "observedAtMs"],
3950
+ properties: {
3951
+ state: { enum: ["unknown", "locked", "enabled"] },
3952
+ grantRevision: { type: "integer", minimum: 0 },
3953
+ observedAtMs: { type: "integer", minimum: 0 }
3954
+ }
3955
+ }
3956
+ }
3957
+ };
3958
+
3959
+ // src/schemas/storageEnvelope.schema.json
3960
+ var storageEnvelope_schema_default = {
3961
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3962
+ $id: "https://invisible.exchange/schemas/sdk/storageEnvelope.schema.json",
3963
+ title: "StorageEnvelope",
3964
+ description: "Optional wrapped form for callers or future encrypted storage adapters (spec section 7). Current browser and extension adapters persist caller-supplied bytes; callers decide whether those bytes are plaintext, wrapped, or encrypted.",
3965
+ type: "object",
3966
+ additionalProperties: false,
3967
+ required: ["namespace", "key", "alg", "ciphertext", "createdAtMs"],
3968
+ properties: {
3969
+ namespace: {
3970
+ type: "string",
3971
+ minLength: 1
3972
+ },
3973
+ key: {
3974
+ type: "string",
3975
+ minLength: 1
3976
+ },
3977
+ alg: {
3978
+ enum: ["AES-GCM", "none"],
3979
+ description: "Wrapping algorithm. `none` means the caller supplied bytes without an envelope-level transform."
3980
+ },
3981
+ ciphertext: {
3982
+ type: "string",
3983
+ description: "Base64url-encoded wrapped bytes, encrypted bytes, or caller-supplied plaintext bytes when alg is `none`."
3984
+ },
3985
+ iv: {
3986
+ type: "string",
3987
+ description: "Base64url-encoded AES-GCM nonce; required when alg is AES-GCM."
3988
+ },
3989
+ createdAtMs: {
3990
+ type: "integer",
3991
+ minimum: 0
3992
+ }
3993
+ },
3994
+ allOf: [
3995
+ {
3996
+ if: {
3997
+ properties: { alg: { const: "AES-GCM" } },
3998
+ required: ["alg"]
3999
+ },
4000
+ then: {
4001
+ properties: { iv: { type: "string" } },
4002
+ required: ["iv"]
4003
+ }
4004
+ }
4005
+ ]
4006
+ };
4007
+
4008
+ // src/validation/ajv.ts
4009
+ var ajv = new Ajv2020({ allErrors: true, strict: true });
4010
+ var validatePayoutPolicyShape = ajv.compile(payoutPolicy_schema_default);
4011
+ var validateCoordinatorPoolConfigShape = ajv.compile(coordinatorPoolConfig_schema_default);
4012
+ ajv.compile(lpPositionView_schema_default);
4013
+ ajv.compile(storageEnvelope_schema_default);
4014
+ function formatAjvErrors(errors) {
4015
+ if (!errors || errors.length === 0) return "schema validation failed";
4016
+ return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? "is invalid"}`.trim()).join("; ");
4017
+ }
4018
+
4019
+ // src/attestation/reattest.ts
4020
+ var DEFAULT_REATTEST_TIMEOUT_MS = 3e4;
4021
+ async function reattest(state, endpoint, timeoutMs = DEFAULT_REATTEST_TIMEOUT_MS) {
4022
+ const { channel, remoteStaticPublicKey } = state;
4023
+ if (channel === null || remoteStaticPublicKey === null) {
4024
+ throw new TransportError("CONNECTION_LOST", "re-attestation requires a live attested channel");
4025
+ }
4026
+ const nonce = crypto.getRandomValues(new Uint8Array(32));
4027
+ const response = await channel.requestAttestation(toHex(nonce), timeoutMs);
4028
+ const metadata = await verifyAttestation({
4029
+ response,
4030
+ expectedNonce: nonce,
4031
+ expectedNoiseStaticPub: remoteStaticPublicKey,
4032
+ policy: toAttestationPolicy(endpoint),
4033
+ timeoutMs
4034
+ });
4035
+ return {
4036
+ mrtd: metadata.mrtd,
4037
+ mode: metadata.mode,
4038
+ binaryHash: metadata.binaryHash,
4039
+ runtimeProfile: metadata.runtimeProfile,
4040
+ pubkeyHex: metadata.pubkeyHex,
4041
+ localAttestation: metadata.localAttestation,
4042
+ rtmr0: metadata.rtmr0,
4043
+ rtmr1: metadata.rtmr1,
4044
+ rtmr2: metadata.rtmr2,
4045
+ rtmr3: metadata.rtmr3,
4046
+ verifiedAtMs: Date.now()
4047
+ };
4048
+ }
4049
+ function reattestSerialized(state, endpoint, timeoutMs = DEFAULT_REATTEST_TIMEOUT_MS) {
4050
+ if (state.reattestPromise !== null) {
4051
+ return state.reattestPromise;
4052
+ }
4053
+ const promise = Promise.resolve().then(() => reattest(state, endpoint, timeoutMs)).finally(() => {
4054
+ if (state.reattestPromise === promise) {
4055
+ state.reattestPromise = null;
4056
+ }
4057
+ });
4058
+ state.reattestPromise = promise;
4059
+ return promise;
4060
+ }
4061
+ function toHex(b) {
4062
+ let s = "";
4063
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
4064
+ return s;
4065
+ }
4066
+
4067
+ // src/session/createSession.ts
4068
+ var REATTEST_EVERY_MS = 9e5;
4069
+ async function createSessionInternal(options) {
4070
+ const { coordinator } = options;
4071
+ if (options.clientPersistence !== void 0 && typeof options.clientPersistence !== "boolean") {
4072
+ throw new TypeError("clientPersistence must be a boolean");
4073
+ }
4074
+ if (!validateCoordinatorPoolConfigShape(coordinator)) {
4075
+ throw new Error(
4076
+ `invalid coordinator pool config: ${formatAjvErrors(validateCoordinatorPoolConfigShape.errors)}`
4077
+ );
4078
+ }
4079
+ const endpoint = coordinator.endpoints[0];
4080
+ assertExpectedHostname(endpoint);
4081
+ const clientPersistence = options.clientPersistence === true ? await resolveClientPersistence(true, options.storage) : disabledClientPersistence();
4082
+ const timeoutMs = coordinator.noiseTransportTimeoutMs;
4083
+ const transport = options.transport ?? noiseWebSocketTransport({ wsUrl: endpoint.wsUrl, connectTimeoutMs: timeoutMs });
4084
+ await transport.connect(options.signal);
4085
+ const nonce = crypto.getRandomValues(new Uint8Array(32));
4086
+ let established;
4087
+ try {
4088
+ established = await abortableSessionEstablishment(
4089
+ establishAttestedChannel({ transport, endpoint, nonce, timeoutMs }),
4090
+ transport,
4091
+ options.signal
4092
+ );
4093
+ } catch (err2) {
4094
+ transport.close();
4095
+ throw err2;
4096
+ }
4097
+ const state = {
4098
+ attested: true,
4099
+ transport,
4100
+ pool: coordinator,
4101
+ attestation: established.attestation,
4102
+ channel: established.channel,
4103
+ nonce,
4104
+ remoteStaticPublicKey: established.handshake.noiseStaticPublicKey,
4105
+ reattestTimer: null,
4106
+ reattestPromise: null,
4107
+ lastFatalError: null,
4108
+ policyViolationHandlers: /* @__PURE__ */ new Set(),
4109
+ clientPersistence,
4110
+ close() {
4111
+ closeSessionState(this);
4112
+ }
4113
+ };
4114
+ established.channel.onFatal((error) => failSessionClosed(state, error));
4115
+ scheduleReattest(state, endpoint);
4116
+ return createSessionHandle(state);
4117
+ }
4118
+ function abortableSessionEstablishment(establishment, transport, signal) {
4119
+ if (signal === void 0) return establishment;
4120
+ if (signal.aborted) {
4121
+ transport.close();
4122
+ return Promise.reject(createSessionAbortError());
4123
+ }
4124
+ return new Promise((resolve, reject) => {
4125
+ const onAbort = () => {
4126
+ transport.close();
4127
+ reject(createSessionAbortError());
4128
+ };
4129
+ signal.addEventListener("abort", onAbort, { once: true });
4130
+ void establishment.then(resolve, reject).finally(() => {
4131
+ signal.removeEventListener("abort", onAbort);
4132
+ });
4133
+ });
4134
+ }
4135
+ function createSessionAbortError() {
4136
+ return new TransportError("CONNECTION_LOST", "session establishment aborted");
4137
+ }
4138
+ var createSession = createSessionInternal;
4139
+ function assertExpectedHostname(endpoint) {
4140
+ let hostname;
4141
+ try {
4142
+ hostname = new URL(endpoint.wsUrl).hostname;
4143
+ } catch (err2) {
4144
+ const error = new Error(`invalid coordinator wsUrl: ${endpoint.wsUrl}`);
4145
+ error.cause = err2;
4146
+ throw error;
4147
+ }
4148
+ if (hostname.toLowerCase() !== endpoint.expectedHostname.toLowerCase()) {
4149
+ throw new Error(
4150
+ `coordinator endpoint hostname ${hostname} does not match expectedHostname ${endpoint.expectedHostname}`
4151
+ );
4152
+ }
4153
+ }
4154
+ function closeSession(session) {
4155
+ getSessionState(session).close();
4156
+ }
4157
+ function scheduleReattest(state, endpoint) {
4158
+ if (state.channel === null) return;
4159
+ const reattestEveryMs = state.pool.reattestEveryMs ?? REATTEST_EVERY_MS;
4160
+ const timer = setTimeout(() => {
4161
+ void runReattest(state, endpoint);
4162
+ }, reattestEveryMs);
4163
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
4164
+ timer.unref();
4165
+ }
4166
+ state.reattestTimer = timer;
4167
+ }
4168
+ async function runReattest(state, endpoint) {
4169
+ state.reattestTimer = null;
4170
+ if (state.channel === null || state.nonce === null || state.remoteStaticPublicKey === null) {
4171
+ return;
4172
+ }
4173
+ try {
4174
+ state.attestation = await reattestSerialized(
4175
+ state,
4176
+ endpoint,
4177
+ state.pool.noiseTransportTimeoutMs
4178
+ );
4179
+ state.attested = true;
4180
+ scheduleReattest(state, endpoint);
4181
+ } catch (err2) {
4182
+ const error = err2 instanceof Error ? err2 : new Error(String(err2));
4183
+ failSessionClosed(state, error);
4184
+ }
4185
+ }
4186
+
4187
+ export { INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256, closeSession, createExponentialBackoff, createSession, createSessionInternal, createStateController, disabledClientPersistence, formatAjvErrors, getPersistedTransfer, isLoopbackUrl, listPersistedTransfers, noiseWebSocketTransport, persistTransferAllocation, persistTransferDkgMetadata, persistTransferReady, persistTransferRecoveryCode, persistTransferStatus, persistTransferTerminal, purgePersistedTransfers, readPersistedTransferForResume, reattestSerialized, removePersistedTransfer, validatePayoutPolicyShape };
4188
+ //# sourceMappingURL=chunk-Y7AK6RBV.js.map
4189
+ //# sourceMappingURL=chunk-Y7AK6RBV.js.map