@calimero-network/mero-js 19.22.0 → 19.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2757,12 +2757,12 @@ var CERT_DOMAIN = new TextEncoder().encode("calimero.device.cert.v1");
2757
2757
  var ACCOUNT_ID_DOMAIN = new TextEncoder().encode("calimero.account.genesis.v1");
2758
2758
  var DEVICE_ID_DOMAIN = new TextEncoder().encode("calimero.device.id.v1");
2759
2759
  var ACCOUNT_GENESIS_VERSION = 2;
2760
- async function mintDeviceId(account, nonce) {
2761
- if (nonce.length !== 16) {
2762
- throw new Error(`nonce must be 16 bytes, got ${nonce.length}`);
2760
+ async function mintDeviceId(account, nonce2) {
2761
+ if (nonce2.length !== 16) {
2762
+ throw new Error(`nonce must be 16 bytes, got ${nonce2.length}`);
2763
2763
  }
2764
2764
  const accountBytes = fromHex(account, "account", 32);
2765
- return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce]));
2765
+ return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce2]));
2766
2766
  }
2767
2767
  async function deviceCertPayload(input) {
2768
2768
  return domainHash(CERT_DOMAIN, [
@@ -4992,12 +4992,12 @@ function base64(bytes) {
4992
4992
  var ROUTING_PROOF_DOMAIN = new TextEncoder().encode(
4993
4993
  "calimero.mdma.routing-read.v1\0"
4994
4994
  );
4995
- async function signRoutingChallenge(nonce, deviceSecret) {
4995
+ async function signRoutingChallenge(nonce2, deviceSecret) {
4996
4996
  fromHex(deviceSecret, "deviceSecret", 32);
4997
4997
  const key = await importSigningKey(deviceSecret);
4998
4998
  const message = concat(
4999
4999
  ROUTING_PROOF_DOMAIN,
5000
- new TextEncoder().encode(nonce)
5000
+ new TextEncoder().encode(nonce2)
5001
5001
  );
5002
5002
  const signature = new Uint8Array(
5003
5003
  await crypto.subtle.sign({ name: "Ed25519" }, key, message)
@@ -5087,11 +5087,11 @@ var CloudClient = class {
5087
5087
  */
5088
5088
  async signInWithAccount(rootSecret) {
5089
5089
  const root = await accountRootFromSecret(rootSecret);
5090
- const { nonce } = await this.getAccountLoginChallenge();
5090
+ const { nonce: nonce2 } = await this.getAccountLoginChallenge();
5091
5091
  return this.submitAccountLogin({
5092
5092
  rootPublicKey: root.publicKey,
5093
- nonce,
5094
- signature: await signAccountLogin({ rootSecret, nonce })
5093
+ nonce: nonce2,
5094
+ signature: await signAccountLogin({ rootSecret, nonce: nonce2 })
5095
5095
  });
5096
5096
  }
5097
5097
  /**
@@ -5510,12 +5510,12 @@ var CloudClient = class {
5510
5510
  */
5511
5511
  async linkAccount(rootSecret) {
5512
5512
  const root = await accountRootFromSecret(rootSecret);
5513
- const { nonce } = await this.getAccountLinkChallenge();
5513
+ const { nonce: nonce2 } = await this.getAccountLinkChallenge();
5514
5514
  return this.submitAccountLink({
5515
5515
  accountId: root.accountId,
5516
5516
  rootPublicKey: root.publicKey,
5517
- nonce,
5518
- signature: await signAccountLink({ rootSecret, nonce })
5517
+ nonce: nonce2,
5518
+ signature: await signAccountLink({ rootSecret, nonce: nonce2 })
5519
5519
  });
5520
5520
  }
5521
5521
  /**
@@ -6200,7 +6200,7 @@ async function signWarrant(input) {
6200
6200
  const publicKey = await derivePublicKey(input.deviceSecret);
6201
6201
  const method = new TextEncoder().encode(input.method);
6202
6202
  const commitment = await intentHash(input.method, input.argsJson);
6203
- const nonce = u64le(input.nonce);
6203
+ const nonce2 = u64le(input.nonce);
6204
6204
  const notAfter = u64le(input.notAfter);
6205
6205
  const preimage = await domainHash(SIGN_DOMAIN2, [
6206
6206
  context,
@@ -6214,7 +6214,7 @@ async function signWarrant(input) {
6214
6214
  ...accountHeads,
6215
6215
  u64le(governanceFloor.length),
6216
6216
  ...governanceFloor,
6217
- nonce,
6217
+ nonce2,
6218
6218
  notAfter
6219
6219
  ]);
6220
6220
  const signature = new Uint8Array(
@@ -6234,7 +6234,7 @@ async function signWarrant(input) {
6234
6234
  ...accountHeads,
6235
6235
  u32le(governanceFloor.length),
6236
6236
  ...governanceFloor,
6237
- nonce,
6237
+ nonce2,
6238
6238
  notAfter,
6239
6239
  signature
6240
6240
  )
@@ -6291,7 +6291,7 @@ var RelayClient = class {
6291
6291
  */
6292
6292
  async execute(contextId, method, argsJson = {}) {
6293
6293
  const executor = this.executorAccount ?? (await this.describe(contextId)).executorAccount;
6294
- const nonce = await this.config.nonces.next();
6294
+ const nonce2 = await this.config.nonces.next();
6295
6295
  const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
6296
6296
  const warrant = await signWarrant({
6297
6297
  context: contextId,
@@ -6299,7 +6299,7 @@ var RelayClient = class {
6299
6299
  executor,
6300
6300
  method,
6301
6301
  argsJson,
6302
- nonce,
6302
+ nonce: nonce2,
6303
6303
  notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
6304
6304
  deviceSecret: this.config.deviceSecret
6305
6305
  });
@@ -6489,14 +6489,10 @@ async function defaultNonceSource(deviceSecret) {
6489
6489
  }
6490
6490
  }
6491
6491
 
6492
- // src/sealed/sealed.ts
6493
- var SEALED_PATH = "/sealed/v1";
6494
- var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
6495
- var VERSION = 1;
6496
- var NONCE_LEN = 12;
6497
- var REQUEST_DOMAIN = "calimero/sealed-http/v1/request";
6498
- var RESPONSE_DOMAIN = "calimero/sealed-http/v1/response";
6499
- var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
6492
+ // src/sealed/noise.ts
6493
+ var PROTOCOL_NAME = "Noise_NK_25519_AESGCM_SHA256";
6494
+ var DH_LEN = 32;
6495
+ var TAG_LEN = 16;
6500
6496
  var PKCS8_X25519_PREFIX = new Uint8Array([
6501
6497
  48,
6502
6498
  46,
@@ -6515,12 +6511,147 @@ var PKCS8_X25519_PREFIX = new Uint8Array([
6515
6511
  4,
6516
6512
  32
6517
6513
  ]);
6514
+ var SymmetricState = class _SymmetricState {
6515
+ constructor(h) {
6516
+ this.k = null;
6517
+ this.n = 0;
6518
+ this.ck = h;
6519
+ this.h = h;
6520
+ }
6521
+ static async initialize() {
6522
+ const name = new TextEncoder().encode(PROTOCOL_NAME);
6523
+ const h = new Uint8Array(32);
6524
+ h.set(name);
6525
+ return new _SymmetricState(h);
6526
+ }
6527
+ async mixHash(data) {
6528
+ this.h = await sha256(concat(this.h, data));
6529
+ }
6530
+ async mixKey(inputKeyMaterial) {
6531
+ const [ck, key] = await hkdf2(this.ck, inputKeyMaterial);
6532
+ this.ck = ck;
6533
+ this.k = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt", "decrypt"]);
6534
+ this.n = 0;
6535
+ }
6536
+ async encryptAndHash(plaintext) {
6537
+ if (!this.k) throw new Error("Noise: no cipher key yet");
6538
+ const ciphertext = new Uint8Array(
6539
+ await crypto.subtle.encrypt(
6540
+ { name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
6541
+ this.k,
6542
+ plaintext
6543
+ )
6544
+ );
6545
+ this.n += 1;
6546
+ await this.mixHash(ciphertext);
6547
+ return ciphertext;
6548
+ }
6549
+ async decryptAndHash(ciphertext) {
6550
+ if (!this.k) throw new Error("Noise: no cipher key yet");
6551
+ const plaintext = new Uint8Array(
6552
+ await crypto.subtle.decrypt(
6553
+ { name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
6554
+ this.k,
6555
+ ciphertext
6556
+ )
6557
+ );
6558
+ this.n += 1;
6559
+ await this.mixHash(ciphertext);
6560
+ return plaintext;
6561
+ }
6562
+ /** The initiator-to-responder key, then the responder-to-initiator key. */
6563
+ split() {
6564
+ return hkdf2(this.ck, new Uint8Array());
6565
+ }
6566
+ };
6567
+ async function initiate(remoteStatic, prologue, fixedEphemeral) {
6568
+ const state = await SymmetricState.initialize();
6569
+ await state.mixHash(prologue);
6570
+ await state.mixHash(remoteStatic);
6571
+ const ephemeral = await x25519KeyPair(fixedEphemeral);
6572
+ await state.mixHash(ephemeral.publicKey);
6573
+ await state.mixKey(await dh(ephemeral.privateKey, remoteStatic));
6574
+ const message1 = concat(ephemeral.publicKey, await state.encryptAndHash(new Uint8Array()));
6575
+ return {
6576
+ message1,
6577
+ async finish(message2) {
6578
+ if (message2.length < DH_LEN + TAG_LEN) throw new Error("Noise: message 2 is too short");
6579
+ const remoteEphemeral = message2.slice(0, DH_LEN);
6580
+ await state.mixHash(remoteEphemeral);
6581
+ await state.mixKey(await dh(ephemeral.privateKey, remoteEphemeral));
6582
+ const payload = await state.decryptAndHash(message2.slice(DH_LEN));
6583
+ return { payload, split: await state.split() };
6584
+ }
6585
+ };
6586
+ }
6587
+ async function x25519KeyPair(secret) {
6588
+ if (!secret) {
6589
+ const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
6590
+ "deriveBits"
6591
+ ]);
6592
+ const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
6593
+ return { privateKey: pair.privateKey, publicKey };
6594
+ }
6595
+ const privateKey = await crypto.subtle.importKey(
6596
+ "pkcs8",
6597
+ concat(PKCS8_X25519_PREFIX, secret),
6598
+ { name: "X25519" },
6599
+ true,
6600
+ ["deriveBits"]
6601
+ );
6602
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey);
6603
+ return { privateKey, publicKey: base64UrlDecode(jwk.x) };
6604
+ }
6605
+ async function dh(privateKey, publicKey) {
6606
+ const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
6607
+ return new Uint8Array(
6608
+ await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
6609
+ );
6610
+ }
6611
+ function noiseNonce(n) {
6612
+ const nonce2 = new Uint8Array(12);
6613
+ new DataView(nonce2.buffer).setBigUint64(4, BigInt(n), false);
6614
+ return nonce2;
6615
+ }
6616
+ async function sha256(data) {
6617
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
6618
+ }
6619
+ async function hmacSha256(key, data) {
6620
+ const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [
6621
+ "sign"
6622
+ ]);
6623
+ return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, data));
6624
+ }
6625
+ async function hkdf2(chainingKey, inputKeyMaterial) {
6626
+ const temp = await hmacSha256(chainingKey, inputKeyMaterial);
6627
+ const first = await hmacSha256(temp, new Uint8Array([1]));
6628
+ const second = await hmacSha256(temp, concat(first, new Uint8Array([2])));
6629
+ return [first, second];
6630
+ }
6631
+ function base64UrlDecode(value) {
6632
+ const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
6633
+ const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
6634
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
6635
+ }
6636
+
6637
+ // src/sealed/sealed.ts
6638
+ var HANDSHAKE_PATH = "/sealed/v2/handshake";
6639
+ var SEALED_PATH = "/sealed/v2";
6640
+ var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
6641
+ var VERSION = 2;
6642
+ var PROLOGUE = new TextEncoder().encode("calimero/sealed-http/v2");
6643
+ var SESSION_ID_LEN = 16;
6644
+ var HEADER_LEN = 1 + SESSION_ID_LEN + 8;
6645
+ var FRAME_HEAD = 0;
6646
+ var FRAME_DATA = 1;
6647
+ var FRAME_END = 2;
6648
+ var RENEW_BEFORE_MS = 6e4;
6649
+ var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
6650
+ var NULL_BODY_STATUSES = [101, 204, 205, 304];
6518
6651
  var utf8 = new TextEncoder();
6519
6652
  var StaleTransportKeyError = class extends Error {
6520
6653
  constructor() {
6521
- super(
6522
- "The node no longer holds the transport key this request was sealed to; attest again for the current one"
6523
- );
6654
+ super("The node no longer holds the attested transport key; attest again for the current one");
6524
6655
  this.name = "StaleTransportKeyError";
6525
6656
  }
6526
6657
  };
@@ -6543,9 +6674,9 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6543
6674
  if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
6544
6675
  throw new Error("applicationId and applicationHash are given together or not at all");
6545
6676
  }
6546
- const nonce = hex(crypto.getRandomValues(new Uint8Array(32)));
6677
+ const nonce2 = hex(crypto.getRandomValues(new Uint8Array(32)));
6547
6678
  const attested = await admin.teeAttest({
6548
- nonce,
6679
+ nonce: nonce2,
6549
6680
  applicationId: options.applicationId,
6550
6681
  bindTransportKey: true
6551
6682
  });
@@ -6555,7 +6686,7 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6555
6686
  const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
6556
6687
  const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
6557
6688
  const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
6558
- if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
6689
+ if (!await verify({ quoteB64: attested.quoteB64, nonce: nonce2, reportDataSuffix })) {
6559
6690
  throw new Error("The attestation did not verify, so its transport key is not trusted");
6560
6691
  }
6561
6692
  return transportKey;
@@ -6563,12 +6694,37 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6563
6694
  function createSealedFetch(options) {
6564
6695
  const base = new URL(options.baseUrl);
6565
6696
  const basePath = base.pathname.replace(/\/+$/, "");
6566
- const sealedUrl = `${base.origin}${basePath}${SEALED_PATH}`;
6567
- const transportKey = options.transportPublicKey;
6568
- if (transportKey.length !== 32) {
6569
- throw new Error("transportPublicKey must be 32 bytes");
6570
- }
6697
+ const nodeUrl = `${base.origin}${basePath}`;
6571
6698
  const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
6699
+ const attest = typeof options.transportPublicKey === "function" ? options.transportPublicKey : void 0;
6700
+ let transportKey = attest ? Promise.resolve().then(attest) : Promise.resolve(options.transportPublicKey);
6701
+ let session;
6702
+ const openSession = async () => {
6703
+ try {
6704
+ return await handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
6705
+ } catch (error) {
6706
+ if (!(error instanceof StaleTransportKeyError) || !attest) throw error;
6707
+ transportKey = Promise.resolve().then(attest);
6708
+ return handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
6709
+ }
6710
+ };
6711
+ const currentSession = async () => {
6712
+ const pending = session ?? renew(void 0);
6713
+ const open = await pending;
6714
+ if (Date.now() < open.renewAt) return [open, pending];
6715
+ const renewed = renew(pending);
6716
+ return [await renewed, renewed];
6717
+ };
6718
+ const renew = (stale) => {
6719
+ if (session === stale) {
6720
+ const opening = openSession();
6721
+ session = opening;
6722
+ opening.catch(() => {
6723
+ if (session === opening) session = void 0;
6724
+ });
6725
+ }
6726
+ return session;
6727
+ };
6572
6728
  return async (input, init) => {
6573
6729
  const request = new Request(input, init);
6574
6730
  const url = new URL(request.url);
@@ -6577,126 +6733,190 @@ function createSealedFetch(options) {
6577
6733
  }
6578
6734
  const headers = [];
6579
6735
  request.headers.forEach((value, name) => headers.push([name, value]));
6580
- const head = {
6581
- method: request.method,
6582
- path: url.pathname.slice(basePath.length) + url.search,
6583
- headers,
6584
- ts: Math.floor(Date.now() / 1e3)
6585
- };
6736
+ const head = { method: request.method, path: url.pathname.slice(basePath.length) + url.search, headers };
6586
6737
  const body = new Uint8Array(await request.arrayBuffer());
6587
- const exchange = await sealRequest(transportKey, head, body);
6588
- const response = await baseFetch(sealedUrl, {
6589
- method: "POST",
6590
- headers: { "content-type": SEALED_CONTENT_TYPE },
6591
- body: exchange.envelope,
6592
- signal: request.signal
6593
- });
6594
- if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
6595
- throw await envelopeRefusal(response);
6738
+ const send = async (open2) => {
6739
+ const { requestId, header, envelope } = await sealRequest(open2, head, body);
6740
+ const response = await baseFetch(`${nodeUrl}${SEALED_PATH}`, {
6741
+ method: "POST",
6742
+ headers: { "content-type": SEALED_CONTENT_TYPE },
6743
+ body: envelope,
6744
+ signal: request.signal
6745
+ });
6746
+ if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
6747
+ throw await envelopeRefusal(response);
6748
+ }
6749
+ return openResponse(response, open2.responseKey, header, requestId);
6750
+ };
6751
+ const [open, pending] = await currentSession();
6752
+ try {
6753
+ return await send(open);
6754
+ } catch (error) {
6755
+ if (!(error instanceof SealedTransportError) || error.code !== "unknown_session") throw error;
6756
+ return send(await renew(pending));
6596
6757
  }
6597
- const opened = await exchange.open(new Uint8Array(await response.arrayBuffer()));
6598
- const nullBody = [101, 204, 205, 304].includes(opened.head.status);
6599
- return new Response(nullBody ? null : opened.body, {
6600
- status: opened.head.status,
6601
- headers: opened.head.headers
6602
- });
6603
6758
  };
6604
6759
  }
6605
- async function envelopeRefusal(response) {
6606
- let code;
6607
- let message = `Sealed request refused: HTTP ${response.status}`;
6608
- try {
6609
- const body = await response.json();
6610
- code = body?.error?.code;
6611
- if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
6612
- } catch {
6760
+ async function checkedKey(key) {
6761
+ const bytes = await key;
6762
+ if (bytes.length !== 32) throw new Error("transportPublicKey must be 32 bytes");
6763
+ return bytes;
6764
+ }
6765
+ async function handshake(baseFetch, nodeUrl, transportKey) {
6766
+ const noise = await initiate(transportKey, PROLOGUE);
6767
+ const response = await baseFetch(`${nodeUrl}${HANDSHAKE_PATH}`, {
6768
+ method: "POST",
6769
+ headers: { "content-type": SEALED_CONTENT_TYPE },
6770
+ body: concat(new Uint8Array([VERSION]), transportKey, noise.message1)
6771
+ });
6772
+ if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
6773
+ throw await envelopeRefusal(response);
6613
6774
  }
6614
- if (code === "stale_transport_key") return new StaleTransportKeyError();
6615
- return new SealedTransportError(response.status, code, message);
6775
+ const reply = new Uint8Array(await response.arrayBuffer());
6776
+ return sessionFrom(noise, reply);
6616
6777
  }
6617
- async function sealRequest(transportKey, head, body, fixed) {
6618
- const { privateKey, publicKey } = await clientKeyPair(fixed?.clientSecret);
6619
- const transport = await crypto.subtle.importKey("raw", transportKey, { name: "X25519" }, false, []);
6620
- const shared = new Uint8Array(
6621
- await crypto.subtle.deriveBits({ name: "X25519", public: transport }, privateKey, 256)
6622
- );
6623
- if (shared.every((byte) => byte === 0)) {
6624
- throw new Error("The transport key is a low-order point");
6625
- }
6626
- const salt = concat(transportKey, publicKey);
6627
- const requestKey = await exchangeKey(shared, salt, REQUEST_DOMAIN, "encrypt");
6628
- const responseKey = await exchangeKey(shared, salt, RESPONSE_DOMAIN, "decrypt");
6629
- const nonce = fixed?.nonce ?? crypto.getRandomValues(new Uint8Array(NONCE_LEN));
6630
- const header = concat(new Uint8Array([VERSION]), transportKey, publicKey, nonce);
6631
- const ciphertext = await crypto.subtle.encrypt(
6632
- { name: "AES-GCM", iv: nonce, additionalData: header },
6633
- requestKey,
6634
- joinInner(head, body)
6635
- );
6636
- const responseAad = concat(new Uint8Array([VERSION]), salt);
6778
+ async function sessionFrom(noise, reply) {
6779
+ if (reply[0] !== VERSION) throw new Error("The handshake reply is malformed");
6780
+ let finished;
6781
+ try {
6782
+ finished = await noise.finish(reply.slice(1));
6783
+ } catch {
6784
+ throw new Error("The handshake reply did not open: it did not come from the attested node");
6785
+ }
6786
+ const { payload, split } = finished;
6787
+ if (payload.length !== SESSION_ID_LEN + 4) throw new Error("The handshake reply is malformed");
6788
+ const sessionId = payload.slice(0, SESSION_ID_LEN);
6789
+ const lifetimeSecs = new DataView(payload.buffer, payload.byteOffset).getUint32(SESSION_ID_LEN, false);
6790
+ const [requestKey, responseKey] = await Promise.all([
6791
+ crypto.subtle.importKey("raw", split[0], "AES-GCM", false, ["encrypt"]),
6792
+ crypto.subtle.importKey("raw", split[1], "AES-GCM", false, ["decrypt"])
6793
+ ]);
6794
+ split[0].fill(0);
6795
+ split[1].fill(0);
6637
6796
  return {
6638
- envelope: concat(header, new Uint8Array(ciphertext)),
6639
- async open(sealed) {
6640
- if (sealed.length < 1 + NONCE_LEN + 16 || sealed[0] !== VERSION) {
6641
- throw new Error("The sealed response is malformed");
6642
- }
6643
- let plaintext;
6644
- try {
6645
- plaintext = new Uint8Array(
6646
- await crypto.subtle.decrypt(
6647
- { name: "AES-GCM", iv: sealed.slice(1, 1 + NONCE_LEN), additionalData: responseAad },
6648
- responseKey,
6649
- sealed.slice(1 + NONCE_LEN)
6650
- )
6651
- );
6652
- } catch {
6653
- throw new Error("The sealed response did not open: it was not sealed by the attested node");
6654
- }
6655
- const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
6656
- const len = view.getUint32(0, false);
6657
- const head2 = JSON.parse(new TextDecoder().decode(plaintext.slice(4, 4 + len)));
6658
- return { head: head2, body: plaintext.slice(4 + len) };
6797
+ requestKey,
6798
+ responseKey,
6799
+ nextRequestId: 1,
6800
+ renewAt: Date.now() + lifetimeSecs * 1e3 - RENEW_BEFORE_MS,
6801
+ header(requestId) {
6802
+ const header = new Uint8Array(HEADER_LEN);
6803
+ header[0] = VERSION;
6804
+ header.set(sessionId, 1);
6805
+ new DataView(header.buffer).setBigUint64(1 + SESSION_ID_LEN, BigInt(requestId), false);
6806
+ return header;
6659
6807
  }
6660
6808
  };
6661
6809
  }
6810
+ async function sealRequest(session, head, body) {
6811
+ const requestId = session.nextRequestId;
6812
+ session.nextRequestId += 1;
6813
+ const header = session.header(requestId);
6814
+ const sealed = await crypto.subtle.encrypt(
6815
+ { name: "AES-GCM", iv: nonce(requestId, 0), additionalData: header },
6816
+ session.requestKey,
6817
+ joinInner(head, body)
6818
+ );
6819
+ return { requestId, header, envelope: concat(header, new Uint8Array(sealed)) };
6820
+ }
6821
+ function nonce(requestId, index) {
6822
+ const bytes = new Uint8Array(12);
6823
+ const view = new DataView(bytes.buffer);
6824
+ view.setBigUint64(0, BigInt(requestId), false);
6825
+ view.setUint32(8, index, false);
6826
+ return bytes;
6827
+ }
6662
6828
  function joinInner(head, body) {
6663
6829
  const encoded = utf8.encode(JSON.stringify(head));
6664
6830
  const len = new Uint8Array(4);
6665
6831
  new DataView(len.buffer).setUint32(0, encoded.length, false);
6666
6832
  return concat(len, encoded, body);
6667
6833
  }
6668
- async function exchangeKey(shared, salt, domain, usage) {
6669
- const ikm = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
6670
- return crypto.subtle.deriveKey(
6671
- { name: "HKDF", hash: "SHA-256", salt, info: utf8.encode(domain) },
6672
- ikm,
6673
- { name: "AES-GCM", length: 256 },
6674
- false,
6675
- [usage]
6676
- );
6834
+ async function openResponse(response, responseKey, header, requestId) {
6835
+ if (!response.body) throw new Error("The sealed response has no body");
6836
+ const frames = frameReader(response.body.getReader());
6837
+ let index = 0;
6838
+ const next = async () => {
6839
+ const sealed = await frames.next();
6840
+ if (!sealed) return null;
6841
+ let plain;
6842
+ try {
6843
+ plain = new Uint8Array(
6844
+ await crypto.subtle.decrypt(
6845
+ { name: "AES-GCM", iv: nonce(requestId, index), additionalData: header },
6846
+ responseKey,
6847
+ sealed
6848
+ )
6849
+ );
6850
+ } catch {
6851
+ throw new Error("A sealed response frame did not open: it was not sealed by the attested node");
6852
+ }
6853
+ index += 1;
6854
+ return { kind: plain[0], data: plain.slice(1) };
6855
+ };
6856
+ const first = await next();
6857
+ if (!first || first.kind !== FRAME_HEAD) throw new Error("The sealed response is malformed");
6858
+ const head = JSON.parse(new TextDecoder().decode(first.data));
6859
+ if (NULL_BODY_STATUSES.includes(head.status)) {
6860
+ await frames.cancel();
6861
+ return new Response(null, { status: head.status, headers: head.headers });
6862
+ }
6863
+ const body = new ReadableStream({
6864
+ async pull(controller) {
6865
+ try {
6866
+ const frame = await next();
6867
+ if (!frame) {
6868
+ controller.error(new Error("The sealed response was cut short"));
6869
+ } else if (frame.kind === FRAME_DATA) {
6870
+ controller.enqueue(frame.data);
6871
+ } else if (frame.kind === FRAME_END) {
6872
+ controller.close();
6873
+ await frames.cancel();
6874
+ } else {
6875
+ controller.error(new Error("The sealed response is malformed"));
6876
+ }
6877
+ } catch (error) {
6878
+ controller.error(error);
6879
+ }
6880
+ },
6881
+ cancel() {
6882
+ return frames.cancel();
6883
+ }
6884
+ });
6885
+ return new Response(body, { status: head.status, headers: head.headers });
6677
6886
  }
6678
- async function clientKeyPair(secret) {
6679
- if (!secret) {
6680
- const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
6681
- "deriveBits"
6682
- ]);
6683
- const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
6684
- return { privateKey: pair.privateKey, publicKey };
6685
- }
6686
- const privateKey = await crypto.subtle.importKey(
6687
- "pkcs8",
6688
- concat(PKCS8_X25519_PREFIX, secret),
6689
- { name: "X25519" },
6690
- true,
6691
- ["deriveBits"]
6692
- );
6693
- const jwk = await crypto.subtle.exportKey("jwk", privateKey);
6694
- return { privateKey, publicKey: base64UrlDecode(jwk.x) };
6887
+ function frameReader(reader) {
6888
+ let buffer = new Uint8Array();
6889
+ const fill = async (length) => {
6890
+ while (buffer.length < length) {
6891
+ const { done, value } = await reader.read();
6892
+ if (done) return false;
6893
+ buffer = concat(buffer, value);
6894
+ }
6895
+ return true;
6896
+ };
6897
+ return {
6898
+ async next() {
6899
+ if (!await fill(4)) return null;
6900
+ const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
6901
+ if (!await fill(4 + length)) return null;
6902
+ const frame = buffer.slice(4, 4 + length);
6903
+ buffer = buffer.slice(4 + length);
6904
+ return frame;
6905
+ },
6906
+ cancel: () => reader.cancel()
6907
+ };
6695
6908
  }
6696
- function base64UrlDecode(value) {
6697
- const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
6698
- const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
6699
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
6909
+ async function envelopeRefusal(response) {
6910
+ let code;
6911
+ let message = `Sealed request refused: HTTP ${response.status}`;
6912
+ try {
6913
+ const body = await response.json();
6914
+ code = body?.error?.code;
6915
+ if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
6916
+ } catch {
6917
+ }
6918
+ if (code === "stale_transport_key") return new StaleTransportKeyError();
6919
+ return new SealedTransportError(response.status, code, message);
6700
6920
  }
6701
6921
 
6702
6922
  // src/namespace-op/namespace-op.ts
@@ -7033,6 +7253,7 @@ export {
7033
7253
  CloudClient,
7034
7254
  DEFAULT_LOCAL_NODE_PORTS,
7035
7255
  EphemeralClient,
7256
+ HANDSHAKE_PATH,
7036
7257
  HTTPError,
7037
7258
  IntentRefusedError,
7038
7259
  LocalStorageTokenStore,