@calimero-network/mero-js 19.20.0 → 19.22.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.d.ts CHANGED
@@ -15,6 +15,7 @@ export type { SseEventData, WsEventData, AppVersionChangedEvent, GroupMembership
15
15
  export { EphemeralClient, jsonCodec } from "./ephemeral/index.js";
16
16
  export type { Codec, EphemeralEntry } from "./ephemeral/index.js";
17
17
  export * from "./cloud/index.js";
18
+ export * from "./sealed/index.js";
18
19
  export * from "./relay/index.js";
19
20
  export { signWarrant, intentHash } from "./warrant/index.js";
20
21
  export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from "./account/index.js";
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ export { SseClient, WsClient } from "./events/index.js";
20
20
  export { EphemeralClient, jsonCodec } from "./ephemeral/index.js";
21
21
  // Cloud client — namespaces, relays, HA, and the cloud sign-in path
22
22
  export * from "./cloud/index.js";
23
+ // Sealed transport — requests encrypted to a TEE node's attested key
24
+ export * from "./sealed/index.js";
23
25
  // Relay client — write through delegated execution, holding only a signing key
24
26
  export * from "./relay/index.js";
25
27
  // Warrant signing — mint the author's consent for a relay to run one intent
package/dist/index.mjs CHANGED
@@ -6489,6 +6489,216 @@ 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";
6500
+ var PKCS8_X25519_PREFIX = new Uint8Array([
6501
+ 48,
6502
+ 46,
6503
+ 2,
6504
+ 1,
6505
+ 0,
6506
+ 48,
6507
+ 5,
6508
+ 6,
6509
+ 3,
6510
+ 43,
6511
+ 101,
6512
+ 110,
6513
+ 4,
6514
+ 34,
6515
+ 4,
6516
+ 32
6517
+ ]);
6518
+ var utf8 = new TextEncoder();
6519
+ var StaleTransportKeyError = class extends Error {
6520
+ constructor() {
6521
+ super(
6522
+ "The node no longer holds the transport key this request was sealed to; attest again for the current one"
6523
+ );
6524
+ this.name = "StaleTransportKeyError";
6525
+ }
6526
+ };
6527
+ var SealedTransportError = class extends Error {
6528
+ constructor(status, code, message) {
6529
+ super(message);
6530
+ this.status = status;
6531
+ this.code = code;
6532
+ this.name = "SealedTransportError";
6533
+ }
6534
+ };
6535
+ async function transportKeyBinding(inner, transportKey) {
6536
+ const digest = await crypto.subtle.digest(
6537
+ "SHA-256",
6538
+ concat(utf8.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
6539
+ );
6540
+ return new Uint8Array(digest);
6541
+ }
6542
+ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6543
+ if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
6544
+ throw new Error("applicationId and applicationHash are given together or not at all");
6545
+ }
6546
+ const nonce = hex(crypto.getRandomValues(new Uint8Array(32)));
6547
+ const attested = await admin.teeAttest({
6548
+ nonce,
6549
+ applicationId: options.applicationId,
6550
+ bindTransportKey: true
6551
+ });
6552
+ if (!attested.transportPublicKey) {
6553
+ throw new Error("The node did not report a transport key; it predates sealed transport");
6554
+ }
6555
+ const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
6556
+ const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
6557
+ const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
6558
+ if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
6559
+ throw new Error("The attestation did not verify, so its transport key is not trusted");
6560
+ }
6561
+ return transportKey;
6562
+ }
6563
+ function createSealedFetch(options) {
6564
+ const base = new URL(options.baseUrl);
6565
+ 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
+ }
6571
+ const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
6572
+ return async (input, init) => {
6573
+ const request = new Request(input, init);
6574
+ const url = new URL(request.url);
6575
+ if (url.origin !== base.origin || !url.pathname.startsWith(`${basePath}/`)) {
6576
+ throw new Error(`Refusing to send ${url.href} outside the sealed node at ${options.baseUrl}`);
6577
+ }
6578
+ const headers = [];
6579
+ 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
+ };
6586
+ 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);
6596
+ }
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
+ };
6604
+ }
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 {
6613
+ }
6614
+ if (code === "stale_transport_key") return new StaleTransportKeyError();
6615
+ return new SealedTransportError(response.status, code, message);
6616
+ }
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);
6637
+ 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) };
6659
+ }
6660
+ };
6661
+ }
6662
+ function joinInner(head, body) {
6663
+ const encoded = utf8.encode(JSON.stringify(head));
6664
+ const len = new Uint8Array(4);
6665
+ new DataView(len.buffer).setUint32(0, encoded.length, false);
6666
+ return concat(len, encoded, body);
6667
+ }
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
+ );
6677
+ }
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) };
6695
+ }
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));
6700
+ }
6701
+
6492
6702
  // src/namespace-op/namespace-op.ts
6493
6703
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
6494
6704
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
@@ -6831,8 +7041,12 @@ export {
6831
7041
  RelayClient,
6832
7042
  RpcClient,
6833
7043
  RpcError,
7044
+ SEALED_CONTENT_TYPE,
7045
+ SEALED_PATH,
6834
7046
  SIGNED_NAMESPACE_OP_SCHEMA_VERSION,
7047
+ SealedTransportError,
6835
7048
  SseClient,
7049
+ StaleTransportKeyError,
6836
7050
  WebHttpClient,
6837
7051
  WsClient,
6838
7052
  accountForRoot,
@@ -6860,11 +7074,13 @@ export {
6860
7074
  createNodeHttpClient,
6861
7075
  createProofSigner,
6862
7076
  createRetryableMethod,
7077
+ createSealedFetch,
6863
7078
  createTimeoutSignal,
6864
7079
  createUniversalHttpClient,
6865
7080
  deviceCertPayload,
6866
7081
  discoverLocalNodes,
6867
7082
  encodeSignedInvitation,
7083
+ fetchAttestedTransportKey,
6868
7084
  generateAccountRoot,
6869
7085
  generateSessionKey,
6870
7086
  hasCap,
@@ -6887,6 +7103,7 @@ export {
6887
7103
  signRequest,
6888
7104
  signRoutingChallenge,
6889
7105
  signWarrant,
7106
+ transportKeyBinding,
6890
7107
  withCap,
6891
7108
  withRetry,
6892
7109
  withoutCap