@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
@@ -1,1150 +0,0 @@
1
- import { failSessionClosed, createSessionHandle, getSessionState, closeSessionState } from './chunk-V5HXJRBW.js';
2
- import { verifyAttestation, assertAttestResponse } from './chunk-DIA6U2CV.js';
3
- import { TransportError, AttestationError } from './chunk-GHBQF3A7.js';
4
- import { generateKeyPair, sharedKey } from '@stablelib/x25519';
5
- import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305';
6
- import { hash, SHA256 } from '@stablelib/sha256';
7
- import { HMAC } from '@stablelib/hmac';
8
- import { Ajv2020 } from 'ajv/dist/2020.js';
9
-
10
- // src/core/exponentialBackoff.ts
11
- function createExponentialBackoff(options) {
12
- if (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs <= 0) {
13
- throw new Error("initialDelayMs must be a positive finite number");
14
- }
15
- if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < options.initialDelayMs) {
16
- throw new Error("maxDelayMs must be finite and >= initialDelayMs");
17
- }
18
- if (options.multiplier !== void 0 && (!Number.isFinite(options.multiplier) || options.multiplier < 1)) {
19
- throw new Error("multiplier must be finite and >= 1");
20
- }
21
- if (options.jitterRatio !== void 0 && (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0 || options.jitterRatio > 1)) {
22
- throw new Error("jitterRatio must be between 0 and 1");
23
- }
24
- const initialDelayMs = options.initialDelayMs;
25
- const maxDelayMs = options.maxDelayMs;
26
- const multiplier = options.multiplier ?? 2;
27
- const jitterRatio = options.jitterRatio ?? 0;
28
- const random = options.random ?? Math.random;
29
- let attempt = 0;
30
- return {
31
- nextDelayMs() {
32
- const baseDelayMs = Math.min(maxDelayMs, initialDelayMs * multiplier ** attempt);
33
- attempt += 1;
34
- if (jitterRatio === 0) return Math.round(baseDelayMs);
35
- const lowerBound = baseDelayMs * (1 - jitterRatio);
36
- const upperBound = baseDelayMs * (1 + jitterRatio);
37
- const boundedRandom = Math.min(Math.max(random(), 0), 1);
38
- return Math.min(
39
- maxDelayMs,
40
- Math.round(lowerBound + (upperBound - lowerBound) * boundedRandom)
41
- );
42
- },
43
- reset() {
44
- attempt = 0;
45
- }
46
- };
47
- }
48
-
49
- // src/transport/stateController.ts
50
- function createStateController(initial) {
51
- let state = initial;
52
- const listeners = /* @__PURE__ */ new Set();
53
- return {
54
- get: () => state,
55
- set(next) {
56
- if (next === state) return;
57
- state = next;
58
- for (const listener of listeners) listener(next);
59
- },
60
- onChange(listener) {
61
- listeners.add(listener);
62
- return () => listeners.delete(listener);
63
- }
64
- };
65
- }
66
-
67
- // src/transport/noiseWebSocketTransport.ts
68
- var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
69
- function defaultWebSocketFactory(url) {
70
- const ctor = globalThis.WebSocket;
71
- if (!ctor) {
72
- throw new TransportError(
73
- "WS_HANDSHAKE_FAILED",
74
- "no global WebSocket available; pass options.webSocketFactory"
75
- );
76
- }
77
- return new ctor(url);
78
- }
79
- function toUint8Array(data) {
80
- if (data instanceof Uint8Array) return data;
81
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
82
- if (ArrayBuffer.isView(data)) {
83
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
84
- }
85
- if (typeof data === "string") return new TextEncoder().encode(data);
86
- throw new TransportError("NOISE_FRAME_TOO_LARGE", "unsupported inbound frame type");
87
- }
88
- function noiseWebSocketTransport(options) {
89
- const factory = options.webSocketFactory ?? defaultWebSocketFactory;
90
- const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
91
- const sc = createStateController("idle");
92
- const messageListeners = /* @__PURE__ */ new Set();
93
- let socket = null;
94
- function teardown() {
95
- if (sc.get() === "closed") return;
96
- sc.set("closing");
97
- const ws = socket;
98
- socket = null;
99
- if (ws) {
100
- ws.onopen = null;
101
- ws.onerror = null;
102
- ws.onmessage = null;
103
- ws.onclose = null;
104
- try {
105
- ws.close();
106
- } catch {
107
- }
108
- }
109
- sc.set("closed");
110
- }
111
- return {
112
- state: () => sc.get(),
113
- onStateChange: (listener) => sc.onChange(listener),
114
- onMessage(listener) {
115
- messageListeners.add(listener);
116
- return () => messageListeners.delete(listener);
117
- },
118
- connect(signal) {
119
- if (sc.get() === "open") return Promise.resolve();
120
- if (signal?.aborted) {
121
- return Promise.reject(
122
- new TransportError("CONNECTION_LOST", "connect aborted before it started")
123
- );
124
- }
125
- sc.set("connecting");
126
- return new Promise((resolve, reject) => {
127
- let settled = false;
128
- const ws = factory(options.wsUrl);
129
- socket = ws;
130
- ws.binaryType = "arraybuffer";
131
- const cleanupConnect = () => {
132
- clearTimeout(timer);
133
- signal?.removeEventListener("abort", onAbort);
134
- };
135
- const fail = (error) => {
136
- if (settled) return;
137
- settled = true;
138
- cleanupConnect();
139
- teardown();
140
- reject(error);
141
- };
142
- const onAbort = () => fail(new TransportError("CONNECTION_LOST", "connect aborted"));
143
- const timer = setTimeout(
144
- () => fail(
145
- new TransportError(
146
- "WS_HANDSHAKE_FAILED",
147
- `connect timed out after ${connectTimeoutMs}ms`
148
- )
149
- ),
150
- connectTimeoutMs
151
- );
152
- signal?.addEventListener("abort", onAbort, { once: true });
153
- ws.onopen = () => {
154
- if (settled) return;
155
- settled = true;
156
- cleanupConnect();
157
- ws.onmessage = (event) => {
158
- const frame = toUint8Array(event.data);
159
- for (const listener of messageListeners) listener(frame);
160
- };
161
- ws.onclose = () => teardown();
162
- ws.onerror = () => teardown();
163
- sc.set("open");
164
- resolve();
165
- };
166
- ws.onerror = () => fail(new TransportError("WS_HANDSHAKE_FAILED", "socket error during connect"));
167
- ws.onclose = () => fail(new TransportError("CONNECTION_LOST", "socket closed during connect"));
168
- });
169
- },
170
- send(frame) {
171
- const ws = socket;
172
- if (sc.get() !== "open" || !ws) {
173
- throw new TransportError("CONNECTION_LOST", "cannot send on a non-open transport");
174
- }
175
- ws.send(frame);
176
- },
177
- close() {
178
- teardown();
179
- }
180
- };
181
- }
182
- var PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256";
183
- function noiseNonce(n) {
184
- const buf = new Uint8Array(12);
185
- const view = new DataView(buf.buffer);
186
- view.setUint32(4, n & 4294967295, true);
187
- view.setUint32(8, Math.floor(n / 4294967296) & 4294967295, true);
188
- return buf;
189
- }
190
- function hkdf2(ck, ikm) {
191
- const mac1 = new HMAC(SHA256, ck);
192
- mac1.update(ikm);
193
- const temp = mac1.digest();
194
- const mac2 = new HMAC(SHA256, temp);
195
- mac2.update(new Uint8Array([1]));
196
- const out1 = mac2.digest();
197
- const mac3 = new HMAC(SHA256, temp);
198
- mac3.update(out1);
199
- mac3.update(new Uint8Array([2]));
200
- const out2 = mac3.digest();
201
- return [out1, out2];
202
- }
203
- function createSymState() {
204
- const proto = new TextEncoder().encode(PROTOCOL_NAME);
205
- const h0 = new Uint8Array(32);
206
- h0.set(proto);
207
- const ck0 = new Uint8Array(32);
208
- ck0.set(h0);
209
- const ss = {
210
- h: h0,
211
- ck: ck0,
212
- k: null,
213
- n: 0,
214
- mixHash(data) {
215
- const combined = new Uint8Array(this.h.length + data.length);
216
- combined.set(this.h);
217
- combined.set(data, this.h.length);
218
- this.h = hash(combined);
219
- },
220
- mixKey(ikm) {
221
- const [newCk, newK] = hkdf2(this.ck, ikm);
222
- this.ck = newCk;
223
- this.k = newK;
224
- this.n = 0;
225
- },
226
- encryptAndHash(plaintext) {
227
- if (!this.k) {
228
- this.mixHash(plaintext);
229
- return new Uint8Array(plaintext);
230
- }
231
- const aead = new ChaCha20Poly1305(this.k);
232
- const ct = aead.seal(noiseNonce(this.n), plaintext, this.h);
233
- this.n++;
234
- this.mixHash(ct);
235
- return ct;
236
- },
237
- decryptAndHash(ciphertext) {
238
- if (!this.k) {
239
- this.mixHash(ciphertext);
240
- return new Uint8Array(ciphertext);
241
- }
242
- const aead = new ChaCha20Poly1305(this.k);
243
- const pt = aead.open(noiseNonce(this.n), ciphertext, this.h);
244
- if (!pt) throw new Error("Noise: AEAD decryption failed");
245
- this.n++;
246
- this.mixHash(ciphertext);
247
- return pt;
248
- },
249
- split() {
250
- const [k1, k2] = hkdf2(this.ck, new Uint8Array(0));
251
- return [k1, k2];
252
- }
253
- };
254
- ss.mixHash(new Uint8Array(0));
255
- return ss;
256
- }
257
- function createNoiseTransport(sendKey, recvKey) {
258
- let sendN = 0;
259
- let recvN = 0;
260
- return {
261
- encrypt(plaintext) {
262
- const aead = new ChaCha20Poly1305(sendKey);
263
- const ct = aead.seal(noiseNonce(sendN), plaintext, void 0);
264
- sendN++;
265
- return ct;
266
- },
267
- decrypt(ciphertext) {
268
- const aead = new ChaCha20Poly1305(recvKey);
269
- const pt = aead.open(noiseNonce(recvN), ciphertext, void 0);
270
- if (!pt) throw new Error("Noise: transport decryption failed");
271
- recvN++;
272
- return pt;
273
- }
274
- };
275
- }
276
- function createNoiseXXInitiator() {
277
- const staticKeyPair = generateKeyPair();
278
- const ephKeyPair = generateKeyPair();
279
- const ss = createSymState();
280
- let remoteEphPub = null;
281
- let remoteStaticPub = null;
282
- return {
283
- get remoteStaticPublicKey() {
284
- return remoteStaticPub;
285
- },
286
- writeMessage1() {
287
- ss.mixHash(ephKeyPair.publicKey);
288
- return new Uint8Array(ephKeyPair.publicKey);
289
- },
290
- readMessage2(msg) {
291
- if (msg.length < 96) {
292
- throw new Error(`noise msg2: expected >=96 bytes, got ${msg.length}`);
293
- }
294
- remoteEphPub = msg.slice(0, 32);
295
- ss.mixHash(remoteEphPub);
296
- const dhEE = sharedKey(ephKeyPair.secretKey, remoteEphPub);
297
- ss.mixKey(dhEE);
298
- remoteStaticPub = ss.decryptAndHash(msg.slice(32, 80));
299
- const dhES = sharedKey(ephKeyPair.secretKey, remoteStaticPub);
300
- ss.mixKey(dhES);
301
- if (msg.length > 80) {
302
- ss.decryptAndHash(msg.slice(80));
303
- }
304
- },
305
- writeMessage3() {
306
- if (!remoteEphPub) {
307
- throw new Error("noise msg3: must call readMessage2 first");
308
- }
309
- const encS = ss.encryptAndHash(staticKeyPair.publicKey);
310
- const dhSE = sharedKey(staticKeyPair.secretKey, remoteEphPub);
311
- ss.mixKey(dhSE);
312
- const encPayload = ss.encryptAndHash(new Uint8Array(0));
313
- const message = new Uint8Array(encS.length + encPayload.length);
314
- message.set(encS);
315
- message.set(encPayload, encS.length);
316
- const [sendKey, recvKey] = ss.split();
317
- return { message, transport: createNoiseTransport(sendKey, recvKey) };
318
- }
319
- };
320
- }
321
-
322
- // src/session/secureChannel.ts
323
- var ATTEST_REQUEST_PREFIX = new TextEncoder().encode("invisible-attest-v1\0");
324
- var DEFAULT_CHANNEL_TIMEOUT_MS = 3e4;
325
- async function establishSecureChannel(options) {
326
- const { transport, endpoint, nonce } = options;
327
- const timeoutMs = options.timeoutMs ?? DEFAULT_CHANNEL_TIMEOUT_MS;
328
- if (transport.state() !== "open") {
329
- throw new TransportError("CONNECTION_LOST", "secure channel requires an open transport");
330
- }
331
- const inbound = createFrameReader(transport);
332
- try {
333
- const initiator = createNoiseXXInitiator();
334
- transport.send(initiator.writeMessage1());
335
- const msg2 = await inbound.next(timeoutMs);
336
- try {
337
- initiator.readMessage2(msg2);
338
- } catch (cause) {
339
- throw new TransportError("NOISE_HANDSHAKE_FAILED", `readMessage2 failed: ${String(cause)}`, {
340
- cause
341
- });
342
- }
343
- const remoteStaticPublicKey = initiator.remoteStaticPublicKey;
344
- if (!remoteStaticPublicKey) {
345
- throw new TransportError(
346
- "NOISE_HANDSHAKE_FAILED",
347
- "missing responder static pubkey after readMessage2"
348
- );
349
- }
350
- let pending;
351
- try {
352
- const result = initiator.writeMessage3();
353
- transport.send(result.message);
354
- pending = result.transport;
355
- } catch (cause) {
356
- throw new TransportError("NOISE_HANDSHAKE_FAILED", `writeMessage3 failed: ${String(cause)}`, {
357
- cause
358
- });
359
- }
360
- const nonceHex = bytesToHex(nonce);
361
- transport.send(pending.encrypt(encodeAttestationRequest(nonceHex)));
362
- const responseFrame = await inbound.next(timeoutMs);
363
- let response;
364
- try {
365
- response = decodeAttestationResponse(pending.decrypt(responseFrame));
366
- } catch (cause) {
367
- if (cause instanceof AttestationError) throw cause;
368
- throw new AttestationError(
369
- "NOT_ATTESTED",
370
- `failed to decode attestation response: ${String(cause)}`,
371
- { cause }
372
- );
373
- }
374
- const policy = toAttestationPolicy(endpoint);
375
- const attestation = await verifyAttestation({
376
- response,
377
- expectedNonce: nonce,
378
- expectedNoiseStaticPub: remoteStaticPublicKey,
379
- policy,
380
- timeoutMs
381
- });
382
- inbound.detach();
383
- return promoteChannel({ transport, cipher: pending, attestation, remoteStaticPublicKey });
384
- } catch (err) {
385
- inbound.detach();
386
- throw err;
387
- }
388
- }
389
- function toAttestationPolicy(endpoint) {
390
- const pin = endpoint.releasePin;
391
- const loopback = isLoopbackUrl(endpoint.wsUrl);
392
- const allowLocalAttestation = endpoint.allowLocalAttestation === true && loopback && endpoint.requiredMode !== "prod";
393
- const policy = {
394
- ...pin.intelRootFingerprint ? { intelRootFingerprint: pin.intelRootFingerprint } : {},
395
- // "auto" leaves the mode unconstrained; "prod"/"dev" are passed through.
396
- ...endpoint.requiredMode !== "auto" ? { requiredMode: endpoint.requiredMode } : {},
397
- // Keep the release pin active in prod and auto mode. Dev local attestations
398
- // ignore the MRTD pin inside the verifier.
399
- expectedMrtd: pin.mrtd,
400
- ...pin.binaryHash ? { expectedBinaryHash: pin.binaryHash } : {},
401
- ...pin.runtimeProfile ? { expectedRuntimeProfile: pin.runtimeProfile } : {},
402
- allowMissingDcapCollateral: pin.allowMissingDcapCollateral === true,
403
- allowLocalAttestation
404
- };
405
- if (pin.azureMaa) {
406
- policy.azureMaa = {
407
- expectedIssuer: pin.azureMaa.issuer,
408
- expectedJwksUrl: pin.azureMaa.jwksUrl,
409
- expectedPolicyHash: pin.azureMaa.policyHash
410
- };
411
- }
412
- return policy;
413
- }
414
- function promoteChannel(params) {
415
- const { transport, cipher, attestation, remoteStaticPublicKey } = params;
416
- const handlers = /* @__PURE__ */ new Set();
417
- const fatalHandlers = /* @__PURE__ */ new Set();
418
- let attestationWaiter = null;
419
- let closed = false;
420
- const unsubscribeRaw = transport.onMessage((frame) => {
421
- if (closed) return;
422
- let plaintext;
423
- try {
424
- plaintext = cipher.decrypt(frame);
425
- } catch (cause) {
426
- const error = new TransportError(
427
- "NOISE_HANDSHAKE_FAILED",
428
- `secure channel decrypt failed: ${String(cause)}`,
429
- { cause }
430
- );
431
- fail(error);
432
- return;
433
- }
434
- if (attestationWaiter !== null) {
435
- const decoded = decodeAttestationControlFrame(plaintext);
436
- if (decoded.kind === "response") {
437
- const waiter = attestationWaiter;
438
- attestationWaiter = null;
439
- clearTimeout(waiter.timer);
440
- waiter.resolve(decoded.response);
441
- return;
442
- }
443
- if (decoded.kind === "invalid") {
444
- const waiter = attestationWaiter;
445
- attestationWaiter = null;
446
- clearTimeout(waiter.timer);
447
- waiter.reject(decoded.error);
448
- return;
449
- }
450
- }
451
- for (const handler of handlers) handler(plaintext);
452
- });
453
- const unsubscribeState = transport.onStateChange((state) => {
454
- if (closed) return;
455
- if (state === "closing" || state === "closed") {
456
- fail(new TransportError("CONNECTION_LOST", "secure channel transport closed"));
457
- }
458
- });
459
- function close() {
460
- if (closed) return;
461
- closed = true;
462
- handlers.clear();
463
- fatalHandlers.clear();
464
- if (attestationWaiter !== null) {
465
- const waiter = attestationWaiter;
466
- attestationWaiter = null;
467
- clearTimeout(waiter.timer);
468
- waiter.reject(new TransportError("CONNECTION_LOST", "secure channel is closed"));
469
- }
470
- unsubscribeRaw();
471
- unsubscribeState();
472
- }
473
- function fail(error) {
474
- if (closed) return;
475
- const notifyHandlers = [...fatalHandlers];
476
- for (const handler of notifyHandlers) handler(error);
477
- if (!closed) {
478
- close();
479
- transport.close();
480
- }
481
- }
482
- return {
483
- attestation,
484
- remoteStaticPublicKey,
485
- requestAttestation(nonceHex, timeoutMs) {
486
- if (closed) {
487
- return Promise.reject(new TransportError("CONNECTION_LOST", "secure channel is closed"));
488
- }
489
- if (attestationWaiter !== null) {
490
- return Promise.reject(
491
- new AttestationError("NOT_ATTESTED", "re-attestation is already in flight")
492
- );
493
- }
494
- return new Promise((resolve, reject) => {
495
- const timer = setTimeout(() => {
496
- if (attestationWaiter === null) return;
497
- attestationWaiter = null;
498
- reject(
499
- new AttestationError("NOT_ATTESTED", `re-attestation timed out after ${timeoutMs}ms`)
500
- );
501
- }, timeoutMs);
502
- attestationWaiter = { resolve, reject, timer };
503
- try {
504
- transport.send(cipher.encrypt(encodeAttestationRequest(nonceHex)));
505
- } catch (cause) {
506
- if (attestationWaiter === null) return;
507
- attestationWaiter = null;
508
- clearTimeout(timer);
509
- reject(cause instanceof Error ? cause : new Error(String(cause)));
510
- }
511
- });
512
- },
513
- send(plaintext) {
514
- if (closed) {
515
- throw new TransportError("CONNECTION_LOST", "secure channel is closed");
516
- }
517
- transport.send(cipher.encrypt(plaintext));
518
- },
519
- onMessage(handler) {
520
- handlers.add(handler);
521
- return () => handlers.delete(handler);
522
- },
523
- onFatal(handler) {
524
- fatalHandlers.add(handler);
525
- return () => fatalHandlers.delete(handler);
526
- },
527
- close
528
- };
529
- }
530
- function createFrameReader(transport) {
531
- const queue = [];
532
- let waiter = null;
533
- let detached = false;
534
- const unsubscribeMessage = transport.onMessage((frame) => {
535
- if (detached) return;
536
- if (waiter) {
537
- const w = waiter;
538
- waiter = null;
539
- w.resolve(frame);
540
- } else {
541
- queue.push(frame);
542
- }
543
- });
544
- const unsubscribeState = transport.onStateChange((state) => {
545
- if (detached) return;
546
- if (state === "closing" || state === "closed") {
547
- const w = waiter;
548
- waiter = null;
549
- w?.reject(
550
- new TransportError("CONNECTION_LOST", "transport closed during handshake/attestation")
551
- );
552
- }
553
- });
554
- function detach() {
555
- if (detached) return;
556
- detached = true;
557
- unsubscribeMessage();
558
- unsubscribeState();
559
- queue.length = 0;
560
- }
561
- return {
562
- next(timeoutMs) {
563
- if (detached) {
564
- return Promise.reject(new TransportError("CONNECTION_LOST", "frame reader detached"));
565
- }
566
- const queued = queue.shift();
567
- if (queued) return Promise.resolve(queued);
568
- return new Promise((resolve, reject) => {
569
- const timer = setTimeout(() => {
570
- if (waiter) {
571
- waiter = null;
572
- reject(
573
- new TransportError(
574
- "NOISE_HANDSHAKE_FAILED",
575
- `timed out waiting for a handshake frame after ${timeoutMs}ms`
576
- )
577
- );
578
- }
579
- }, timeoutMs);
580
- waiter = {
581
- resolve: (f) => {
582
- clearTimeout(timer);
583
- resolve(f);
584
- },
585
- reject: (e) => {
586
- clearTimeout(timer);
587
- reject(e);
588
- }
589
- };
590
- });
591
- },
592
- detach
593
- };
594
- }
595
- function decodeAttestationControlFrame(plaintext) {
596
- const text = new TextDecoder().decode(plaintext);
597
- let body;
598
- try {
599
- body = JSON.parse(text);
600
- } catch {
601
- return { kind: "app" };
602
- }
603
- try {
604
- assertAttestResponse(body);
605
- return { kind: "response", response: body };
606
- } catch (cause) {
607
- if (looksLikeAttestationResponse(body)) {
608
- return {
609
- kind: "invalid",
610
- error: cause instanceof AttestationError ? cause : new AttestationError("NOT_ATTESTED", String(cause), { cause })
611
- };
612
- }
613
- return { kind: "app" };
614
- }
615
- }
616
- function looksLikeAttestationResponse(value) {
617
- if (typeof value !== "object" || value === null) return false;
618
- const body = value;
619
- 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;
620
- }
621
- function encodeAttestationRequest(nonceHex) {
622
- const nonce = new TextEncoder().encode(nonceHex);
623
- const out = new Uint8Array(ATTEST_REQUEST_PREFIX.length + nonce.length);
624
- out.set(ATTEST_REQUEST_PREFIX);
625
- out.set(nonce, ATTEST_REQUEST_PREFIX.length);
626
- return out;
627
- }
628
- function decodeAttestationResponse(plaintext) {
629
- const text = new TextDecoder().decode(plaintext);
630
- let body;
631
- try {
632
- body = JSON.parse(text);
633
- } catch (cause) {
634
- throw new AttestationError(
635
- "NOT_ATTESTED",
636
- `attestation response is not valid JSON: ${String(cause)}`,
637
- { cause }
638
- );
639
- }
640
- assertAttestResponse(body);
641
- return body;
642
- }
643
- function isLoopbackUrl(value) {
644
- try {
645
- return isLoopbackHostname(new URL(value).hostname);
646
- } catch {
647
- return false;
648
- }
649
- }
650
- function isLoopbackHostname(hostname) {
651
- const normalized = hostname.toLowerCase();
652
- if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") {
653
- return true;
654
- }
655
- if (isLoopbackIpv4(normalized)) {
656
- return true;
657
- }
658
- if (!normalized.startsWith("[::ffff:") || !normalized.endsWith("]")) {
659
- return false;
660
- }
661
- const mapped = normalized.slice("[::ffff:".length, -1);
662
- if (isLoopbackIpv4(mapped)) {
663
- return true;
664
- }
665
- const parts = mapped.split(":");
666
- if (parts.length !== 2) {
667
- return false;
668
- }
669
- const high = Number.parseInt(parts[0] ?? "", 16);
670
- const low = Number.parseInt(parts[1] ?? "", 16);
671
- if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 65535 || low < 0 || low > 65535) {
672
- return false;
673
- }
674
- return (high << 16 | low) >>> 24 === 127;
675
- }
676
- function isLoopbackIpv4(hostname) {
677
- const parts = hostname.split(".");
678
- return parts.length === 4 && parts.every((part) => /^\d+$/.test(part)) && Number(parts[0]) === 127 && parts.every((part) => Number(part) >= 0 && Number(part) <= 255);
679
- }
680
- function bytesToHex(b) {
681
- let s = "";
682
- for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
683
- return s;
684
- }
685
-
686
- // src/transport/handshake.ts
687
- async function establishAttestedChannel(params) {
688
- const { transport, endpoint, nonce, timeoutMs } = params;
689
- const channel = await establishSecureChannel({ transport, endpoint, nonce, timeoutMs });
690
- const handshake = {
691
- noiseStaticPublicKey: channel.remoteStaticPublicKey,
692
- channel
693
- };
694
- const attestation = {
695
- mrtd: channel.attestation.mrtd,
696
- mode: channel.attestation.mode,
697
- binaryHash: channel.attestation.binaryHash,
698
- runtimeProfile: channel.attestation.runtimeProfile,
699
- pubkeyHex: channel.attestation.pubkeyHex,
700
- localAttestation: channel.attestation.localAttestation,
701
- rtmr0: channel.attestation.rtmr0,
702
- rtmr1: channel.attestation.rtmr1,
703
- rtmr2: channel.attestation.rtmr2,
704
- rtmr3: channel.attestation.rtmr3,
705
- verifiedAtMs: Date.now()
706
- };
707
- void toAttestationPolicy(endpoint);
708
- return { channel, handshake, attestation };
709
- }
710
-
711
- // src/schemas/payoutPolicy.schema.json
712
- var payoutPolicy_schema_default = {
713
- $schema: "https://json-schema.org/draft/2020-12/schema",
714
- $id: "https://invisible.exchange/schemas/sdk/payoutPolicy.schema.json",
715
- title: "PayoutPolicy",
716
- 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).",
717
- type: "object",
718
- additionalProperties: false,
719
- required: ["destinations"],
720
- properties: {
721
- destinations: {
722
- type: "array",
723
- minItems: 1,
724
- description: "V0 normal-user requests accept exactly one destination. Multi-address receive is not supported yet.",
725
- items: { $ref: "#/$defs/payoutDestination" }
726
- },
727
- totalDeadlineMs: {
728
- type: "integer",
729
- minimum: 0,
730
- description: "Payout-window selector in milliseconds: 0 for instant, or a currently supported scheduled window duration."
731
- },
732
- batchExclusionId: {
733
- type: "string",
734
- description: "Not supported in V0 normal-user requests. Reserved for V1 campaign-scoped batch exclusion; never a stable actor identity."
735
- }
736
- },
737
- $defs: {
738
- payoutDestination: {
739
- title: "PayoutDestination",
740
- type: "object",
741
- additionalProperties: false,
742
- required: ["address", "sharePercent"],
743
- properties: {
744
- address: {
745
- type: "string",
746
- description: "Base58 Solana account address."
747
- },
748
- sharePercent: {
749
- type: "number",
750
- minimum: 0,
751
- maximum: 100,
752
- description: "Share of the payout. Sums to 100 across destinations."
753
- },
754
- minDelayMs: {
755
- type: "integer",
756
- minimum: 0,
757
- description: "Not supported in V0 normal-user requests; the SDK rejects this field because the current runtime does not honor per-destination delay bounds."
758
- },
759
- maxDelayMs: {
760
- type: "integer",
761
- minimum: 0,
762
- description: "Not supported in V0 normal-user requests; the SDK rejects this field because the current runtime does not honor per-destination delay bounds."
763
- },
764
- minLamports: { type: "integer", minimum: 0 }
765
- }
766
- }
767
- }
768
- };
769
-
770
- // src/schemas/coordinatorPoolConfig.schema.json
771
- var coordinatorPoolConfig_schema_default = {
772
- $schema: "https://json-schema.org/draft/2020-12/schema",
773
- $id: "https://invisible.exchange/schemas/sdk/coordinatorPoolConfig.schema.json",
774
- title: "CoordinatorPoolConfig",
775
- 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).",
776
- type: "object",
777
- additionalProperties: false,
778
- required: ["endpoints"],
779
- properties: {
780
- endpoints: {
781
- type: "array",
782
- minItems: 1,
783
- items: { $ref: "#/$defs/coordinatorEndpoint" }
784
- },
785
- reattestEveryMs: { type: "integer", minimum: 1 },
786
- failoverDebounceMs: { type: "integer", minimum: 0 },
787
- maxReconnectAttempts: { type: "integer", minimum: 0 },
788
- noiseTransportTimeoutMs: { type: "integer", minimum: 1 },
789
- preferLeader: { type: "boolean" },
790
- allowedRoles: {
791
- type: "array",
792
- items: { enum: ["leader", "watcher"] }
793
- }
794
- },
795
- $defs: {
796
- coordinatorEndpoint: {
797
- title: "CoordinatorEndpoint",
798
- type: "object",
799
- additionalProperties: false,
800
- required: ["wsUrl", "expectedHostname", "releasePin", "requiredMode"],
801
- allOf: [
802
- {
803
- if: {
804
- properties: {
805
- releasePin: {
806
- type: "object",
807
- required: ["allowMissingDcapCollateral"],
808
- properties: {
809
- allowMissingDcapCollateral: { const: true }
810
- }
811
- }
812
- },
813
- required: ["releasePin"]
814
- },
815
- then: {
816
- properties: {
817
- requiredMode: { const: "dev" }
818
- }
819
- }
820
- }
821
- ],
822
- properties: {
823
- wsUrl: { type: "string", description: "wss://<host>/ws-noise" },
824
- expectedHostname: {
825
- type: "string",
826
- description: "Must match the SNI / TLS certificate host."
827
- },
828
- releasePin: { $ref: "#/$defs/releasePin" },
829
- requiredMode: { enum: ["prod", "dev", "auto"] },
830
- allowLocalAttestation: {
831
- type: "boolean",
832
- description: "Explicit local-dev fallback opt-in. Honored only for loopback non-prod endpoints."
833
- },
834
- roleHint: { enum: ["leader", "watcher"] },
835
- weight: { type: "number", minimum: 0 }
836
- }
837
- },
838
- releasePin: {
839
- title: "CoordinatorReleasePin",
840
- type: "object",
841
- additionalProperties: false,
842
- required: ["mrtd"],
843
- properties: {
844
- mrtd: { type: "string", description: "Hex MRTD release pin." },
845
- binaryHash: {
846
- type: "string",
847
- pattern: "^[0-9a-fA-F]{64}$",
848
- description: "Optional SHA-256 hash of the approved coordinator binary; required for production Azure attestation."
849
- },
850
- runtimeProfile: {
851
- enum: ["prod-hardened", "prod-devnet-simplified"],
852
- description: "Optional compile-time coordinator runtime profile; production simplified deployments must pin it."
853
- },
854
- intelRootFingerprint: {
855
- type: "string",
856
- description: "Hex SHA-256 of the Intel root cert."
857
- },
858
- allowMissingDcapCollateral: {
859
- type: "boolean",
860
- description: "Temporary non-prod legacy escape hatch for coordinators that do not yet emit Intel DCAP collateral."
861
- },
862
- azureMaa: { $ref: "#/$defs/azureMaaPin" }
863
- }
864
- },
865
- azureMaaPin: {
866
- title: "AzureMaaPin",
867
- type: "object",
868
- additionalProperties: false,
869
- required: ["issuer", "jwksUrl", "policyHash"],
870
- properties: {
871
- issuer: { type: "string" },
872
- jwksUrl: { type: "string" },
873
- policyHash: { type: "string" }
874
- }
875
- }
876
- }
877
- };
878
-
879
- // src/schemas/lpPositionView.schema.json
880
- var lpPositionView_schema_default = {
881
- $schema: "https://json-schema.org/draft/2020-12/schema",
882
- $id: "https://invisible.exchange/schemas/sdk/lpPositionView.schema.json",
883
- title: "LpPositionView",
884
- description: "The actor-safe LP position projection (spec section 4.4.1). Earned fees are per-position, never aggregated globally.",
885
- type: "object",
886
- additionalProperties: false,
887
- required: [
888
- "id",
889
- "status",
890
- "targetShardCount",
891
- "authPublicKey",
892
- "refillPublicKey",
893
- "withdrawalCommitment",
894
- "committedLamports",
895
- "earnedLamports",
896
- "refillThreshold",
897
- "shards",
898
- "createdAt"
899
- ],
900
- properties: {
901
- id: { type: "string", description: "lp_position_id" },
902
- status: { enum: ["dkg_pending", "active", "withdrawing", "closed"] },
903
- targetShardCount: { type: "integer", minimum: 0, maximum: 300 },
904
- authPublicKey: {
905
- type: "string",
906
- description: "Derived from the LP Position Code; signs LP commands."
907
- },
908
- refillPublicKey: { type: "string", description: "Ed25519, 32 bytes." },
909
- withdrawalCommitment: {
910
- type: "string",
911
- description: "SHA256(withdrawal_secret)."
912
- },
913
- committedLamports: { type: "integer", minimum: 0 },
914
- earnedLamports: { type: "integer", minimum: 0 },
915
- refillThreshold: { type: "integer", minimum: 0 },
916
- shards: {
917
- type: "array",
918
- description: "LpInventoryShard[] from the protocol package; opaque to the SDK skeleton.",
919
- items: { type: "object" }
920
- },
921
- createdAt: { type: "integer", minimum: 0 }
922
- }
923
- };
924
-
925
- // src/schemas/storageEnvelope.schema.json
926
- var storageEnvelope_schema_default = {
927
- $schema: "https://json-schema.org/draft/2020-12/schema",
928
- $id: "https://invisible.exchange/schemas/sdk/storageEnvelope.schema.json",
929
- title: "StorageEnvelope",
930
- 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.",
931
- type: "object",
932
- additionalProperties: false,
933
- required: ["namespace", "key", "alg", "ciphertext", "createdAtMs"],
934
- properties: {
935
- namespace: {
936
- type: "string",
937
- minLength: 1
938
- },
939
- key: {
940
- type: "string",
941
- minLength: 1
942
- },
943
- alg: {
944
- enum: ["AES-GCM", "none"],
945
- description: "Wrapping algorithm. `none` means the caller supplied bytes without an envelope-level transform."
946
- },
947
- ciphertext: {
948
- type: "string",
949
- description: "Base64url-encoded wrapped bytes, encrypted bytes, or caller-supplied plaintext bytes when alg is `none`."
950
- },
951
- iv: {
952
- type: "string",
953
- description: "Base64url-encoded AES-GCM nonce; required when alg is AES-GCM."
954
- },
955
- createdAtMs: {
956
- type: "integer",
957
- minimum: 0
958
- }
959
- },
960
- allOf: [
961
- {
962
- if: {
963
- properties: { alg: { const: "AES-GCM" } },
964
- required: ["alg"]
965
- },
966
- then: {
967
- properties: { iv: { type: "string" } },
968
- required: ["iv"]
969
- }
970
- }
971
- ]
972
- };
973
-
974
- // src/validation/ajv.ts
975
- var ajv = new Ajv2020({ allErrors: true, strict: true });
976
- var validatePayoutPolicyShape = ajv.compile(payoutPolicy_schema_default);
977
- var validateCoordinatorPoolConfigShape = ajv.compile(coordinatorPoolConfig_schema_default);
978
- ajv.compile(lpPositionView_schema_default);
979
- ajv.compile(storageEnvelope_schema_default);
980
- function formatAjvErrors(errors) {
981
- if (!errors || errors.length === 0) return "schema validation failed";
982
- return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? "is invalid"}`.trim()).join("; ");
983
- }
984
-
985
- // src/attestation/reattest.ts
986
- var DEFAULT_REATTEST_TIMEOUT_MS = 3e4;
987
- async function reattest(state, endpoint, timeoutMs = DEFAULT_REATTEST_TIMEOUT_MS) {
988
- const { channel, remoteStaticPublicKey } = state;
989
- if (channel === null || remoteStaticPublicKey === null) {
990
- throw new TransportError("CONNECTION_LOST", "re-attestation requires a live attested channel");
991
- }
992
- const nonce = crypto.getRandomValues(new Uint8Array(32));
993
- const response = await channel.requestAttestation(toHex(nonce), timeoutMs);
994
- const metadata = await verifyAttestation({
995
- response,
996
- expectedNonce: nonce,
997
- expectedNoiseStaticPub: remoteStaticPublicKey,
998
- policy: toAttestationPolicy(endpoint),
999
- timeoutMs
1000
- });
1001
- return {
1002
- mrtd: metadata.mrtd,
1003
- mode: metadata.mode,
1004
- binaryHash: metadata.binaryHash,
1005
- runtimeProfile: metadata.runtimeProfile,
1006
- pubkeyHex: metadata.pubkeyHex,
1007
- localAttestation: metadata.localAttestation,
1008
- rtmr0: metadata.rtmr0,
1009
- rtmr1: metadata.rtmr1,
1010
- rtmr2: metadata.rtmr2,
1011
- rtmr3: metadata.rtmr3,
1012
- verifiedAtMs: Date.now()
1013
- };
1014
- }
1015
- function reattestSerialized(state, endpoint, timeoutMs = DEFAULT_REATTEST_TIMEOUT_MS) {
1016
- if (state.reattestPromise !== null) {
1017
- return state.reattestPromise;
1018
- }
1019
- const promise = Promise.resolve().then(() => reattest(state, endpoint, timeoutMs)).finally(() => {
1020
- if (state.reattestPromise === promise) {
1021
- state.reattestPromise = null;
1022
- }
1023
- });
1024
- state.reattestPromise = promise;
1025
- return promise;
1026
- }
1027
- function toHex(b) {
1028
- let s = "";
1029
- for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
1030
- return s;
1031
- }
1032
-
1033
- // src/session/createSession.ts
1034
- var REATTEST_EVERY_MS = 9e5;
1035
- async function createSessionInternal(options) {
1036
- const { coordinator } = options;
1037
- if (!validateCoordinatorPoolConfigShape(coordinator)) {
1038
- throw new Error(
1039
- `invalid coordinator pool config: ${formatAjvErrors(validateCoordinatorPoolConfigShape.errors)}`
1040
- );
1041
- }
1042
- const endpoint = coordinator.endpoints[0];
1043
- assertExpectedHostname(endpoint);
1044
- const timeoutMs = coordinator.noiseTransportTimeoutMs;
1045
- const transport = options.transport ?? noiseWebSocketTransport({ wsUrl: endpoint.wsUrl, connectTimeoutMs: timeoutMs });
1046
- await transport.connect(options.signal);
1047
- const nonce = crypto.getRandomValues(new Uint8Array(32));
1048
- let established;
1049
- try {
1050
- established = await abortableSessionEstablishment(
1051
- establishAttestedChannel({ transport, endpoint, nonce, timeoutMs }),
1052
- transport,
1053
- options.signal
1054
- );
1055
- } catch (err) {
1056
- transport.close();
1057
- throw err;
1058
- }
1059
- const state = {
1060
- attested: true,
1061
- transport,
1062
- pool: coordinator,
1063
- attestation: established.attestation,
1064
- channel: established.channel,
1065
- nonce,
1066
- remoteStaticPublicKey: established.handshake.noiseStaticPublicKey,
1067
- reattestTimer: null,
1068
- reattestPromise: null,
1069
- lastFatalError: null,
1070
- policyViolationHandlers: /* @__PURE__ */ new Set(),
1071
- close() {
1072
- closeSessionState(this);
1073
- }
1074
- };
1075
- established.channel.onFatal((error) => failSessionClosed(state, error));
1076
- scheduleReattest(state, endpoint);
1077
- return createSessionHandle(state);
1078
- }
1079
- function abortableSessionEstablishment(establishment, transport, signal) {
1080
- if (signal === void 0) return establishment;
1081
- if (signal.aborted) {
1082
- transport.close();
1083
- return Promise.reject(createSessionAbortError());
1084
- }
1085
- return new Promise((resolve, reject) => {
1086
- const onAbort = () => {
1087
- transport.close();
1088
- reject(createSessionAbortError());
1089
- };
1090
- signal.addEventListener("abort", onAbort, { once: true });
1091
- void establishment.then(resolve, reject).finally(() => {
1092
- signal.removeEventListener("abort", onAbort);
1093
- });
1094
- });
1095
- }
1096
- function createSessionAbortError() {
1097
- return new TransportError("CONNECTION_LOST", "session establishment aborted");
1098
- }
1099
- var createSession = createSessionInternal;
1100
- function assertExpectedHostname(endpoint) {
1101
- let hostname;
1102
- try {
1103
- hostname = new URL(endpoint.wsUrl).hostname;
1104
- } catch (err) {
1105
- const error = new Error(`invalid coordinator wsUrl: ${endpoint.wsUrl}`);
1106
- error.cause = err;
1107
- throw error;
1108
- }
1109
- if (hostname.toLowerCase() !== endpoint.expectedHostname.toLowerCase()) {
1110
- throw new Error(
1111
- `coordinator endpoint hostname ${hostname} does not match expectedHostname ${endpoint.expectedHostname}`
1112
- );
1113
- }
1114
- }
1115
- function closeSession(session) {
1116
- getSessionState(session).close();
1117
- }
1118
- function scheduleReattest(state, endpoint) {
1119
- if (state.channel === null) return;
1120
- const reattestEveryMs = state.pool.reattestEveryMs ?? REATTEST_EVERY_MS;
1121
- const timer = setTimeout(() => {
1122
- void runReattest(state, endpoint);
1123
- }, reattestEveryMs);
1124
- if (typeof timer === "object" && timer !== null && "unref" in timer) {
1125
- timer.unref();
1126
- }
1127
- state.reattestTimer = timer;
1128
- }
1129
- async function runReattest(state, endpoint) {
1130
- state.reattestTimer = null;
1131
- if (state.channel === null || state.nonce === null || state.remoteStaticPublicKey === null) {
1132
- return;
1133
- }
1134
- try {
1135
- state.attestation = await reattestSerialized(
1136
- state,
1137
- endpoint,
1138
- state.pool.noiseTransportTimeoutMs
1139
- );
1140
- state.attested = true;
1141
- scheduleReattest(state, endpoint);
1142
- } catch (err) {
1143
- const error = err instanceof Error ? err : new Error(String(err));
1144
- failSessionClosed(state, error);
1145
- }
1146
- }
1147
-
1148
- export { closeSession, createExponentialBackoff, createSession, createSessionInternal, createStateController, formatAjvErrors, isLoopbackUrl, noiseWebSocketTransport, reattestSerialized, validatePayoutPolicyShape };
1149
- //# sourceMappingURL=chunk-5NNEO7IK.js.map
1150
- //# sourceMappingURL=chunk-5NNEO7IK.js.map