@calimero-network/mero-js 19.25.0 → 19.27.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
@@ -1848,6 +1848,10 @@ var AdminApiClient = class {
1848
1848
  async teeAttest(request) {
1849
1849
  return unwrap(await this.httpClient.post("/admin-api/tee/attest", request));
1850
1850
  }
1851
+ /**
1852
+ * @deprecated Nodes no longer serve `/admin-api/tee/verify-quote` (removed in
1853
+ * core#3262), so this fails. Verify quotes with `createQuoteVerifier`.
1854
+ */
1851
1855
  async teeVerifyQuote(request) {
1852
1856
  return unwrap(
1853
1857
  await this.httpClient.post("/admin-api/tee/verify-quote", request)
@@ -6463,92 +6467,6 @@ function createLocalStorageNonceSource(key, storage) {
6463
6467
  };
6464
6468
  }
6465
6469
 
6466
- // src/cloud/connect.ts
6467
- async function connectCloud(options) {
6468
- if (!options.sessionToken && !options.googleIdToken) {
6469
- throw new Error("connectCloud needs either a sessionToken or a googleIdToken");
6470
- }
6471
- const cloud = new CloudClient({
6472
- cloudBaseUrl: options.cloudBaseUrl,
6473
- sessionToken: options.sessionToken,
6474
- onSession: options.onSession,
6475
- fetch: options.fetch,
6476
- timeoutMs: options.timeoutMs
6477
- });
6478
- if (!options.sessionToken && options.googleIdToken) {
6479
- await cloud.signInWithGoogle(options.googleIdToken);
6480
- }
6481
- const namespaces = await cloud.getMyNamespaces();
6482
- const namespace = pickNamespace(namespaces, options.namespaceId);
6483
- const relays = await cloud.getNamespaceRelays(namespace.namespaceId);
6484
- const relayInfo = relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
6485
- if (!relayInfo) {
6486
- throw new Error(explainNoRelay(namespace, relays));
6487
- }
6488
- const relay = new RelayClient({
6489
- // Checked by `findExecutingRelay`, which is the only thing that returns a
6490
- // relay — narrowed here rather than re-validated, so the invariant lives in
6491
- // one place.
6492
- relayUrl: relayInfo.relayUrl,
6493
- executorAccount: relayInfo.executorAccount,
6494
- authorAccount: options.authorAccount,
6495
- authorProof: options.authorProof,
6496
- deviceSecret: options.deviceSecret,
6497
- nonces: options.nonces ?? await defaultNonceSource(options.deviceSecret),
6498
- ttlSeconds: options.ttlSeconds,
6499
- fetch: options.fetch,
6500
- timeoutMs: options.timeoutMs
6501
- });
6502
- return {
6503
- cloud,
6504
- relay,
6505
- namespaceId: namespace.namespaceId,
6506
- namespace,
6507
- relayInfo,
6508
- execute: (contextId, method, argsJson) => relay.execute(contextId, method, argsJson)
6509
- };
6510
- }
6511
- function pickNamespace(namespaces, requested) {
6512
- if (requested) {
6513
- const found = namespaces.find((n) => n.namespaceId === requested);
6514
- if (!found) {
6515
- throw new Error(
6516
- `this account owns no namespace ${requested}; it owns: ${namespaces.map((n) => n.namespaceId).join(", ") || "(none)"}`
6517
- );
6518
- }
6519
- return found;
6520
- }
6521
- if (namespaces.length === 0) {
6522
- throw new Error(
6523
- "this account owns no namespaces in the cloud: claim one first, then enable HA on it so a relay is assigned"
6524
- );
6525
- }
6526
- if (namespaces.length > 1) {
6527
- throw new Error(
6528
- `this account owns ${namespaces.length} namespaces \u2014 pass namespaceId to say which: ${namespaces.map((n) => n.namespaceId).join(", ")}`
6529
- );
6530
- }
6531
- return namespaces[0];
6532
- }
6533
- function explainNoRelay(namespace, relays) {
6534
- if (relays.length === 0) {
6535
- return namespace.haStatus === "enabled" ? `namespace ${namespace.namespaceId} has HA enabled but no relay has been assigned to it yet \u2014 retry shortly` : `namespace ${namespace.namespaceId} has no relays: enable HA on it in the cloud to have an attested node assigned`;
6536
- }
6537
- const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
6538
- if (waitingOnGrant.length === relays.length) {
6539
- return `every relay on namespace ${namespace.namespaceId} is waiting for the authorship grant: an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(", ") || "the relay account"} \u2014 with AdminClient.grantAuthorship(namespaceId, account) for these relays, and AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`;
6540
- }
6541
- return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet \u2014 retry shortly`;
6542
- }
6543
- async function defaultNonceSource(deviceSecret) {
6544
- const key = `mero-js:warrant-nonce:${hex(await derivePublicKey(deviceSecret))}`;
6545
- try {
6546
- return createLocalStorageNonceSource(key);
6547
- } catch {
6548
- return createMemoryNonceSource(1);
6549
- }
6550
- }
6551
-
6552
6470
  // src/sealed/noise.ts
6553
6471
  var PROTOCOL_NAME = "Noise_NK_25519_AESGCM_SHA256";
6554
6472
  var DH_LEN = 32;
@@ -6646,9 +6564,16 @@ async function initiate(remoteStatic, prologue, fixedEphemeral) {
6646
6564
  }
6647
6565
  async function x25519KeyPair(secret) {
6648
6566
  if (!secret) {
6649
- const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
6650
- "deriveBits"
6651
- ]);
6567
+ let pair;
6568
+ try {
6569
+ pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
6570
+ "deriveBits"
6571
+ ]);
6572
+ } catch (error) {
6573
+ throw new Error(
6574
+ `Sealed transport needs WebCrypto with X25519, which this runtime lacks (${error?.name ?? "no crypto.subtle"}). Current browsers and Node have it; in React Native, install react-native-quick-crypto as the global crypto.`
6575
+ );
6576
+ }
6652
6577
  const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
6653
6578
  return { privateKey: pair.privateKey, publicKey };
6654
6579
  }
@@ -6741,7 +6666,8 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6741
6666
  const attested = await admin.teeAttest({
6742
6667
  nonce: nonce2,
6743
6668
  applicationId: options.applicationId,
6744
- bindTransportKey: true
6669
+ bindTransportKey: true,
6670
+ ...verify.includeCollateral ? { includeCollateral: true } : {}
6745
6671
  });
6746
6672
  if (!attested.transportPublicKey) {
6747
6673
  throw new Error("The node did not report a transport key; it predates sealed transport");
@@ -6749,11 +6675,44 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6749
6675
  const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
6750
6676
  const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
6751
6677
  const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
6752
- if (!await verify({ quoteB64: attested.quoteB64, nonce: nonce2, reportDataSuffix })) {
6678
+ const verified = await verify({
6679
+ quoteB64: attested.quoteB64,
6680
+ nonce: nonce2,
6681
+ reportDataSuffix,
6682
+ collateral: attested.collateral
6683
+ });
6684
+ if (!verified) {
6753
6685
  throw new Error("The attestation did not verify, so its transport key is not trusted");
6754
6686
  }
6755
6687
  return transportKey;
6756
6688
  }
6689
+ function createAttestedSealedFetch(options) {
6690
+ const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
6691
+ const attestUrl = `${options.baseUrl.replace(/\/+$/, "")}/admin-api/tee/attest`;
6692
+ const admin = {
6693
+ async teeAttest(request) {
6694
+ const response = await baseFetch(attestUrl, {
6695
+ method: "POST",
6696
+ headers: { "content-type": "application/json" },
6697
+ body: JSON.stringify(request)
6698
+ });
6699
+ if (!response.ok) {
6700
+ throw new Error(`The node refused to attest: HTTP ${response.status}`);
6701
+ }
6702
+ const body = await response.json();
6703
+ if (!body.data) throw new Error("The attestation response is malformed");
6704
+ return body.data;
6705
+ }
6706
+ };
6707
+ return createSealedFetch({
6708
+ baseUrl: options.baseUrl,
6709
+ transportPublicKey: () => fetchAttestedTransportKey(admin, options.verify, {
6710
+ applicationId: options.applicationId,
6711
+ applicationHash: options.applicationHash
6712
+ }),
6713
+ fetch: baseFetch
6714
+ });
6715
+ }
6757
6716
  function createSealedFetch(options) {
6758
6717
  const base = new URL(options.baseUrl);
6759
6718
  const basePath = base.pathname.replace(/\/+$/, "");
@@ -6761,6 +6720,7 @@ function createSealedFetch(options) {
6761
6720
  const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
6762
6721
  const attest = typeof options.transportPublicKey === "function" ? options.transportPublicKey : void 0;
6763
6722
  let transportKey = attest ? Promise.resolve().then(attest) : Promise.resolve(options.transportPublicKey);
6723
+ transportKey.catch(() => void 0);
6764
6724
  let session;
6765
6725
  const openSession = async () => {
6766
6726
  try {
@@ -7011,6 +6971,175 @@ async function envelopeRefusal(response) {
7011
6971
  return new SealedTransportError(response.status, code, message, retryAfterMs);
7012
6972
  }
7013
6973
 
6974
+ // src/cloud/connect.ts
6975
+ async function connectCloud(options) {
6976
+ if (!options.sessionToken && !options.googleIdToken) {
6977
+ throw new Error("connectCloud needs either a sessionToken or a googleIdToken");
6978
+ }
6979
+ const cloud = new CloudClient({
6980
+ cloudBaseUrl: options.cloudBaseUrl,
6981
+ sessionToken: options.sessionToken,
6982
+ onSession: options.onSession,
6983
+ fetch: options.fetch,
6984
+ timeoutMs: options.timeoutMs
6985
+ });
6986
+ if (!options.sessionToken && options.googleIdToken) {
6987
+ await cloud.signInWithGoogle(options.googleIdToken);
6988
+ }
6989
+ const namespaces = await cloud.getMyNamespaces();
6990
+ const namespace = pickNamespace(namespaces, options.namespaceId);
6991
+ const relays = await cloud.getNamespaceRelays(namespace.namespaceId);
6992
+ const relayInfo = relays.find((r) => r.authorshipReady && r.relayUrl !== null && r.executorAccount !== null) ?? null;
6993
+ if (!relayInfo) {
6994
+ throw new Error(explainNoRelay(namespace, relays));
6995
+ }
6996
+ const relay = new RelayClient({
6997
+ // Checked by `findExecutingRelay`, which is the only thing that returns a
6998
+ // relay — narrowed here rather than re-validated, so the invariant lives in
6999
+ // one place.
7000
+ relayUrl: relayInfo.relayUrl,
7001
+ executorAccount: relayInfo.executorAccount,
7002
+ authorAccount: options.authorAccount,
7003
+ authorProof: options.authorProof,
7004
+ deviceSecret: options.deviceSecret,
7005
+ nonces: options.nonces ?? await defaultNonceSource(options.deviceSecret),
7006
+ ttlSeconds: options.ttlSeconds,
7007
+ // Only the relay is sealed: a sealed fetch is bound to one node and refuses
7008
+ // any other URL, and the cloud calls above carry no intent.
7009
+ fetch: options.seal ? createAttestedSealedFetch({
7010
+ baseUrl: relayInfo.relayUrl,
7011
+ verify: options.seal,
7012
+ fetch: options.fetch
7013
+ }) : options.fetch,
7014
+ timeoutMs: options.timeoutMs
7015
+ });
7016
+ return {
7017
+ cloud,
7018
+ relay,
7019
+ namespaceId: namespace.namespaceId,
7020
+ namespace,
7021
+ relayInfo,
7022
+ execute: (contextId, method, argsJson) => relay.execute(contextId, method, argsJson)
7023
+ };
7024
+ }
7025
+ function pickNamespace(namespaces, requested) {
7026
+ if (requested) {
7027
+ const found = namespaces.find((n) => n.namespaceId === requested);
7028
+ if (!found) {
7029
+ throw new Error(
7030
+ `this account owns no namespace ${requested}; it owns: ${namespaces.map((n) => n.namespaceId).join(", ") || "(none)"}`
7031
+ );
7032
+ }
7033
+ return found;
7034
+ }
7035
+ if (namespaces.length === 0) {
7036
+ throw new Error(
7037
+ "this account owns no namespaces in the cloud: claim one first, then enable HA on it so a relay is assigned"
7038
+ );
7039
+ }
7040
+ if (namespaces.length > 1) {
7041
+ throw new Error(
7042
+ `this account owns ${namespaces.length} namespaces \u2014 pass namespaceId to say which: ${namespaces.map((n) => n.namespaceId).join(", ")}`
7043
+ );
7044
+ }
7045
+ return namespaces[0];
7046
+ }
7047
+ function explainNoRelay(namespace, relays) {
7048
+ if (relays.length === 0) {
7049
+ return namespace.haStatus === "enabled" ? `namespace ${namespace.namespaceId} has HA enabled but no relay has been assigned to it yet \u2014 retry shortly` : `namespace ${namespace.namespaceId} has no relays: enable HA on it in the cloud to have an attested node assigned`;
7050
+ }
7051
+ const waitingOnGrant = relays.filter((r) => !r.authorshipReady);
7052
+ if (waitingOnGrant.length === relays.length) {
7053
+ return `every relay on namespace ${namespace.namespaceId} is waiting for the authorship grant: an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(", ") || "the relay account"} \u2014 with AdminClient.grantAuthorship(namespaceId, account) for these relays, and AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`;
7054
+ }
7055
+ return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none reported both a URL and an executor account yet \u2014 retry shortly`;
7056
+ }
7057
+ async function defaultNonceSource(deviceSecret) {
7058
+ const key = `mero-js:warrant-nonce:${hex(await derivePublicKey(deviceSecret))}`;
7059
+ try {
7060
+ return createLocalStorageNonceSource(key);
7061
+ } catch {
7062
+ return createMemoryNonceSource(1);
7063
+ }
7064
+ }
7065
+
7066
+ // src/sealed/verify.ts
7067
+ var MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
7068
+ function createQuoteVerifier(options) {
7069
+ const allowedMrtd = measurements(options.allowedMrtd, "allowedMrtd");
7070
+ if (allowedMrtd.length === 0) {
7071
+ throw new Error("allowedMrtd is empty, so no quote would ever be accepted");
7072
+ }
7073
+ const allowedRtmr = [
7074
+ measurements(options.allowedRtmr0 ?? [], "allowedRtmr0"),
7075
+ measurements(options.allowedRtmr1 ?? [], "allowedRtmr1"),
7076
+ measurements(options.allowedRtmr2 ?? [], "allowedRtmr2"),
7077
+ measurements(options.allowedRtmr3 ?? [], "allowedRtmr3")
7078
+ ];
7079
+ const allowedStatuses = options.allowedTcbStatuses ?? ["UpToDate"];
7080
+ const now = options.now ?? Date.now;
7081
+ const verifier = async ({
7082
+ quoteB64,
7083
+ nonce: nonce2,
7084
+ reportDataSuffix,
7085
+ collateral
7086
+ }) => {
7087
+ const quote = fromBase64(quoteB64);
7088
+ const against = collateral ?? await options.fetchCollateral?.(quote);
7089
+ if (!against) {
7090
+ throw new Error(
7091
+ "The node sent no collateral for its quote, and no fetchCollateral was given to get it"
7092
+ );
7093
+ }
7094
+ let verified;
7095
+ try {
7096
+ verified = options.dcapVerify(quote, against, Math.floor(now() / 1e3));
7097
+ } catch (error) {
7098
+ throw new Error(`The quote did not verify: ${error?.message ?? String(error)}`);
7099
+ }
7100
+ if (verified.status === "Revoked" || !allowedStatuses.includes(verified.status)) {
7101
+ throw new Error(`The platform's TCB status is ${verified.status}, which is not accepted`);
7102
+ }
7103
+ const td = tdReport(verified.report);
7104
+ if (!allowedMrtd.includes(hex(td.mrTd))) {
7105
+ throw new Error(`MRTD ${hex(td.mrTd)} is not an image this verifier trusts`);
7106
+ }
7107
+ const registers = [td.rtMr0, td.rtMr1, td.rtMr2, td.rtMr3];
7108
+ registers.forEach((register, index) => {
7109
+ const allowed = allowedRtmr[index];
7110
+ if (allowed.length > 0 && !allowed.includes(hex(register))) {
7111
+ throw new Error(`RTMR${index} ${hex(register)} is not one this verifier trusts`);
7112
+ }
7113
+ });
7114
+ if (hex(td.reportData) !== `${nonce2}${reportDataSuffix}`.toLowerCase()) {
7115
+ throw new Error("The quote does not commit to this nonce and binding");
7116
+ }
7117
+ return true;
7118
+ };
7119
+ return Object.assign(verifier, { includeCollateral: true });
7120
+ }
7121
+ function measurements(values, label) {
7122
+ return values.map((value) => {
7123
+ const normalized = value.trim().toLowerCase();
7124
+ if (!MEASUREMENT_HEX.test(normalized)) {
7125
+ throw new Error(`${label} holds ${JSON.stringify(value)}, which is not a 48-byte hex measurement`);
7126
+ }
7127
+ return normalized;
7128
+ });
7129
+ }
7130
+ function tdReport(report) {
7131
+ const data = report.data;
7132
+ if (report.type === "td10" && data) return data;
7133
+ if (report.type === "td15" && data?.base) return data.base;
7134
+ throw new Error(`The quote is a ${report.type} quote, not a TDX one`);
7135
+ }
7136
+ function fromBase64(value) {
7137
+ const binary = atob(value);
7138
+ const bytes = new Uint8Array(binary.length);
7139
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
7140
+ return bytes;
7141
+ }
7142
+
7014
7143
  // src/namespace-op/namespace-op.ts
7015
7144
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
7016
7145
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
@@ -7373,6 +7502,7 @@ export {
7373
7502
  connectCloud,
7374
7503
  createAdminApiClient,
7375
7504
  createAdminApiClientFromHttpClient,
7505
+ createAttestedSealedFetch,
7376
7506
  createAuthApiClient,
7377
7507
  createAuthApiClientFromHttpClient,
7378
7508
  createBrowserAdminApiClient,
@@ -7386,6 +7516,7 @@ export {
7386
7516
  createNodeAuthApiClient,
7387
7517
  createNodeHttpClient,
7388
7518
  createProofSigner,
7519
+ createQuoteVerifier,
7389
7520
  createRetryableMethod,
7390
7521
  createSealedFetch,
7391
7522
  createTimeoutSignal,