@calimero-network/mero-js 19.19.0 → 19.21.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
@@ -1668,6 +1668,15 @@ var AdminApiClient = class {
1668
1668
  const response = await this.httpClient.get(`/admin-api/groups/${groupId}/settings/tee-admission-policy`);
1669
1669
  return response.data ?? response;
1670
1670
  }
1671
+ /**
1672
+ * Sets which admitted TEEs may author as the TEE authority. Pass
1673
+ * `{ allowedMrtd: [] }` to turn TEE authorship off. `groupId` must be a
1674
+ * namespace root; the node refuses a subgroup. The node exposes no read-back
1675
+ * route for this policy.
1676
+ */
1677
+ async setTeeAuthoringPolicy(groupId, request) {
1678
+ await this.httpClient.put(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`, request);
1679
+ }
1671
1680
  // ---- Group / member / context metadata ----
1672
1681
  async setGroupMetadata(groupId, request) {
1673
1682
  await this.httpClient.put(`/admin-api/groups/${groupId}/metadata`, request);
@@ -6480,6 +6489,216 @@ async function defaultNonceSource(deviceSecret) {
6480
6489
  }
6481
6490
  }
6482
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
+
6483
6702
  // src/namespace-op/namespace-op.ts
6484
6703
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
6485
6704
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
@@ -6822,8 +7041,12 @@ export {
6822
7041
  RelayClient,
6823
7042
  RpcClient,
6824
7043
  RpcError,
7044
+ SEALED_CONTENT_TYPE,
7045
+ SEALED_PATH,
6825
7046
  SIGNED_NAMESPACE_OP_SCHEMA_VERSION,
7047
+ SealedTransportError,
6826
7048
  SseClient,
7049
+ StaleTransportKeyError,
6827
7050
  WebHttpClient,
6828
7051
  WsClient,
6829
7052
  accountForRoot,
@@ -6851,11 +7074,13 @@ export {
6851
7074
  createNodeHttpClient,
6852
7075
  createProofSigner,
6853
7076
  createRetryableMethod,
7077
+ createSealedFetch,
6854
7078
  createTimeoutSignal,
6855
7079
  createUniversalHttpClient,
6856
7080
  deviceCertPayload,
6857
7081
  discoverLocalNodes,
6858
7082
  encodeSignedInvitation,
7083
+ fetchAttestedTransportKey,
6859
7084
  generateAccountRoot,
6860
7085
  generateSessionKey,
6861
7086
  hasCap,
@@ -6878,6 +7103,7 @@ export {
6878
7103
  signRequest,
6879
7104
  signRoutingChallenge,
6880
7105
  signWarrant,
7106
+ transportKeyBinding,
6881
7107
  withCap,
6882
7108
  withRetry,
6883
7109
  withoutCap