@calimero-network/mero-js 22.6.0 → 22.8.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.
Files changed (76) hide show
  1. package/dist/account/account.d.ts +99 -4
  2. package/dist/account/account.js +0 -0
  3. package/dist/account/index.d.ts +6 -2
  4. package/dist/account/index.js +3 -1
  5. package/dist/admin-api/admin-client.d.ts +138 -2
  6. package/dist/admin-api/admin-client.js +237 -1
  7. package/dist/admin-api/admin-types.d.ts +159 -5
  8. package/dist/capabilities.d.ts +42 -0
  9. package/dist/capabilities.js +64 -0
  10. package/dist/cloud/cloud-client.d.ts +38 -8
  11. package/dist/cloud/cloud-client.js +38 -21
  12. package/dist/cloud/connect.d.ts +101 -2
  13. package/dist/cloud/connect.js +84 -8
  14. package/dist/cloud/enrol-redirect.d.ts +195 -0
  15. package/dist/cloud/enrol-redirect.js +236 -0
  16. package/dist/cloud/index.d.ts +6 -2
  17. package/dist/cloud/index.js +7 -1
  18. package/dist/cloud/link-redirect.d.ts +85 -0
  19. package/dist/cloud/link-redirect.js +105 -0
  20. package/dist/crypto/internal.d.ts +15 -0
  21. package/dist/crypto/internal.js +18 -0
  22. package/dist/device-cert/device-cert.d.ts +85 -2
  23. package/dist/device-cert/device-cert.js +100 -9
  24. package/dist/device-cert/index.d.ts +2 -2
  25. package/dist/device-cert/index.js +1 -1
  26. package/dist/events/sse.d.ts +22 -2
  27. package/dist/events/sse.js +20 -11
  28. package/dist/http-client/http-types.d.ts +18 -0
  29. package/dist/http-client/web-client.js +21 -1
  30. package/dist/index.browser.mjs +2 -2
  31. package/dist/index.browser.mjs.map +4 -4
  32. package/dist/index.cjs +1999 -536
  33. package/dist/index.cjs.map +4 -4
  34. package/dist/index.d.ts +11 -6
  35. package/dist/index.js +17 -3
  36. package/dist/index.mjs +1999 -536
  37. package/dist/index.mjs.map +4 -4
  38. package/dist/login/login.d.ts +11 -1
  39. package/dist/login/login.js +5 -4
  40. package/dist/login/session.d.ts +21 -5
  41. package/dist/login/session.js +7 -15
  42. package/dist/namespace-op/namespace-op.d.ts +14 -2
  43. package/dist/namespace-op/namespace-op.js +9 -5
  44. package/dist/relay/index.d.ts +3 -3
  45. package/dist/relay/index.js +1 -1
  46. package/dist/relay/nonce-source.d.ts +135 -0
  47. package/dist/relay/nonce-source.js +187 -19
  48. package/dist/relay/relay-client.d.ts +148 -5
  49. package/dist/relay/relay-client.js +187 -7
  50. package/dist/relay-attestation/attest-node-key.d.ts +66 -0
  51. package/dist/relay-attestation/attest-node-key.js +110 -0
  52. package/dist/relay-attestation/index.d.ts +1 -0
  53. package/dist/relay-attestation/index.js +1 -0
  54. package/dist/request-proof/authorizer.d.ts +48 -0
  55. package/dist/request-proof/authorizer.js +65 -0
  56. package/dist/request-proof/index.d.ts +4 -0
  57. package/dist/request-proof/index.js +2 -0
  58. package/dist/request-proof/request-proof.d.ts +56 -0
  59. package/dist/request-proof/request-proof.js +111 -0
  60. package/dist/rpc/index.d.ts +21 -0
  61. package/dist/rpc/index.js +20 -0
  62. package/dist/transport/index.d.ts +6 -0
  63. package/dist/transport/index.js +3 -0
  64. package/dist/transport/mero-client.d.ts +286 -0
  65. package/dist/transport/mero-client.js +379 -0
  66. package/dist/transport/relay-observer.d.ts +152 -0
  67. package/dist/transport/relay-observer.js +173 -0
  68. package/dist/transport/relay-transport.d.ts +73 -0
  69. package/dist/transport/relay-transport.js +94 -0
  70. package/dist/transport/types.d.ts +100 -0
  71. package/dist/transport/types.js +32 -0
  72. package/dist/warrant/governance-op.d.ts +47 -1
  73. package/dist/warrant/governance-op.js +62 -2
  74. package/dist/warrant/index.d.ts +2 -2
  75. package/dist/warrant/index.js +1 -1
  76. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  AuthApiClient: () => AuthApiClient,
24
24
  AuthRevokedError: () => AuthRevokedError,
25
25
  CAPABILITIES: () => CAPABILITIES,
26
+ CAPABILITY_PRESETS: () => CAPABILITY_PRESETS,
26
27
  CloudClient: () => CloudClient,
27
28
  DEFAULT_LOCAL_NODE_PORTS: () => DEFAULT_LOCAL_NODE_PORTS,
28
29
  DEFAULT_RELEASE_MIRROR: () => DEFAULT_RELEASE_MIRROR,
@@ -32,9 +33,13 @@ __export(index_exports, {
32
33
  IntentRefusedError: () => IntentRefusedError,
33
34
  LocalStorageTokenStore: () => LocalStorageTokenStore,
34
35
  MemoryTokenStore: () => MemoryTokenStore,
36
+ MeroClient: () => MeroClient,
35
37
  MeroJs: () => MeroJs,
36
38
  NODE_RELEASE_SIGNER: () => NODE_RELEASE_SIGNER,
39
+ REQUEST_PROOF_HEADER: () => REQUEST_PROOF_HEADER,
37
40
  RelayClient: () => RelayClient,
41
+ RelayObserver: () => RelayObserver,
42
+ RelayTransport: () => RelayTransport,
38
43
  RpcClient: () => RpcClient,
39
44
  RpcError: () => RpcError,
40
45
  SEALED_CONTENT_TYPE: () => SEALED_CONTENT_TYPE,
@@ -43,19 +48,36 @@ __export(index_exports, {
43
48
  SealedTransportError: () => SealedTransportError,
44
49
  SseClient: () => SseClient,
45
50
  StaleTransportKeyError: () => StaleTransportKeyError,
51
+ WarrantNonceExhaustedError: () => WarrantNonceExhaustedError,
52
+ WarrantNonceRouteUnavailableError: () => WarrantNonceRouteUnavailableError,
46
53
  WebHttpClient: () => WebHttpClient,
47
54
  WsClient: () => WsClient,
48
55
  accountForRoot: () => accountForRoot,
56
+ accountForRootPublicKey: () => accountForRootPublicKey,
49
57
  accountRootFromPhrase: () => accountRootFromPhrase,
50
58
  accountRootFromSecret: () => accountRootFromSecret,
59
+ accountRootSignerFromPhrase: () => accountRootSignerFromPhrase,
51
60
  admitterAddrsFromNetworkStatus: () => admitterAddrsFromNetworkStatus,
61
+ attestKeyBinding: () => attestKeyBinding,
62
+ attestRelayNodeKey: () => attestRelayNodeKey,
63
+ authorNonceLookup: () => authorNonceLookup,
64
+ beginCloudLink: () => beginCloudLink,
65
+ beginDeviceEnrolment: () => beginDeviceEnrolment,
52
66
  buildAuthLoginUrl: () => buildAuthLoginUrl,
67
+ callbackIsForeign: () => callbackIsForeign,
53
68
  callerProof: () => callerProof,
69
+ capMask: () => capMask,
70
+ capNames: () => capNames,
54
71
  classifyError: () => classifyError,
72
+ cloudLinkUrl: () => cloudLinkUrl,
55
73
  cloudNodeReleaseUrl: () => cloudNodeReleaseUrl,
56
74
  combineSignals: () => combineSignals,
57
75
  compareSemver: () => compareSemver,
76
+ completeCloudLink: () => completeCloudLink,
77
+ completeDeviceEnrolment: () => completeDeviceEnrolment,
58
78
  connectCloud: () => connectCloud,
79
+ connectCloudWithAccount: () => connectCloudWithAccount,
80
+ createAccountRootSigner: () => createAccountRootSigner,
59
81
  createAdminApiClient: () => createAdminApiClient,
60
82
  createAdminApiClientFromHttpClient: () => createAdminApiClientFromHttpClient,
61
83
  createAttestedSealedFetch: () => createAttestedSealedFetch,
@@ -67,12 +89,15 @@ __export(index_exports, {
67
89
  createHttpClient: () => createHttpClient,
68
90
  createLocalStorageNonceSource: () => createLocalStorageNonceSource,
69
91
  createMemoryNonceSource: () => createMemoryNonceSource,
92
+ createMeroClient: () => createMeroClient,
70
93
  createMeroJs: () => createMeroJs,
71
94
  createNodeAdminApiClient: () => createNodeAdminApiClient,
72
95
  createNodeAuthApiClient: () => createNodeAuthApiClient,
73
96
  createNodeHttpClient: () => createNodeHttpClient,
97
+ createProofAuthorizer: () => createProofAuthorizer,
74
98
  createProofSigner: () => createProofSigner,
75
99
  createQuoteVerifier: () => createQuoteVerifier,
100
+ createRecoveringNonceSource: () => createRecoveringNonceSource,
76
101
  createRetryableMethod: () => createRetryableMethod,
77
102
  createSealedFetch: () => createSealedFetch,
78
103
  createSignedReleaseSealedFetch: () => createSignedReleaseSealedFetch,
@@ -80,12 +105,16 @@ __export(index_exports, {
80
105
  createTimeoutSignal: () => createTimeoutSignal,
81
106
  createUniversalHttpClient: () => createUniversalHttpClient,
82
107
  creationInitHash: () => creationInitHash,
108
+ defaultAudience: () => defaultAudience,
109
+ defaultCapabilitiesSetOp: () => defaultCapabilitiesSetOp,
83
110
  deviceCertPayload: () => deviceCertPayload,
111
+ deviceEnrolmentUrl: () => deviceEnrolmentUrl,
84
112
  discoverLocalNodes: () => discoverLocalNodes,
85
113
  encodeSignedInvitation: () => encodeSignedInvitation,
86
114
  fetchAttestedTransportKey: () => fetchAttestedTransportKey,
87
115
  fetchNodeRelease: () => fetchNodeRelease,
88
116
  fetchNodeReleaseVersion: () => fetchNodeReleaseVersion,
117
+ foundedNamespaceId: () => foundedNamespaceId,
89
118
  generateAccountRoot: () => generateAccountRoot,
90
119
  generateSessionKey: () => generateSessionKey,
91
120
  governanceOpHash: () => governanceOpHash,
@@ -104,14 +133,22 @@ __export(index_exports, {
104
133
  memberRemovedOp: () => memberRemovedOp,
105
134
  memberRoleSetOp: () => memberRoleSetOp,
106
135
  mintDeviceId: () => mintDeviceId,
136
+ namespaceCreatedOp: () => namespaceCreatedOp,
107
137
  nodeEndpoint: () => nodeEndpoint,
108
138
  nodeReleaseVersionFromImage: () => nodeReleaseVersionFromImage,
109
139
  parseAuthCallback: () => parseAuthCallback,
110
140
  parseCreationWarrant: () => parseCreationWarrant,
141
+ parseDeviceCredential: () => parseDeviceCredential,
111
142
  parseGovernanceWarrant: () => parseGovernanceWarrant,
143
+ parseWarrantNonce: () => parseWarrantNonce,
112
144
  probeNodeHealth: () => probeNodeHealth,
145
+ readCloudLinkCallback: () => readCloudLinkCallback,
146
+ readEnrolmentCallback: () => readEnrolmentCallback,
113
147
  redeemInvitation: () => redeemInvitation,
148
+ reportDataOf: () => reportDataOf,
114
149
  requestBodyHash: () => requestBodyHash,
150
+ resolveRoot: () => resolveRoot,
151
+ resolveRootPair: () => resolveRootPair,
115
152
  routingProofHeaders: () => routingProofHeaders,
116
153
  shouldRetain: () => shouldRetain,
117
154
  signAccountLink: () => signAccountLink,
@@ -122,13 +159,16 @@ __export(index_exports, {
122
159
  signLoginStatement: () => signLoginStatement,
123
160
  signMemberJoinOp: () => signMemberJoinOp,
124
161
  signRequest: () => signRequest,
162
+ signRequestProof: () => signRequestProof,
125
163
  signRoutingChallenge: () => signRoutingChallenge,
126
164
  signWarrant: () => signWarrant,
165
+ signedPath: () => signedPath,
127
166
  signerFromCryptoKey: () => signerFromCryptoKey,
128
167
  signerFromSecret: () => signerFromSecret,
129
168
  transportKeyBinding: () => transportKeyBinding,
130
169
  trustSignedRelease: () => trustSignedRelease,
131
170
  trustedMeasurementsFromReleases: () => trustedMeasurementsFromReleases,
171
+ verifyDeviceCredential: () => verifyDeviceCredential,
132
172
  verifySignedNodeRelease: () => verifySignedNodeRelease,
133
173
  withCap: () => withCap,
134
174
  withRetry: () => withRetry,
@@ -308,8 +348,27 @@ var WebHttpClient = class {
308
348
  } else {
309
349
  headersObj = headers;
310
350
  }
351
+ const method = init?.method || "GET";
352
+ if (this.transport.authorizeRequest) {
353
+ const body = init?.body;
354
+ if (body !== void 0 && typeof body !== "string" && !(body instanceof Uint8Array)) {
355
+ throw new Error(
356
+ "this transport authorizes each request against its body, so the body must be a string or Uint8Array \u2014 a stream cannot be signed, because the signature would have to read bytes the request then cannot send."
357
+ );
358
+ }
359
+ Object.assign(
360
+ headersObj,
361
+ await this.transport.authorizeRequest({
362
+ method,
363
+ path: new URL(url).pathname,
364
+ // Narrowed by the guard above; `BodyInit` is wider than what can be
365
+ // signed.
366
+ body
367
+ })
368
+ );
369
+ }
311
370
  const requestInit = {
312
- method: init?.method || "GET",
371
+ method,
313
372
  headers: headersObj
314
373
  };
315
374
  const isStreamBody = init?.body instanceof ReadableStream || typeof init?.body === "object" && init?.body !== null && "getReader" in init.body && !(init.body instanceof Blob);
@@ -883,6 +942,41 @@ var CAPABILITIES = {
883
942
  */
884
943
  CAN_AUTHOR_ON_BEHALF: 1 << 9
885
944
  };
945
+ var CAPABILITY_PRESETS = {
946
+ /** No capabilities. */
947
+ NONE: 0,
948
+ /** Join Open subgroups only: what a namespace starts with. */
949
+ JOIN_ONLY: CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS,
950
+ /**
951
+ * Create contexts, invite members, join Open subgroups (= 7). A member who
952
+ * can work in the namespace but not reshape it.
953
+ */
954
+ CONTRIBUTOR: CAPABILITIES.CAN_CREATE_CONTEXT | CAPABILITIES.CAN_INVITE_MEMBERS | CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS,
955
+ /**
956
+ * `CONTRIBUTOR` plus create and delete subgroups and manage their visibility
957
+ * (= 231). What mero-chat gives members, so they can start and close their
958
+ * own channels.
959
+ */
960
+ COLLABORATOR: CAPABILITIES.CAN_CREATE_CONTEXT | CAPABILITIES.CAN_INVITE_MEMBERS | CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS | CAPABILITIES.CAN_CREATE_SUBGROUP | CAPABILITIES.CAN_DELETE_SUBGROUP | CAPABILITIES.CAN_MANAGE_VISIBILITY
961
+ };
962
+ function capMask(...caps) {
963
+ let mask = 0;
964
+ for (const cap of caps) {
965
+ if (typeof cap === "number") {
966
+ mask |= cap;
967
+ } else if (Object.prototype.hasOwnProperty.call(CAPABILITIES, cap)) {
968
+ mask |= CAPABILITIES[cap];
969
+ } else {
970
+ throw new Error(`unknown capability: ${String(cap)}`);
971
+ }
972
+ }
973
+ return mask >>> 0;
974
+ }
975
+ function capNames(mask) {
976
+ return Object.keys(CAPABILITIES).filter(
977
+ (name) => hasCap(mask, CAPABILITIES[name])
978
+ );
979
+ }
886
980
  function hasCap(mask, cap) {
887
981
  const capU32 = cap >>> 0;
888
982
  return (mask & capU32) >>> 0 === capU32;
@@ -1034,6 +1128,50 @@ async function redeemInvitation(parsed, redeemer) {
1034
1128
  }
1035
1129
 
1036
1130
  // src/admin-api/admin-client.ts
1131
+ var WarrantNonceRouteUnavailableError = class extends Error {
1132
+ constructor(path, cause) {
1133
+ super(
1134
+ `this node does not serve warrant-nonce discovery (404 on ${path}); it predates the route, so a client must fall back to its own nonce counter`
1135
+ );
1136
+ this.path = path;
1137
+ this.cause = cause;
1138
+ this.name = "WarrantNonceRouteUnavailableError";
1139
+ }
1140
+ };
1141
+ function u64FromBody(body, parsed, field) {
1142
+ const value = parsed[field];
1143
+ if (value === void 0 || value === null) return void 0;
1144
+ const digits = new RegExp(`"${field}"\\s*:\\s*(\\d+)`).exec(body);
1145
+ if (digits) return BigInt(digits[1]);
1146
+ if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value);
1147
+ throw new Error(`warrant-nonce response field \`${field}\` is not a u64: ${JSON.stringify(value)}`);
1148
+ }
1149
+ function parseWarrantNonce(body) {
1150
+ const parsed = JSON.parse(body);
1151
+ const data = parsed?.data;
1152
+ if (!data || typeof data !== "object") {
1153
+ throw new Error(`warrant-nonce response had no \`data\`: ${body.slice(0, 200)}`);
1154
+ }
1155
+ const common = {
1156
+ contextId: String(data.contextId ?? ""),
1157
+ authorDeviceKey: String(data.authorDeviceKey ?? ""),
1158
+ seen: data.seen === true,
1159
+ highWaterNonce: u64FromBody(body, data, "highWaterNonce"),
1160
+ // Not defaulted: a node that stopped sending it is a node whose window this
1161
+ // client would be guessing at, and the window is not a nonce input anyway.
1162
+ windowWidth: u64FromBody(body, data, "windowWidth") ?? 0n
1163
+ };
1164
+ const nextNonce = u64FromBody(body, data, "nextNonce");
1165
+ if (nextNonce === void 0) {
1166
+ return {
1167
+ ...common,
1168
+ kind: "exhausted",
1169
+ seen: true,
1170
+ highWaterNonce: common.highWaterNonce ?? 0xffffffffffffffffn
1171
+ };
1172
+ }
1173
+ return { ...common, kind: "open", nextNonce };
1174
+ }
1037
1175
  function unwrap(response) {
1038
1176
  return response?.data;
1039
1177
  }
@@ -1834,6 +1972,13 @@ var AdminApiClient = class {
1834
1972
  * `namespaceId` names where to publish from, not the extent of the revocation:
1835
1973
  * a device belongs to the account, so every namespace holding a binding for it
1836
1974
  * is withdrawn from and reported back.
1975
+ *
1976
+ * **Revocation is forward-only.** It withdraws the device's authority from
1977
+ * here on and does NOT retract past authorship: ops the device already signed
1978
+ * keep verifying and stay in the DAG, because the certificate that authorised
1979
+ * them was valid when the signature was made. Nothing in this system can
1980
+ * un-author what a device wrote, so a UI built on this call must not offer
1981
+ * that - "this device can no longer act" is the only claim it supports.
1837
1982
  */
1838
1983
  async revokeAccountDevice(namespaceId, request) {
1839
1984
  return unwrap(
@@ -1876,6 +2021,138 @@ var AdminApiClient = class {
1876
2021
  }
1877
2022
  return response;
1878
2023
  }
2024
+ /**
2025
+ * Account -> devices for the members of a group, as its live bindings record
2026
+ * them.
2027
+ *
2028
+ * The join between a group member listing, which names accounts, and a
2029
+ * context's identities, which name bare signing keys - both 64 hex, and
2030
+ * nothing but the source tells them apart. Batch rather than per-account
2031
+ * because the key -> account direction gets asked about arbitrary authors,
2032
+ * which a per-account route would answer in N calls.
2033
+ *
2034
+ * What comes back is **caller-scoped by the node**: an admin of the group sees
2035
+ * every member's devices, a plain member only its own entry. A single-entry
2036
+ * answer is therefore a statement about this caller's role, not about the
2037
+ * group's size.
2038
+ *
2039
+ * Only live bindings appear, so a revoked device is absent here - use
2040
+ * {@link listAccountDevices} for the account's own devices, which keeps a
2041
+ * revoked one with `revoked: true`. Absence here never means the device never
2042
+ * acted: revocation is forward-only and its past ops remain in the DAG.
2043
+ *
2044
+ * Wrapped in `members` on the wire rather than `data`, unwrapped here for the
2045
+ * same reason {@link listAccountDevices} unwraps `devices`.
2046
+ */
2047
+ async listGroupMemberDevices(groupId, options) {
2048
+ const params = new URLSearchParams();
2049
+ if (options?.offset !== void 0) params.set("offset", String(options.offset));
2050
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
2051
+ const query = params.toString();
2052
+ const base = `/admin-api/groups/${encodeURIComponent(groupId)}/member-devices`;
2053
+ const response = await this.httpClient.get(
2054
+ query ? `${base}?${query}` : base
2055
+ );
2056
+ return response.members;
2057
+ }
2058
+ /**
2059
+ * Where `authorDeviceKey` stands in its warrant-nonce sequence in `contextId`.
2060
+ *
2061
+ * # What this is for
2062
+ *
2063
+ * The nonce is the one input to a warrant that is stateful on the *client*.
2064
+ * Everything else an author needs it either holds (its keys, the method, the
2065
+ * args) or can read from the relay ({@link RelayClient.describe} gives the
2066
+ * executor account and the grant). So an author that has lost its counter —
2067
+ * a browser keyholder in partitioned or periodically cleared storage, which
2068
+ * is its normal lifecycle rather than a fault — otherwise has exactly one
2069
+ * strategy: guess upward and burn a refused round trip per wrong guess, with
2070
+ * each refusal indistinguishable at the API from a genuinely spent nonce.
2071
+ *
2072
+ * Read {@link WarrantNonceOpen.nextNonce} and mint there. If you still hold
2073
+ * your own counter, mint at `max(ownNext, nextNonce)`: nonce state folds per
2074
+ * peer, so a node can only be BEHIND a client whose warrants it has not
2075
+ * applied — never ahead of the truth. An author spread across several relays
2076
+ * asks each and takes the highest answer. {@link createRecoveringNonceSource}
2077
+ * does all of that.
2078
+ *
2079
+ * Purely a read. It spends nothing and advances nothing, so it may be called
2080
+ * as often as you like.
2081
+ *
2082
+ * # Forward compatibility — this route is NEW
2083
+ *
2084
+ * It is not on core `master` yet, so a current node answers **404**, which
2085
+ * this method surfaces as {@link WarrantNonceRouteUnavailableError} rather
2086
+ * than as an opaque {@link HTTPError}. That distinction matters because the
2087
+ * handler itself never 404s: a device with no row is a `200` with
2088
+ * `seen: false`, and a malformed id is a `400`. So a 404 here means the node
2089
+ * does not serve the route at all — an older build, or a deployment that has
2090
+ * not re-mounted it — and the answer is to fall back to a local counter, not
2091
+ * to retry.
2092
+ *
2093
+ * # Authentication
2094
+ *
2095
+ * On the PROTECTED admin router, unlike the two delegated-execution routes it
2096
+ * sits beside: it takes another principal's device key as input and reports
2097
+ * that principal's activity. A `--public-intents` deployment does not serve
2098
+ * this anonymously, so an admin credential is required.
2099
+ *
2100
+ * @param authorDeviceKey the device's signing key, base58 (43-44 chars) or
2101
+ * hex (exactly 64). The node accepts either; the two never collide.
2102
+ */
2103
+ async getWarrantNonce(contextId, authorDeviceKey) {
2104
+ const path = `/admin-api/contexts/${encodeURIComponent(contextId)}/warrant-nonce/${encodeURIComponent(authorDeviceKey)}`;
2105
+ let raw;
2106
+ try {
2107
+ raw = await this.httpClient.get(path, { parse: "text" });
2108
+ } catch (error) {
2109
+ if (error instanceof HTTPError && error.status === 404) {
2110
+ throw new WarrantNonceRouteUnavailableError(path, error);
2111
+ }
2112
+ throw error;
2113
+ }
2114
+ return parseWarrantNonce(raw);
2115
+ }
2116
+ /**
2117
+ * The same answer, asked for the way a delegated client has to ask.
2118
+ *
2119
+ * {@link getWarrantNonce} is on the protected admin router, so a keyholder
2120
+ * writing through a relay — the client this whole facility exists for — is
2121
+ * refused it: measured `403` for a session carrying exactly
2122
+ * `context:intent`, `context:query`, `context:subscribe`. That client has no
2123
+ * admin credential and cannot be given one without defeating the point of the
2124
+ * delegated surface.
2125
+ *
2126
+ * What it does have is the credential proving the device is its own, which it
2127
+ * already sends on every write. Presenting it here makes the request
2128
+ * self-scoped: you learn a device's position only by proving, at call time,
2129
+ * that the device is yours. So there is no oracle to protect against, and no
2130
+ * reason for an admin gate.
2131
+ *
2132
+ * The device key is NOT a parameter. It is read from the verified
2133
+ * certificate server-side, which is the only reading that cannot disagree
2134
+ * with the signature — a key supplied alongside would be a second spelling of
2135
+ * the same fact, and two spellings can differ.
2136
+ *
2137
+ * Returns the identical shape {@link getWarrantNonce} returns, so a caller can
2138
+ * swap one for the other without touching what it does with the answer.
2139
+ *
2140
+ * @param authorProof hex-encoded borsh `AccountProof<DeviceCert>` — the same
2141
+ * value passed as `authorProof` to the relay client.
2142
+ */
2143
+ async getWarrantNonceAsAuthor(contextId, authorProof) {
2144
+ const path = `/admin-api/contexts/${encodeURIComponent(contextId)}/warrant-nonce`;
2145
+ let raw;
2146
+ try {
2147
+ raw = await this.httpClient.post(path, { authorProof }, { parse: "text" });
2148
+ } catch (error) {
2149
+ if (error instanceof HTTPError && error.status === 404) {
2150
+ throw new WarrantNonceRouteUnavailableError(path, error);
2151
+ }
2152
+ throw error;
2153
+ }
2154
+ return parseWarrantNonce(raw);
2155
+ }
1879
2156
  /**
1880
2157
  * Member accounts in the group and the devices bound to each. An admin node
1881
2158
  * sees every account; a plain member only its own. `403` if this node is not a
@@ -2386,6 +2663,16 @@ async function importSigningKey(secretHex) {
2386
2663
  );
2387
2664
  }
2388
2665
  }
2666
+ async function verifySignature(publicKey, signature, payload) {
2667
+ const key = await crypto.subtle.importKey(
2668
+ "raw",
2669
+ fromHex(publicKey, "publicKey", 32),
2670
+ { name: "Ed25519" },
2671
+ false,
2672
+ ["verify"]
2673
+ );
2674
+ return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
2675
+ }
2389
2676
  async function derivePublicKey(secretHex) {
2390
2677
  const seed = fromHex(secretHex, "deviceSecret", 32);
2391
2678
  const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
@@ -2509,6 +2796,15 @@ async function jsonRpcCall(httpClient, method, params) {
2509
2796
  }
2510
2797
  var RpcClient = class {
2511
2798
  constructor(opts) {
2799
+ /**
2800
+ * Which write path this is. Present so `RpcClient` satisfies
2801
+ * `ExecuteTransport` as-is — the transport abstraction adopted *this* class's
2802
+ * `execute` shape as the canonical one, so the node side is the interface
2803
+ * rather than an adapter onto it.
2804
+ */
2805
+ this.kind = "node";
2806
+ /** A node serves `/sse` and `/ws` to the credential it issued. */
2807
+ this.canSubscribe = true;
2512
2808
  this.httpClient = opts.httpClient;
2513
2809
  }
2514
2810
  async execute(params) {
@@ -2522,6 +2818,17 @@ var RpcClient = class {
2522
2818
  }
2523
2819
  return result;
2524
2820
  }
2821
+ /**
2822
+ * The same call, in the shape the transport abstraction shares with the
2823
+ * relay.
2824
+ *
2825
+ * `rootHash` is left absent rather than synthesized: JSON-RPC `execute` does
2826
+ * not report the context's scope root, and inventing a value — or a `''` —
2827
+ * would make "the node does not tell us" indistinguishable from an answer.
2828
+ */
2829
+ async executeWithMetadata(params) {
2830
+ return { returns: await this.execute(params), transport: "node" };
2831
+ }
2525
2832
  /**
2526
2833
  * One-tap owner-driven convert: re-signs the caller's identity-gated entries
2527
2834
  * to the current schema. The export converts all of the caller's
@@ -2567,7 +2874,14 @@ var SseClient = class {
2567
2874
  this.closed = false;
2568
2875
  this.listeners = { connect: [], event: [], error: [] };
2569
2876
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
2570
- this.getAuthToken = opts.getAuthToken;
2877
+ if (opts.authorize) {
2878
+ this.authorize = opts.authorize;
2879
+ } else if (opts.getAuthToken) {
2880
+ const getAuthToken = opts.getAuthToken;
2881
+ this.authorize = async () => ({ Authorization: `Bearer ${await getAuthToken()}` });
2882
+ } else {
2883
+ throw new Error("SseClient needs either `getAuthToken` or `authorize`");
2884
+ }
2571
2885
  this.reconnectDelayMs = opts.reconnectDelayMs ?? 3e3;
2572
2886
  this.fetchImpl = (url, init) => opts.fetch ? opts.fetch(url, init) : globalThis.fetch(url, init);
2573
2887
  }
@@ -2634,11 +2948,10 @@ var SseClient = class {
2634
2948
  this.closed = false;
2635
2949
  this.abortController = new AbortController();
2636
2950
  try {
2637
- const token = await this.getAuthToken();
2638
2951
  const url = `${this.baseUrl}/sse`;
2639
2952
  const response = await this.fetchImpl(url, {
2640
2953
  headers: {
2641
- "Authorization": `Bearer ${token}`,
2954
+ ...await this.authorize({ method: "GET", path: new URL(url).pathname }),
2642
2955
  "Accept": "text/event-stream"
2643
2956
  },
2644
2957
  signal: this.abortController.signal
@@ -2791,21 +3104,18 @@ var SseClient = class {
2791
3104
  }
2792
3105
  async sendSubscription(method, ids) {
2793
3106
  try {
2794
- const token = await this.getAuthToken();
2795
3107
  const params = {};
2796
3108
  if (ids.contextIds && ids.contextIds.length > 0) params.contextIds = ids.contextIds;
2797
3109
  if (ids.groupIds && ids.groupIds.length > 0) params.groupIds = ids.groupIds;
2798
- const response = await this.fetchImpl(`${this.baseUrl}/sse/subscription`, {
3110
+ const url = `${this.baseUrl}/sse/subscription`;
3111
+ const body = JSON.stringify({ id: this.sessionId, method, params });
3112
+ const response = await this.fetchImpl(url, {
2799
3113
  method: "POST",
2800
3114
  headers: {
2801
- "Authorization": `Bearer ${token}`,
3115
+ ...await this.authorize({ method: "POST", path: new URL(url).pathname, body }),
2802
3116
  "Content-Type": "application/json"
2803
3117
  },
2804
- body: JSON.stringify({
2805
- id: this.sessionId,
2806
- method,
2807
- params
2808
- })
3118
+ body
2809
3119
  });
2810
3120
  if (!response.ok) {
2811
3121
  const terminal = isTerminalAuthFailure(response);
@@ -3124,11 +3434,123 @@ var EphemeralClient = class {
3124
3434
  }
3125
3435
  };
3126
3436
 
3437
+ // src/signer/signer.ts
3438
+ var PKCS8_ED25519_PREFIX2 = new Uint8Array([
3439
+ 48,
3440
+ 46,
3441
+ 2,
3442
+ 1,
3443
+ 0,
3444
+ 48,
3445
+ 5,
3446
+ 6,
3447
+ 3,
3448
+ 43,
3449
+ 101,
3450
+ 112,
3451
+ 4,
3452
+ 34,
3453
+ 4,
3454
+ 32
3455
+ ]);
3456
+ function unsupported(cause) {
3457
+ const detail = cause instanceof Error ? `: ${cause.message}` : "";
3458
+ return new Error(
3459
+ "this runtime has no WebCrypto Ed25519, so nothing can be signed here. Node 18.4+, Safari 17+, Firefox 129+ and Chrome 137+ have it" + detail
3460
+ );
3461
+ }
3462
+ async function signerFromSecret(secret, label2 = "secret") {
3463
+ const seed = fromHex(secret, label2, 32);
3464
+ const pkcs8 = concat(PKCS8_ED25519_PREFIX2, seed);
3465
+ let privateKey;
3466
+ let publicKey;
3467
+ try {
3468
+ const exportable = await crypto.subtle.importKey(
3469
+ "pkcs8",
3470
+ pkcs8,
3471
+ { name: "Ed25519" },
3472
+ true,
3473
+ ["sign"]
3474
+ );
3475
+ const jwk = await crypto.subtle.exportKey("jwk", exportable);
3476
+ publicKey = hex(
3477
+ Uint8Array.from(
3478
+ atob(jwk.x.replace(/-/g, "+").replace(/_/g, "/")),
3479
+ (c) => c.charCodeAt(0)
3480
+ )
3481
+ );
3482
+ privateKey = await crypto.subtle.importKey(
3483
+ "pkcs8",
3484
+ pkcs8,
3485
+ { name: "Ed25519" },
3486
+ false,
3487
+ ["sign"]
3488
+ );
3489
+ } catch (cause) {
3490
+ throw unsupported(cause);
3491
+ }
3492
+ return fromKeyAndPublic(privateKey, publicKey);
3493
+ }
3494
+ async function signerFromCryptoKey(privateKey, publicKey) {
3495
+ if (privateKey.type !== "private") {
3496
+ throw new Error(
3497
+ `signerFromCryptoKey needs the private half of the pair, got a ${privateKey.type} key`
3498
+ );
3499
+ }
3500
+ if (!privateKey.usages.includes("sign")) {
3501
+ throw new Error(
3502
+ "this CryptoKey cannot sign: generate it with usages ['sign', 'verify']"
3503
+ );
3504
+ }
3505
+ let publicHex;
3506
+ if (typeof publicKey === "string") {
3507
+ fromHex(publicKey, "publicKey", 32);
3508
+ publicHex = publicKey.trim().toLowerCase();
3509
+ } else {
3510
+ try {
3511
+ publicHex = hex(
3512
+ new Uint8Array(await crypto.subtle.exportKey("raw", publicKey))
3513
+ );
3514
+ } catch (cause) {
3515
+ const detail = cause instanceof Error ? `: ${cause.message}` : "";
3516
+ throw new Error(
3517
+ "could not read the public half of this key pair; pass the public key as 64 hex instead" + detail
3518
+ );
3519
+ }
3520
+ }
3521
+ return fromKeyAndPublic(privateKey, publicHex);
3522
+ }
3523
+ function fromKeyAndPublic(privateKey, publicKey) {
3524
+ return {
3525
+ publicKey,
3526
+ async sign(payload) {
3527
+ try {
3528
+ return new Uint8Array(
3529
+ await crypto.subtle.sign({ name: "Ed25519" }, privateKey, payload)
3530
+ );
3531
+ } catch (cause) {
3532
+ throw unsupported(cause);
3533
+ }
3534
+ }
3535
+ };
3536
+ }
3537
+ async function resolveSigner(secret, signer, label2) {
3538
+ if (signer && secret !== void 0) {
3539
+ throw new Error(
3540
+ `pass either ${label2} or signer, not both \u2014 they may name different keys`
3541
+ );
3542
+ }
3543
+ if (signer) return signer;
3544
+ if (secret !== void 0) return signerFromSecret(secret, label2);
3545
+ throw new Error(`${label2} or signer is required`);
3546
+ }
3547
+
3127
3548
  // src/device-cert/device-cert.ts
3128
3549
  var CERT_DOMAIN = new TextEncoder().encode("calimero.device.cert.v1");
3129
3550
  var ACCOUNT_ID_DOMAIN = new TextEncoder().encode("calimero.account.genesis.v1");
3130
3551
  var DEVICE_ID_DOMAIN = new TextEncoder().encode("calimero.device.id.v1");
3131
3552
  var ACCOUNT_GENESIS_VERSION = 2;
3553
+ var ACCOUNT_PROOF_BYTES = 237;
3132
3554
  async function mintDeviceId(account, nonce2) {
3133
3555
  if (nonce2.length !== 16) {
3134
3556
  throw new Error(`nonce must be 16 bytes, got ${nonce2.length}`);
@@ -3161,12 +3583,9 @@ function accountProofBytes(input) {
3161
3583
  );
3162
3584
  }
3163
3585
  async function signDeviceCert(input) {
3164
- const rootPublicKey = await derivePublicKey(input.rootSecret);
3165
- const account = hex(
3166
- await domainHash(ACCOUNT_ID_DOMAIN, [
3167
- concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), rootPublicKey)
3168
- ])
3169
- );
3586
+ const signer = await resolveSigner(input.rootSecret, input.signer, "rootSecret");
3587
+ const rootPublicKey = fromHex(signer.publicKey, "signer.publicKey", 32);
3588
+ const account = await accountForRootPublicKey(signer.publicKey);
3170
3589
  const keyEpoch = 0;
3171
3590
  const payload = await deviceCertPayload({
3172
3591
  account,
@@ -3176,10 +3595,7 @@ async function signDeviceCert(input) {
3176
3595
  keyEpoch,
3177
3596
  deviceEpoch: input.deviceEpoch
3178
3597
  });
3179
- const key = await importSigningKey(input.rootSecret);
3180
- const signature = new Uint8Array(
3181
- await crypto.subtle.sign("Ed25519", key, payload)
3182
- );
3598
+ const signature = await signer.sign(payload);
3183
3599
  const credential = accountProofBytes({
3184
3600
  rootPublicKey,
3185
3601
  account,
@@ -3192,11 +3608,67 @@ async function signDeviceCert(input) {
3192
3608
  });
3193
3609
  return hex(credential);
3194
3610
  }
3611
+ function parseDeviceCredential(credential) {
3612
+ const bytes = fromHex(
3613
+ credential,
3614
+ "credential",
3615
+ // Fixed-width with an empty chain: 1 + 32 + 4 + 32*4 + 4 + 4 + 64.
3616
+ ACCOUNT_PROOF_BYTES
3617
+ );
3618
+ if (bytes[0] !== ACCOUNT_GENESIS_VERSION) {
3619
+ throw new Error(
3620
+ `this credential has account genesis version ${bytes[0]}, not ${ACCOUNT_GENESIS_VERSION} \u2014 it names a different account than its root key derives here, and core would refuse it`
3621
+ );
3622
+ }
3623
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3624
+ const chainLength = view.getUint32(33, true);
3625
+ if (chainLength !== 0) {
3626
+ throw new Error(
3627
+ `this credential carries a ${chainLength}-handoff root chain, which this SDK cannot verify yet \u2014 the signing root is not the genesis root, so checking it means walking the chain`
3628
+ );
3629
+ }
3630
+ const at = (offset, length) => hex(bytes.subarray(offset, offset + length));
3631
+ return {
3632
+ rootPublicKey: at(1, 32),
3633
+ account: at(37, 32),
3634
+ device: at(69, 32),
3635
+ signPublicKey: at(101, 32),
3636
+ kemPublicKey: at(133, 32),
3637
+ keyEpoch: view.getUint32(165, true),
3638
+ deviceEpoch: view.getUint32(169, true),
3639
+ signature: at(173, 64)
3640
+ };
3641
+ }
3642
+ async function verifyDeviceCredential(credential) {
3643
+ const parsed = parseDeviceCredential(credential);
3644
+ const derived = await accountForRootPublicKey(parsed.rootPublicKey);
3645
+ if (derived !== parsed.account) {
3646
+ throw new Error(
3647
+ `this credential names account ${parsed.account}, but its root key derives ${derived} \u2014 the certificate was re-pointed at another account`
3648
+ );
3649
+ }
3650
+ const signed = await verifySignature(
3651
+ parsed.rootPublicKey,
3652
+ fromHex(parsed.signature, "signature", 64),
3653
+ await deviceCertPayload(parsed)
3654
+ );
3655
+ if (!signed) {
3656
+ throw new Error(
3657
+ `the root key of account ${parsed.account} did not sign this certificate`
3658
+ );
3659
+ }
3660
+ return parsed;
3661
+ }
3195
3662
  async function accountForRoot(rootSecret) {
3196
- const rootPublicKey = await derivePublicKey(rootSecret);
3663
+ return accountForRootPublicKey(hex(await derivePublicKey(rootSecret)));
3664
+ }
3665
+ async function accountForRootPublicKey(rootPublicKey) {
3197
3666
  return hex(
3198
3667
  await domainHash(ACCOUNT_ID_DOMAIN, [
3199
- concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), rootPublicKey)
3668
+ concat(
3669
+ new Uint8Array([ACCOUNT_GENESIS_VERSION]),
3670
+ fromHex(rootPublicKey, "rootPublicKey", 32)
3671
+ )
3200
3672
  ])
3201
3673
  );
3202
3674
  }
@@ -5279,27 +5751,50 @@ async function accountRootFromSecret(secret) {
5279
5751
  accountId: await accountForRoot(secret)
5280
5752
  };
5281
5753
  }
5754
+ async function createAccountRootSigner() {
5755
+ const entropy = crypto.getRandomValues(new Uint8Array(ENTROPY_BITS / 8));
5756
+ const phrase = await encodePhrase(entropy);
5757
+ return { ...await rootSignerFromEntropy(entropy), phrase };
5758
+ }
5759
+ async function accountRootSignerFromPhrase(phrase) {
5760
+ return rootSignerFromEntropy(await decodePhrase(normalisePhrase(phrase)));
5761
+ }
5762
+ async function rootSignerFromEntropy(entropy) {
5763
+ const signer = await signerFromSecret(hex(entropy), "rootSecret");
5764
+ entropy.fill(0);
5765
+ return {
5766
+ publicKey: signer.publicKey,
5767
+ accountId: await accountForRootPublicKey(signer.publicKey),
5768
+ signer
5769
+ };
5770
+ }
5771
+ async function resolveRoot(source) {
5772
+ const signer = typeof source === "string" ? await signerFromSecret(source, "rootSecret") : source;
5773
+ return identify(signer);
5774
+ }
5775
+ async function resolveRootPair(rootSecret, signer) {
5776
+ return identify(await resolveSigner(rootSecret, signer, "rootSecret"));
5777
+ }
5778
+ async function identify(signer) {
5779
+ return {
5780
+ signer,
5781
+ publicKey: signer.publicKey,
5782
+ accountId: await accountForRootPublicKey(signer.publicKey)
5783
+ };
5784
+ }
5282
5785
  async function signAccountLink(input) {
5283
5786
  if (!input.nonce) throw new Error("nonce is required to sign an account link");
5284
- const key = await importSigningKey(input.rootSecret);
5285
- const signature = new Uint8Array(
5286
- await crypto.subtle.sign(
5287
- "Ed25519",
5288
- key,
5289
- concat(ACCOUNT_LINK_DOMAIN, new TextEncoder().encode(input.nonce))
5290
- )
5787
+ const signer = await resolveSigner(input.rootSecret, input.signer, "rootSecret");
5788
+ const signature = await signer.sign(
5789
+ concat(ACCOUNT_LINK_DOMAIN, new TextEncoder().encode(input.nonce))
5291
5790
  );
5292
5791
  return base64(signature);
5293
5792
  }
5294
5793
  async function signAccountLogin(input) {
5295
5794
  if (!input.nonce) throw new Error("nonce is required to sign an account login");
5296
- const key = await importSigningKey(input.rootSecret);
5297
- const signature = new Uint8Array(
5298
- await crypto.subtle.sign(
5299
- "Ed25519",
5300
- key,
5301
- concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce))
5302
- )
5795
+ const signer = await resolveSigner(input.rootSecret, input.signer, "rootSecret");
5796
+ const signature = await signer.sign(
5797
+ concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce))
5303
5798
  );
5304
5799
  return base64(signature);
5305
5800
  }
@@ -5393,7 +5888,7 @@ function base642(bytes) {
5393
5888
  }
5394
5889
 
5395
5890
  // src/cloud/cloud-client.ts
5396
- var DEFAULT_CLOUD_BASE_URL = "https://cloud.calimero.network";
5891
+ var DEFAULT_CLOUD_BASE_URL = "https://manager.cloud.calimero.network";
5397
5892
  function canExecuteOf(row, legacy) {
5398
5893
  return typeof row.can_execute === "boolean" ? row.can_execute : legacy;
5399
5894
  }
@@ -5457,8 +5952,10 @@ var CloudClient = class {
5457
5952
  * account behind those reads was claimed by its owner. The claim is recorded
5458
5953
  * even when the session is refused.
5459
5954
  *
5460
- * @param rootSecret the account root's signing secret, 64 hex. It never
5461
- * leaves this process — only a signature over the cloud's nonce travels.
5955
+ * @param root the account root: its 64-hex secret, or a `Signer` over a root
5956
+ * this process may use and cannot read — the form a phrase-held root in a
5957
+ * browser takes. Either way nothing but a signature over the cloud's nonce
5958
+ * travels.
5462
5959
  *
5463
5960
  * A `403` means either the signature did not verify, or the account is not
5464
5961
  * linked to a cloud login: ownership alone says who you are, and the link
@@ -5466,13 +5963,13 @@ var CloudClient = class {
5466
5963
  * on the proof alone would authenticate perfectly and authorize nothing. The
5467
5964
  * ownership claim is recorded either way, so linking later needs no re-proof.
5468
5965
  */
5469
- async signInWithAccount(rootSecret) {
5470
- const root = await accountRootFromSecret(rootSecret);
5966
+ async signInWithAccount(root) {
5967
+ const { signer, publicKey } = await resolveRoot(root);
5471
5968
  const { nonce: nonce2 } = await this.getAccountLoginChallenge();
5472
5969
  return this.submitAccountLogin({
5473
- rootPublicKey: root.publicKey,
5970
+ rootPublicKey: publicKey,
5474
5971
  nonce: nonce2,
5475
- signature: await signAccountLogin({ rootSecret, nonce: nonce2 })
5972
+ signature: await signAccountLogin({ signer, nonce: nonce2 })
5476
5973
  });
5477
5974
  }
5478
5975
  /**
@@ -5569,6 +6066,7 @@ var CloudClient = class {
5569
6066
  peerId: String(row.peer_id ?? ""),
5570
6067
  relayUrl,
5571
6068
  executorAccount,
6069
+ nodeKey: row.node_key ?? row.node_public_key ?? null,
5572
6070
  status: String(row.status ?? ""),
5573
6071
  authorshipReady,
5574
6072
  teeRole: teeRoleOf(row),
@@ -5911,22 +6409,23 @@ var CloudClient = class {
5911
6409
  * an intent; the cloud needs it to know whose plan a hosted node is spending,
5912
6410
  * and to sign that account back in later.
5913
6411
  *
5914
- * @param rootSecret the account root's signing secret, 64 hex — from
5915
- * {@link generateAccountRoot}, from a restored recovery phrase, or from a
5916
- * desktop app or hardware key that already holds one.
6412
+ * @param root the account root: its 64-hex secret — from
6413
+ * {@link generateAccountRoot}, a restored recovery phrase, or a desktop app
6414
+ * or hardware key that already holds one — or a `Signer` over a root this
6415
+ * process cannot read, which is what a phrase-held browser root is.
5917
6416
  *
5918
6417
  * Idempotent for the same login. An account already linked to a *different*
5919
6418
  * cloud login is a `409`; a replayed or expired challenge is a `403`; and
5920
6419
  * exceeding the plan's account limit is a `402`.
5921
6420
  */
5922
- async linkAccount(rootSecret) {
5923
- const root = await accountRootFromSecret(rootSecret);
6421
+ async linkAccount(root) {
6422
+ const { signer, publicKey, accountId } = await resolveRoot(root);
5924
6423
  const { nonce: nonce2 } = await this.getAccountLinkChallenge();
5925
6424
  return this.submitAccountLink({
5926
- accountId: root.accountId,
5927
- rootPublicKey: root.publicKey,
6425
+ accountId,
6426
+ rootPublicKey: publicKey,
5928
6427
  nonce: nonce2,
5929
- signature: await signAccountLink({ rootSecret, nonce: nonce2 })
6428
+ signature: await signAccountLink({ signer, nonce: nonce2 })
5930
6429
  });
5931
6430
  }
5932
6431
  /**
@@ -6046,16 +6545,19 @@ var CloudClient = class {
6046
6545
  * login; `402` that the plan is full.
6047
6546
  */
6048
6547
  async linkAccountWithGrant(options) {
6049
- const root = await accountRootFromSecret(options.rootSecret);
6548
+ const { signer, publicKey, accountId } = await resolveRootPair(
6549
+ options.rootSecret,
6550
+ options.signer
6551
+ );
6050
6552
  const body = await this.request(
6051
6553
  "POST",
6052
6554
  "/api/cloud/accounts/link",
6053
6555
  {
6054
6556
  body: {
6055
6557
  grant: options.grant,
6056
- root_public_key: root.publicKey,
6558
+ root_public_key: publicKey,
6057
6559
  signature: await signAccountLink({
6058
- rootSecret: options.rootSecret,
6560
+ signer,
6059
6561
  nonce: options.grant
6060
6562
  })
6061
6563
  },
@@ -6063,7 +6565,7 @@ var CloudClient = class {
6063
6565
  }
6064
6566
  );
6065
6567
  return {
6066
- accountId: String(body.account_id ?? root.accountId),
6568
+ accountId: String(body.account_id ?? accountId),
6067
6569
  linkedAt: body.linked_at ?? null,
6068
6570
  alreadyLinked: body.already_linked === true
6069
6571
  };
@@ -6587,117 +7089,6 @@ var LocalStorageTokenStore = class {
6587
7089
  }
6588
7090
  };
6589
7091
 
6590
- // src/signer/signer.ts
6591
- var PKCS8_ED25519_PREFIX2 = new Uint8Array([
6592
- 48,
6593
- 46,
6594
- 2,
6595
- 1,
6596
- 0,
6597
- 48,
6598
- 5,
6599
- 6,
6600
- 3,
6601
- 43,
6602
- 101,
6603
- 112,
6604
- 4,
6605
- 34,
6606
- 4,
6607
- 32
6608
- ]);
6609
- function unsupported(cause) {
6610
- const detail = cause instanceof Error ? `: ${cause.message}` : "";
6611
- return new Error(
6612
- "this runtime has no WebCrypto Ed25519, so nothing can be signed here. Node 18.4+, Safari 17+, Firefox 129+ and Chrome 137+ have it" + detail
6613
- );
6614
- }
6615
- async function signerFromSecret(secret, label2 = "secret") {
6616
- const seed = fromHex(secret, label2, 32);
6617
- const pkcs8 = concat(PKCS8_ED25519_PREFIX2, seed);
6618
- let privateKey;
6619
- let publicKey;
6620
- try {
6621
- const exportable = await crypto.subtle.importKey(
6622
- "pkcs8",
6623
- pkcs8,
6624
- { name: "Ed25519" },
6625
- true,
6626
- ["sign"]
6627
- );
6628
- const jwk = await crypto.subtle.exportKey("jwk", exportable);
6629
- publicKey = hex(
6630
- Uint8Array.from(
6631
- atob(jwk.x.replace(/-/g, "+").replace(/_/g, "/")),
6632
- (c) => c.charCodeAt(0)
6633
- )
6634
- );
6635
- privateKey = await crypto.subtle.importKey(
6636
- "pkcs8",
6637
- pkcs8,
6638
- { name: "Ed25519" },
6639
- false,
6640
- ["sign"]
6641
- );
6642
- } catch (cause) {
6643
- throw unsupported(cause);
6644
- }
6645
- return fromKeyAndPublic(privateKey, publicKey);
6646
- }
6647
- async function signerFromCryptoKey(privateKey, publicKey) {
6648
- if (privateKey.type !== "private") {
6649
- throw new Error(
6650
- `signerFromCryptoKey needs the private half of the pair, got a ${privateKey.type} key`
6651
- );
6652
- }
6653
- if (!privateKey.usages.includes("sign")) {
6654
- throw new Error(
6655
- "this CryptoKey cannot sign: generate it with usages ['sign', 'verify']"
6656
- );
6657
- }
6658
- let publicHex;
6659
- if (typeof publicKey === "string") {
6660
- fromHex(publicKey, "publicKey", 32);
6661
- publicHex = publicKey.trim().toLowerCase();
6662
- } else {
6663
- try {
6664
- publicHex = hex(
6665
- new Uint8Array(await crypto.subtle.exportKey("raw", publicKey))
6666
- );
6667
- } catch (cause) {
6668
- const detail = cause instanceof Error ? `: ${cause.message}` : "";
6669
- throw new Error(
6670
- "could not read the public half of this key pair; pass the public key as 64 hex instead" + detail
6671
- );
6672
- }
6673
- }
6674
- return fromKeyAndPublic(privateKey, publicHex);
6675
- }
6676
- function fromKeyAndPublic(privateKey, publicKey) {
6677
- return {
6678
- publicKey,
6679
- async sign(payload) {
6680
- try {
6681
- return new Uint8Array(
6682
- await crypto.subtle.sign({ name: "Ed25519" }, privateKey, payload)
6683
- );
6684
- } catch (cause) {
6685
- throw unsupported(cause);
6686
- }
6687
- }
6688
- };
6689
- }
6690
- async function resolveSigner(secret, signer, label2) {
6691
- if (signer && secret !== void 0) {
6692
- throw new Error(
6693
- `pass either ${label2} or signer, not both \u2014 they may name different keys`
6694
- );
6695
- }
6696
- if (signer) return signer;
6697
- if (secret !== void 0) return signerFromSecret(secret, label2);
6698
- throw new Error(`${label2} or signer is required`);
6699
- }
6700
-
6701
7092
  // src/warrant/warrant.ts
6702
7093
  var SIGN_DOMAIN2 = new TextEncoder().encode("calimero.warrant.v2");
6703
7094
  var INTENT_DOMAIN = new TextEncoder().encode("calimero.warrant.intent.v1");
@@ -7059,6 +7450,138 @@ function kindByte(kind) {
7059
7450
  return byte;
7060
7451
  }
7061
7452
 
7453
+ // src/warrant/governance-op.ts
7454
+ var ROLE = {
7455
+ Admin: 0,
7456
+ Member: 1,
7457
+ ReadOnly: 2
7458
+ };
7459
+ var GROUP_OP = {
7460
+ MemberAdded: 1,
7461
+ MemberRemoved: 2,
7462
+ MemberLeft: 3,
7463
+ MemberRoleSet: 4,
7464
+ DefaultCapabilitiesSet: 6
7465
+ };
7466
+ var ROOT_OP = {
7467
+ GroupCreated: 0,
7468
+ GroupReparented: 1,
7469
+ GroupDeleted: 2,
7470
+ NamespaceCreatedV2: 9
7471
+ };
7472
+ var NAMESPACE_ID_DOMAIN = new TextEncoder().encode("calimero.namespace.id.v1");
7473
+ var CLEARED_HASH = new Uint8Array(32);
7474
+ var EMPTY_VEC = u32le(0);
7475
+ function role(value) {
7476
+ const byte = ROLE[value];
7477
+ if (byte === void 0) {
7478
+ throw new Error(`role must be Admin, Member or ReadOnly, got ${String(value)}`);
7479
+ }
7480
+ return new Uint8Array([byte]);
7481
+ }
7482
+ function tag(byte) {
7483
+ return new Uint8Array([byte]);
7484
+ }
7485
+ function memberAddedOp(member, memberRole) {
7486
+ return {
7487
+ kind: "group",
7488
+ bytes: concat(tag(GROUP_OP.MemberAdded), fromHex(member, "member", 32), role(memberRole))
7489
+ };
7490
+ }
7491
+ function memberRemovedOp(member) {
7492
+ return {
7493
+ kind: "group",
7494
+ bytes: concat(
7495
+ tag(GROUP_OP.MemberRemoved),
7496
+ fromHex(member, "member", 32),
7497
+ CLEARED_HASH,
7498
+ EMPTY_VEC
7499
+ )
7500
+ };
7501
+ }
7502
+ function memberLeftOp(member) {
7503
+ return {
7504
+ kind: "group",
7505
+ bytes: concat(
7506
+ tag(GROUP_OP.MemberLeft),
7507
+ fromHex(member, "member", 32),
7508
+ CLEARED_HASH,
7509
+ EMPTY_VEC
7510
+ )
7511
+ };
7512
+ }
7513
+ function memberRoleSetOp(member, memberRole) {
7514
+ return {
7515
+ kind: "group",
7516
+ bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, "member", 32), role(memberRole))
7517
+ };
7518
+ }
7519
+ function defaultCapabilitiesSetOp(capabilities) {
7520
+ if (!Number.isInteger(capabilities) || capabilities < 0 || capabilities > 4294967295) {
7521
+ throw new Error(`capabilities must be a u32 bit mask, got ${String(capabilities)}`);
7522
+ }
7523
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
7524
+ throw new Error(
7525
+ "capabilities may not include CAN_AUTHOR_ON_BEHALF (512): a relay never carries a change to it"
7526
+ );
7527
+ }
7528
+ return {
7529
+ kind: "group",
7530
+ bytes: concat(tag(GROUP_OP.DefaultCapabilitiesSet), u32le(capabilities))
7531
+ };
7532
+ }
7533
+ function groupCreatedOp(input) {
7534
+ return {
7535
+ kind: "root",
7536
+ bytes: concat(
7537
+ tag(ROOT_OP.GroupCreated),
7538
+ fromHex(input.groupId, "groupId", 32),
7539
+ fromHex(input.parentId, "parentId", 32),
7540
+ tag(input.restricted ? 1 : 0),
7541
+ fromHex(input.admin, "admin", 32)
7542
+ )
7543
+ };
7544
+ }
7545
+ function groupReparentedOp(childGroupId, newParentId) {
7546
+ return {
7547
+ kind: "root",
7548
+ bytes: concat(
7549
+ tag(ROOT_OP.GroupReparented),
7550
+ fromHex(childGroupId, "childGroupId", 32),
7551
+ fromHex(newParentId, "newParentId", 32)
7552
+ )
7553
+ };
7554
+ }
7555
+ function groupDeletedOp(rootGroupId) {
7556
+ return {
7557
+ kind: "root",
7558
+ bytes: concat(tag(ROOT_OP.GroupDeleted), fromHex(rootGroupId, "rootGroupId", 32), EMPTY_VEC, EMPTY_VEC)
7559
+ };
7560
+ }
7561
+ async function foundedNamespaceId(founder, salt) {
7562
+ return hex(
7563
+ await domainHash(NAMESPACE_ID_DOMAIN, [
7564
+ fromHex(founder, "founder", 32),
7565
+ fromHex(salt, "salt", 32)
7566
+ ])
7567
+ );
7568
+ }
7569
+ function namespaceCreatedOp(input) {
7570
+ const credential = fromHexUnsized(input.credential, "credential");
7571
+ if (credential.length === 0) {
7572
+ throw new Error("credential must be the founder's AccountProof<DeviceCert>, got nothing");
7573
+ }
7574
+ return {
7575
+ kind: "root",
7576
+ bytes: concat(
7577
+ tag(ROOT_OP.NamespaceCreatedV2),
7578
+ fromHex(input.founder, "founder", 32),
7579
+ credential,
7580
+ fromHex(input.salt, "salt", 32)
7581
+ )
7582
+ };
7583
+ }
7584
+
7062
7585
  // src/relay/relay-client.ts
7063
7586
  var DEFAULT_TTL_SECONDS = 300;
7064
7587
  var IntentRefusedError = class extends Error {
@@ -7070,6 +7593,16 @@ var IntentRefusedError = class extends Error {
7070
7593
  this.name = "IntentRefusedError";
7071
7594
  }
7072
7595
  };
7596
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7597
+ function retryAfterMs(headers) {
7598
+ const raw = headers?.get?.("Retry-After");
7599
+ if (!raw) return null;
7600
+ const seconds = Number.parseInt(raw, 10);
7601
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
7602
+ const at = Date.parse(raw);
7603
+ if (Number.isFinite(at)) return Math.max(0, at - Date.now());
7604
+ return null;
7605
+ }
7073
7606
  var RelayClient = class {
7074
7607
  constructor(config) {
7075
7608
  this.config = config;
@@ -7078,11 +7611,40 @@ var RelayClient = class {
7078
7611
  const injected = config.fetch;
7079
7612
  this.fetchImpl = injected ? (input, init) => injected(input, init) : (input, init) => globalThis.fetch(input, init);
7080
7613
  this.timeoutMs = config.timeoutMs ?? 1e4;
7614
+ this.rateLimitRetries = config.rateLimitRetries ?? 3;
7615
+ }
7616
+ /**
7617
+ * The relay's origin, trailing slash stripped.
7618
+ *
7619
+ * Exposed because a relay **is a node**: the same origin that answers the
7620
+ * intents routes also answers `/auth/challenge`, `/auth/token`, `/sse` and
7621
+ * `/ws`. Anything building an observing session over this relay needs the
7622
+ * URL, and asking the caller to pass it a second time is how the two drift.
7623
+ */
7624
+ get relayUrl() {
7625
+ return this.baseUrl;
7626
+ }
7627
+ /** The author's account, hex — whose consent every warrant here carries. */
7628
+ get authorAccount() {
7629
+ return this.config.authorAccount;
7630
+ }
7631
+ /**
7632
+ * The author's `AccountProof<DeviceCert>`, hex.
7633
+ *
7634
+ * The same credential `login()` wants, which is why it is readable: a caller
7635
+ * that already handed it to this client should not have to hand an identical
7636
+ * copy to the login as well, where the two could disagree.
7637
+ */
7638
+ get authorProof() {
7639
+ return this.config.authorProof;
7081
7640
  }
7082
7641
  /**
7083
- * The author device's signer, built from `deviceSecret` when that is what was
7084
- * configured. A {@link Signer} and never the secret, so no caller of a
7085
- * `RelayClient` gains the ability to read key material.
7642
+ * The author device's signer.
7643
+ *
7644
+ * A {@link Signer} and never the secret: this returns something that can
7645
+ * *sign*, so no caller of a `RelayClient` gains the ability to read a key
7646
+ * that was passed in as `deviceSecret`. That asymmetry is the point — the
7647
+ * login needs a signature, not key material.
7086
7648
  */
7087
7649
  async authorSigner() {
7088
7650
  return resolveSigner(this.config.deviceSecret, this.config.signer, "deviceSecret");
@@ -7259,12 +7821,93 @@ var RelayClient = class {
7259
7821
  notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
7260
7822
  signer
7261
7823
  });
7262
- const body = await this.json(
7263
- "POST",
7264
- `/admin-api/groups/${encodeURIComponent(input.groupId)}/governance-intents`,
7265
- { warrant, authorProof: this.config.authorProof, op: hex(input.op.bytes) }
7266
- );
7267
- return { groupId: body.data.groupId };
7824
+ const data = await this.postGovernance(input.groupId, input.op, warrant);
7825
+ return { groupId: data.groupId };
7826
+ }
7827
+ /**
7828
+ * Found a namespace through the relay, with the author as its founder, owner
7829
+ * and admin. For an account with no node, this is how it gets a namespace at
7830
+ * all.
7831
+ *
7832
+ * The author signs the exact genesis (`NamespaceCreatedV2`, carrying the
7833
+ * author's own credential and the salt) under a root-plane governance
7834
+ * warrant scoped to the new namespace's id, which is derived from the
7835
+ * author's account and the salt, so the relay can neither choose another id
7836
+ * nor found it for anyone else. The relay takes an identity in the new
7837
+ * namespace, mints its key, publishes the genesis, and is seated as the
7838
+ * founding relay (a `Member` holding `CAN_AUTHOR_ON_BEHALF`), so later
7839
+ * `govern`, `createContext` and `execute` calls in the namespace work
7840
+ * through it straight away. A TEE relay then admits itself as the
7841
+ * namespace's first TEE; `teeEnabled` reports whether it did.
7842
+ *
7843
+ * The executor cannot be learned from the relay first: the namespace does not
7844
+ * exist, so `describeGovernance` has nothing to answer about. It comes from
7845
+ * `input.executorAccount`, the configured one, or one an earlier call learned
7846
+ * (`describe`, `createContext`, `govern`), and without any of them this
7847
+ * throws before taking a nonce.
7848
+ * The nonce is spent in the new namespace's ledger.
7849
+ *
7850
+ * With `defaultCapabilities`, the founder then sets the namespace's default
7851
+ * capability mask through the same relay (a second warrant, a second nonce)
7852
+ * and `defaultCapabilitiesSet` reports whether it landed. A failure there
7853
+ * does not throw, since the namespace is already founded.
7854
+ *
7855
+ * Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
7856
+ * that names an existing namespace is a `409`, an `HTTPError`.
7857
+ */
7858
+ async foundNamespace(input = {}) {
7859
+ const executor = input.executorAccount ? this.checkedExecutor(input.executorAccount) : this.executorAccount;
7860
+ if (!executor) {
7861
+ throw new Error(
7862
+ "foundNamespace needs the relay's account: pass executorAccount or configure it (checked before signing, no nonce was spent)"
7863
+ );
7864
+ }
7865
+ const salt = input.salt ? hex(fromHex(input.salt, "salt", 32)) : hex(crypto.getRandomValues(new Uint8Array(32)));
7866
+ const namespaceId = await foundedNamespaceId(this.config.authorAccount, salt);
7867
+ const op = namespaceCreatedOp({
7868
+ founder: this.config.authorAccount,
7869
+ credential: this.config.authorProof,
7870
+ salt
7871
+ });
7872
+ const capabilitiesOp = input.defaultCapabilities === void 0 ? void 0 : defaultCapabilitiesSetOp(input.defaultCapabilities);
7873
+ const signer = await this.authorSigner();
7874
+ const nonce2 = await this.config.nonces.next();
7875
+ const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
7876
+ const warrant = await signGovernanceWarrant({
7877
+ scope: namespaceId,
7878
+ op,
7879
+ authorAccount: this.config.authorAccount,
7880
+ executor,
7881
+ nonce: nonce2,
7882
+ notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
7883
+ signer
7884
+ });
7885
+ const data = await this.postGovernance(namespaceId, op, warrant);
7886
+ const founded = {
7887
+ namespaceId: data.groupId,
7888
+ salt,
7889
+ teeEnabled: data.teeEnabled ?? false,
7890
+ ...data.teeError ? { teeError: data.teeError } : {}
7891
+ };
7892
+ if (!capabilitiesOp) return founded;
7893
+ try {
7894
+ await this.govern({ groupId: founded.namespaceId, op: capabilitiesOp });
7895
+ return { ...founded, defaultCapabilitiesSet: true };
7896
+ } catch (err) {
7897
+ return {
7898
+ ...founded,
7899
+ defaultCapabilitiesSet: false,
7900
+ defaultCapabilitiesError: err instanceof Error ? err.message : String(err)
7901
+ };
7902
+ }
7903
+ }
7904
+ async postGovernance(groupId, op, warrant) {
7905
+ const body = await this.json("POST", `/admin-api/groups/${encodeURIComponent(groupId)}/governance-intents`, {
7906
+ warrant,
7907
+ authorProof: this.config.authorProof,
7908
+ op: hex(op.bytes)
7909
+ });
7910
+ return body.data;
7268
7911
  }
7269
7912
  /**
7270
7913
  * The executor a warrant must name: the one the relay answered with, which a
@@ -7280,10 +7923,40 @@ var RelayClient = class {
7280
7923
  this.executorAccount = answered;
7281
7924
  return answered;
7282
7925
  }
7926
+ /**
7927
+ * One request, waiting out a rate limit rather than failing the caller.
7928
+ *
7929
+ * The relay's ingress limits the delegated-execution routes to 20 requests a
7930
+ * second per source IP, burst 40 — tighter than everything else it serves,
7931
+ * because those two routes are the ones it answers to anonymous callers.
7932
+ *
7933
+ * Retrying a 429 is safe here in a way retrying most refusals is not: the
7934
+ * limiter sits *first* in the middleware chain, so a throttled request never
7935
+ * reached the node and its warrant nonce was never spent. The same bytes can
7936
+ * go again. Contrast the spent-nonce 403 below, which needs a fresh warrant.
7937
+ *
7938
+ * `Retry-After` is honoured when the ingress sends one; otherwise the wait
7939
+ * doubles from a little over the limiter's own window, which is the shortest
7940
+ * delay that can actually clear.
7941
+ */
7283
7942
  async json(method, path, body) {
7284
- const headers = { Accept: "application/json" };
7285
- if (body !== void 0) headers["Content-Type"] = "application/json";
7286
- const controller = new AbortController();
7943
+ let wait = 250;
7944
+ for (let attempt = 0; ; attempt += 1) {
7945
+ try {
7946
+ return await this.jsonOnce(method, path, body);
7947
+ } catch (err) {
7948
+ const limited = err instanceof HTTPError && err.status === 429;
7949
+ if (!limited || attempt >= this.rateLimitRetries) throw err;
7950
+ const after = retryAfterMs(err.headers);
7951
+ await sleep(after ?? wait);
7952
+ wait *= 2;
7953
+ }
7954
+ }
7955
+ }
7956
+ async jsonOnce(method, path, body) {
7957
+ const headers = { Accept: "application/json" };
7958
+ if (body !== void 0) headers["Content-Type"] = "application/json";
7959
+ const controller = new AbortController();
7287
7960
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
7288
7961
  let response;
7289
7962
  try {
@@ -7318,19 +7991,681 @@ var RelayClient = class {
7318
7991
  text
7319
7992
  );
7320
7993
  }
7321
- return await response.json();
7994
+ return await response.json();
7995
+ }
7996
+ };
7997
+ function extractReason(bodyText) {
7998
+ if (!bodyText) return "no reason given";
7999
+ try {
8000
+ const parsed = JSON.parse(bodyText);
8001
+ for (const candidate of [parsed.error, parsed.message]) {
8002
+ if (typeof candidate === "string" && candidate) return candidate;
8003
+ }
8004
+ } catch {
8005
+ }
8006
+ return bodyText;
8007
+ }
8008
+
8009
+ // src/transport/relay-transport.ts
8010
+ var RelayTransport = class {
8011
+ constructor(relay, observer = null) {
8012
+ this.relay = relay;
8013
+ this.observer = observer;
8014
+ this.kind = "relay";
8015
+ }
8016
+ /**
8017
+ * Whether this relay can be observed — a fact, not a constant.
8018
+ *
8019
+ * A relay IS a node: it serves `/auth/challenge`, `/auth/token`, `/sse` and
8020
+ * `/ws`, and the `account_proof` provider grants `context:subscribe` by
8021
+ * default. What decides the answer is therefore not the transport but whether
8022
+ * a caller supplied the node's device signing key, which cannot be inferred
8023
+ * from the node or from its `peerId`. See `./relay-observer.ts`.
8024
+ */
8025
+ get canSubscribe() {
8026
+ return this.observer !== null;
8027
+ }
8028
+ /** The event session, or `null` when no node key was supplied. */
8029
+ get events() {
8030
+ return this.observer;
8031
+ }
8032
+ /** The underlying client, for `describe()` and anything else relay-specific. */
8033
+ get client() {
8034
+ return this.relay;
8035
+ }
8036
+ async execute(params) {
8037
+ const { returns } = await this.executeWithMetadata(params);
8038
+ return returns;
8039
+ }
8040
+ async executeWithMetadata(params) {
8041
+ const result = await this.relay.execute(
8042
+ params.contextId,
8043
+ params.method,
8044
+ // The node defaults a missing `argsJson` to `{}`; matching that here keeps
8045
+ // a zero-argument call identical on both transports rather than sending
8046
+ // `undefined` and letting the relay's JSON body decide.
8047
+ params.argsJson ?? {}
8048
+ );
8049
+ return {
8050
+ // Verbatim. `RelayClient` has already normalized a missing `returns` to
8051
+ // `null`, which is what a node reports for a null output — see the
8052
+ // module note on why this is not rewritten to `undefined`.
8053
+ returns: result.returns,
8054
+ transport: "relay",
8055
+ rootHash: result.rootHash
8056
+ };
8057
+ }
8058
+ /**
8059
+ * Both of these are plain context methods, so they work through a relay
8060
+ * exactly as they do on a node — the warrant names `migrate_my_entries` and
8061
+ * the delta is attributed to the author, which is precisely the identity the
8062
+ * convert is scoped to. They are re-declared rather than inherited because
8063
+ * `ExecuteTransport` is an interface, not a base class.
8064
+ */
8065
+ async migrateMyEntries(contextId) {
8066
+ return this.execute({
8067
+ contextId,
8068
+ method: "migrate_my_entries"
8069
+ });
8070
+ }
8071
+ /** Read-only count of the caller's entries still below the target schema. */
8072
+ async countMyPending(contextId) {
8073
+ return this.execute({ contextId, method: "count_my_pending" });
8074
+ }
8075
+ };
8076
+
8077
+ // src/login/login.ts
8078
+ var SIGN_DOMAIN5 = new TextEncoder().encode("calimero.auth.login.v1");
8079
+ var AUDIENCE_TAG = {
8080
+ webOrigin: 0,
8081
+ codeSigningId: 1,
8082
+ cli: 2
8083
+ };
8084
+ function audienceBody(audience) {
8085
+ const encoder = new TextEncoder();
8086
+ switch (audience.kind) {
8087
+ case "webOrigin":
8088
+ return encoder.encode(audience.origin);
8089
+ case "codeSigningId":
8090
+ return encoder.encode(audience.id);
8091
+ case "cli":
8092
+ return new Uint8Array(0);
8093
+ }
8094
+ }
8095
+ function audienceSigningBytes(audience) {
8096
+ return concat(
8097
+ Uint8Array.from([AUDIENCE_TAG[audience.kind]]),
8098
+ audienceBody(audience)
8099
+ );
8100
+ }
8101
+ function audienceWire(audience) {
8102
+ const tag2 = Uint8Array.from([AUDIENCE_TAG[audience.kind]]);
8103
+ if (audience.kind === "cli") {
8104
+ return tag2;
8105
+ }
8106
+ const body = audienceBody(audience);
8107
+ return concat(tag2, u32le(body.length), body);
8108
+ }
8109
+ async function signLoginStatement(input) {
8110
+ const node = fromHex(input.node, "node", 32);
8111
+ const challenge = fromHex(input.challenge, "challenge", 32);
8112
+ const sessionKey = fromHex(input.sessionKey, "sessionKey", 32);
8113
+ const signer = await resolveSigner(
8114
+ input.deviceSecret,
8115
+ input.signer,
8116
+ "deviceSecret"
8117
+ );
8118
+ const deviceKey = fromHex(signer.publicKey, "signer.publicKey", 32);
8119
+ const issuedAt = u64le(input.issuedAt);
8120
+ const expiresAt = u64le(input.expiresAt);
8121
+ const preimage = await domainHash(SIGN_DOMAIN5, [
8122
+ node,
8123
+ audienceSigningBytes(input.audience),
8124
+ challenge,
8125
+ sessionKey,
8126
+ deviceKey,
8127
+ issuedAt,
8128
+ expiresAt
8129
+ ]);
8130
+ const signature = await signer.sign(preimage);
8131
+ return hex(
8132
+ concat(
8133
+ node,
8134
+ audienceWire(input.audience),
8135
+ challenge,
8136
+ sessionKey,
8137
+ deviceKey,
8138
+ issuedAt,
8139
+ expiresAt,
8140
+ signature
8141
+ )
8142
+ );
8143
+ }
8144
+
8145
+ // src/login/session.ts
8146
+ var DEFAULT_TTL_SECONDS2 = 300;
8147
+ async function generateSessionKey() {
8148
+ const secret = hex(crypto.getRandomValues(new Uint8Array(32)));
8149
+ return { publicKey: hex(await derivePublicKey(secret)), secret };
8150
+ }
8151
+ async function login(config) {
8152
+ const baseUrl = config.nodeUrl.replace(/\/+$/, "");
8153
+ const timeoutMs = config.timeoutMs ?? 1e4;
8154
+ const request = async (path, init) => {
8155
+ const controller = new AbortController();
8156
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8157
+ let response;
8158
+ try {
8159
+ const requestInit = { ...init, signal: controller.signal };
8160
+ response = config.fetch ? await config.fetch(`${baseUrl}${path}`, requestInit) : await globalThis.fetch(`${baseUrl}${path}`, requestInit);
8161
+ } catch (err) {
8162
+ throw new HTTPError(
8163
+ 0,
8164
+ err instanceof Error ? err.name : "NetworkError",
8165
+ `${baseUrl}${path}`,
8166
+ new Headers(),
8167
+ err instanceof Error ? err.message : String(err)
8168
+ );
8169
+ } finally {
8170
+ clearTimeout(timer);
8171
+ }
8172
+ const text = await response.text().catch(() => "");
8173
+ if (!response.ok) {
8174
+ throw new HTTPError(
8175
+ response.status,
8176
+ response.statusText,
8177
+ `${baseUrl}${path}`,
8178
+ response.headers,
8179
+ text
8180
+ );
8181
+ }
8182
+ return text ? JSON.parse(text) : {};
8183
+ };
8184
+ const challengeBody = await request("/auth/challenge", {
8185
+ method: "GET",
8186
+ headers: { Accept: "application/json" }
8187
+ });
8188
+ const challenge = challengeBody.challenge ?? challengeBody.data?.challenge;
8189
+ if (!challenge) {
8190
+ throw new Error("the node issued no challenge");
8191
+ }
8192
+ const session = await generateSessionKey();
8193
+ const issuedAt = Math.floor(Date.now() / 1e3);
8194
+ const statement = await signLoginStatement({
8195
+ node: config.node,
8196
+ audience: config.audience,
8197
+ challenge,
8198
+ sessionKey: session.publicKey,
8199
+ issuedAt,
8200
+ expiresAt: issuedAt + (config.ttlSeconds ?? DEFAULT_TTL_SECONDS2),
8201
+ // Resolved once and handed on, so a config carrying a secret and a config
8202
+ // carrying a `CryptoKey` take exactly the same path from here down.
8203
+ signer: await resolveSigner(config.deviceSecret, config.signer, "deviceSecret")
8204
+ });
8205
+ const tokenBody = await request("/auth/token", {
8206
+ method: "POST",
8207
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
8208
+ body: JSON.stringify({
8209
+ auth_method: "account_proof",
8210
+ public_key: session.publicKey,
8211
+ client_name: config.clientName ?? baseUrl,
8212
+ timestamp: issuedAt,
8213
+ provider_data: {
8214
+ challenge,
8215
+ login_statement: statement,
8216
+ account_proof: config.accountProof
8217
+ }
8218
+ })
8219
+ });
8220
+ const accessToken = tokenBody.access_token ?? tokenBody.data?.access_token;
8221
+ const refreshToken = tokenBody.refresh_token ?? tokenBody.data?.refresh_token;
8222
+ if (!accessToken) {
8223
+ throw new Error("the node minted no session");
8224
+ }
8225
+ return {
8226
+ accessToken,
8227
+ refreshToken: refreshToken ?? "",
8228
+ sessionKey: session.publicKey,
8229
+ sessionSecret: session.secret
8230
+ };
8231
+ }
8232
+
8233
+ // src/transport/relay-observer.ts
8234
+ var EXPIRY_SKEW_MS = 3e4;
8235
+ function defaultAudience() {
8236
+ const origin = globalThis.location?.origin;
8237
+ return origin ? { kind: "webOrigin", origin } : { kind: "cli" };
8238
+ }
8239
+ function jwtExpiryMs(token) {
8240
+ try {
8241
+ const parts = token.split(".");
8242
+ if (parts.length !== 3) return null;
8243
+ let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
8244
+ while (b64.length % 4) b64 += "=";
8245
+ const payload = JSON.parse(atob(b64));
8246
+ return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
8247
+ } catch {
8248
+ return null;
8249
+ }
8250
+ }
8251
+ var RelayObserver = class {
8252
+ constructor(config) {
8253
+ this.session = null;
8254
+ this.sessionExpiresAt = null;
8255
+ /** In-flight login, so two surfaces asking at once mint one session, not two. */
8256
+ this.pending = null;
8257
+ this.sseClient = null;
8258
+ this.wsClient = null;
8259
+ this.config = config;
8260
+ }
8261
+ /** The node key this observer logs in with. Read for diagnostics. */
8262
+ get nodeKey() {
8263
+ return this.config.nodeKey;
8264
+ }
8265
+ /**
8266
+ * A usable access token, logging in if there is not one.
8267
+ *
8268
+ * Re-mints once the current token is within {@link EXPIRY_SKEW_MS} of
8269
+ * expiring, and on every call when the token's lifetime cannot be read —
8270
+ * reconnects are backed off, so an extra handshake costs far less than a
8271
+ * stream that silently stops. The refresh token is *held* but not spent:
8272
+ * rotating it is tracked in #87/#88 and a second login is correct in the
8273
+ * meantime, where re-presenting an unrotated refresh token would not be.
8274
+ */
8275
+ async accessToken() {
8276
+ const now = Date.now();
8277
+ if (this.session && this.sessionExpiresAt !== null && this.sessionExpiresAt - EXPIRY_SKEW_MS > now) {
8278
+ return this.session.accessToken;
8279
+ }
8280
+ if (!this.pending) {
8281
+ this.pending = this.loginNow().finally(() => {
8282
+ this.pending = null;
8283
+ });
8284
+ }
8285
+ const session = await this.pending;
8286
+ return session.accessToken;
8287
+ }
8288
+ async loginNow() {
8289
+ const session = await login({
8290
+ nodeUrl: this.config.nodeUrl,
8291
+ node: this.config.nodeKey,
8292
+ signer: await this.config.signer(),
8293
+ accountProof: this.config.accountProof,
8294
+ audience: this.config.audience,
8295
+ clientName: this.config.clientName,
8296
+ ttlSeconds: this.config.ttlSeconds,
8297
+ fetch: this.config.fetch,
8298
+ timeoutMs: this.config.timeoutMs
8299
+ });
8300
+ this.session = session;
8301
+ this.sessionExpiresAt = jwtExpiryMs(session.accessToken);
8302
+ return session;
8303
+ }
8304
+ /**
8305
+ * SSE events over the relay node — the same `SseClient` a node client hands
8306
+ * out, constructed the same way, so nothing downstream can tell which
8307
+ * transport produced it.
8308
+ */
8309
+ get events() {
8310
+ if (!this.sseClient) {
8311
+ this.sseClient = new SseClient({
8312
+ baseUrl: this.config.nodeUrl,
8313
+ getAuthToken: () => this.accessToken()
8314
+ });
8315
+ }
8316
+ return this.sseClient;
8317
+ }
8318
+ /** WebSocket events over the relay node. @experimental — prefer `events`. */
8319
+ get ws() {
8320
+ if (!this.wsClient) {
8321
+ this.wsClient = new WsClient({
8322
+ baseUrl: this.config.nodeUrl,
8323
+ getAuthToken: () => this.accessToken()
8324
+ });
8325
+ }
8326
+ return this.wsClient;
8327
+ }
8328
+ /** Close whatever was opened. Never touches the relay's write path. */
8329
+ close() {
8330
+ this.sseClient?.close();
8331
+ this.wsClient?.close();
8332
+ }
8333
+ };
8334
+
8335
+ // src/request-proof/request-proof.ts
8336
+ var utf8 = (text) => new TextEncoder().encode(text);
8337
+ var REQUEST_SIGN_DOMAIN = utf8("calimero.auth.request.v1");
8338
+ var REQUEST_BODY_DOMAIN = utf8("calimero.auth.request.body.v1");
8339
+ var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
8340
+ var DEFAULT_VALID_FOR = 300;
8341
+ async function signRequestProof(input) {
8342
+ const { method, path, credential } = input;
8343
+ if (!method) throw new Error("signRequestProof needs the method the request will use");
8344
+ if (!path.startsWith("/")) {
8345
+ throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
8346
+ }
8347
+ if (path.includes("?")) {
8348
+ throw new Error(
8349
+ `the path must carry no query string, got ${JSON.stringify(path)} \u2014 a query is excluded from the signature on purpose, because a proxy may rewrite one. Anything that must be bound belongs in the body.`
8350
+ );
8351
+ }
8352
+ const bodyBytes2 = input.body === void 0 ? new Uint8Array() : typeof input.body === "string" ? utf8(input.body) : input.body;
8353
+ const bodyHash = await domainHash(REQUEST_BODY_DOMAIN, [bodyBytes2]);
8354
+ const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
8355
+ const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
8356
+ const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
8357
+ utf8(method),
8358
+ utf8(path),
8359
+ bodyHash,
8360
+ u64le(issuedAt),
8361
+ u64le(expiresAt)
8362
+ ]);
8363
+ const signature = await signPayload(payload, input);
8364
+ if (signature.length !== 64) {
8365
+ throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
8366
+ }
8367
+ const requestSig = concat(
8368
+ u32le(utf8(method).length),
8369
+ utf8(method),
8370
+ u32le(utf8(path).length),
8371
+ utf8(path),
8372
+ bodyHash,
8373
+ u64le(issuedAt),
8374
+ u64le(expiresAt),
8375
+ signature
8376
+ );
8377
+ const accountProof = hexToBytes(credential, "credential");
8378
+ return hex(concat(accountProof, new Uint8Array([0]), requestSig));
8379
+ }
8380
+ async function signPayload(payload, input) {
8381
+ if (input.signer) return input.signer.sign(payload);
8382
+ if (!input.deviceSecret) {
8383
+ throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
8384
+ }
8385
+ const key = await importSigningKey(input.deviceSecret);
8386
+ return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
8387
+ }
8388
+ function hexToBytes(value, label2) {
8389
+ const clean = value.trim();
8390
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
8391
+ throw new Error(`${label2} is not an even-length hex string`);
8392
+ }
8393
+ return fromHex(clean, label2, clean.length / 2);
8394
+ }
8395
+
8396
+ // src/request-proof/authorizer.ts
8397
+ function createProofAuthorizer(credential) {
8398
+ if (!credential.credential) {
8399
+ throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
8400
+ }
8401
+ if (!credential.deviceSecret && !credential.signer) {
8402
+ throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
8403
+ }
8404
+ return async ({ method, path, body }) => {
8405
+ const proof = await signRequestProof({
8406
+ method,
8407
+ path,
8408
+ body,
8409
+ credential: credential.credential,
8410
+ deviceSecret: credential.deviceSecret,
8411
+ signer: credential.signer,
8412
+ validForSeconds: credential.validForSeconds
8413
+ });
8414
+ return { [REQUEST_PROOF_HEADER]: proof };
8415
+ };
8416
+ }
8417
+ function signedPath(url) {
8418
+ return new URL(url).pathname;
8419
+ }
8420
+
8421
+ // src/transport/mero-client.ts
8422
+ function relayHasNo(surface, why) {
8423
+ return new Error(
8424
+ `this client was constructed with the relay transport, which has no ${surface}: ${why}. Construct a node client (createMeroClient({ baseUrl })) for that surface \u2014 nothing here will silently connect to a node you did not choose.`
8425
+ );
8426
+ }
8427
+ function missingNodeKey(surface) {
8428
+ return new Error(
8429
+ `this relay-transport client cannot open ${surface}: it was given no relay node key. A relay is an ordinary node and does serve /sse and /ws \u2014 but the login statement must be signed against the node's device signing key, learned out of band (never read from the node, and never derived from its peerId). Pass it as \`observe: { nodeKey }\`. For a cloud-hosted relay the cloud does not publish it yet \u2014 see mdma #312.`
8430
+ );
8431
+ }
8432
+ function buildObserver(relay, observe) {
8433
+ const nodeKey = observe?.nodeKey;
8434
+ if (!nodeKey) return null;
8435
+ return new RelayObserver({
8436
+ nodeUrl: observe?.nodeUrl ?? relay.relayUrl,
8437
+ nodeKey,
8438
+ accountProof: observe?.accountProof ?? relay.authorProof,
8439
+ audience: observe?.audience ?? defaultAudience(),
8440
+ signer: observe?.signer ? async () => observe.signer : () => relay.authorSigner(),
8441
+ clientName: observe?.clientName,
8442
+ ttlSeconds: observe?.ttlSeconds,
8443
+ fetch: observe?.fetch,
8444
+ timeoutMs: observe?.timeoutMs
8445
+ });
8446
+ }
8447
+ var MeroClient = class {
8448
+ constructor(config) {
8449
+ /** The relay's event session, or `null` on a node client / with no node key. */
8450
+ this.relayObserver = null;
8451
+ /** Admin reads against the relay, when a session or a proof was supplied. */
8452
+ this.relayAdmin = null;
8453
+ /**
8454
+ * Events off the relay authenticated by a request proof, built lazily.
8455
+ *
8456
+ * Separate from {@link relayObserver} rather than a mode inside it: the
8457
+ * observer's whole subject is a session addressed to a node key, and a proof
8458
+ * has neither. Holding the authorizer instead of the client keeps construction
8459
+ * free of the first signature.
8460
+ */
8461
+ this.proofAuthorizer = null;
8462
+ this.relayUrl = null;
8463
+ this.proofEvents = null;
8464
+ if (config.transport === "relay") {
8465
+ this.transport = "relay";
8466
+ this.nodeClient = null;
8467
+ const relay = config.relay instanceof RelayClient ? config.relay : new RelayClient(config.relay);
8468
+ this.relayObserver = buildObserver(relay, config.observe);
8469
+ this.rpcTransport = new RelayTransport(relay, this.relayObserver);
8470
+ this.relayUrl = relay.relayUrl;
8471
+ if (config.proof) {
8472
+ this.proofAuthorizer = createProofAuthorizer(config.proof);
8473
+ }
8474
+ if (config.session) {
8475
+ const token = config.session;
8476
+ this.relayAdmin = createAdminApiClient({
8477
+ baseUrl: relay.relayUrl,
8478
+ getAuthToken: typeof token === "string" ? async () => token : token
8479
+ });
8480
+ } else if (this.proofAuthorizer) {
8481
+ const authorizeRequest = this.proofAuthorizer;
8482
+ this.relayAdmin = createAdminApiClientFromHttpClient(
8483
+ createHttpClient({
8484
+ fetch: (url, init) => globalThis.fetch(url, init),
8485
+ baseUrl: relay.relayUrl,
8486
+ authorizeRequest
8487
+ }),
8488
+ { baseUrl: relay.relayUrl }
8489
+ );
8490
+ }
8491
+ return;
8492
+ }
8493
+ this.transport = "node";
8494
+ const { transport: _ignored, ...nodeConfig } = config;
8495
+ this.nodeClient = new MeroJs(nodeConfig);
8496
+ this.rpcTransport = this.nodeClient.rpc;
8497
+ }
8498
+ /**
8499
+ * The write surface. Identical on both transports — this is the whole point.
8500
+ *
8501
+ * On a node this IS the `RpcClient` a `MeroJs` would have handed out, not a
8502
+ * wrapper around it, so `client.rpc` and `client.node.rpc` are the same
8503
+ * object.
8504
+ */
8505
+ get rpc() {
8506
+ return this.rpcTransport;
8507
+ }
8508
+ /**
8509
+ * Whether `events` / `ws` will work on this client.
8510
+ *
8511
+ * Always `true` on the node transport. On the relay transport it says whether
8512
+ * a node key was supplied — the relay can be observed, but only by a caller
8513
+ * who can name the key the login statement is signed against.
8514
+ */
8515
+ get canSubscribe() {
8516
+ return this.rpcTransport.canSubscribe || this.proofAuthorizer !== null;
8517
+ }
8518
+ /** The underlying `MeroJs`. Node transport only. */
8519
+ get node() {
8520
+ if (!this.nodeClient) {
8521
+ throw relayHasNo("node connection", "it holds a signing key and a relay URL, not a node");
8522
+ }
8523
+ return this.nodeClient;
8524
+ }
8525
+ /** The underlying `RelayClient`, for `describe()`. Relay transport only. */
8526
+ get relay() {
8527
+ if (!(this.rpcTransport instanceof RelayTransport)) {
8528
+ throw new Error("this client was constructed with the node transport and has no relay");
8529
+ }
8530
+ return this.rpcTransport.client;
8531
+ }
8532
+ /**
8533
+ * Admin API. Node transport only.
8534
+ *
8535
+ * Two different things live behind this accessor today, and only one of them
8536
+ * is genuinely unavailable through a relay.
8537
+ *
8538
+ * The **mutations** — `createNamespace`, `setMemberCapabilities`,
8539
+ * `upgradeGroup` and the rest — are operator actions on somebody else's
8540
+ * machine. A keyholder does not get those, and should not; admin is for
8541
+ * admins.
8542
+ *
8543
+ * The **reads** are a different matter. `getContext`, `getContexts`,
8544
+ * `getApplication`, `listNamespaces`, `getBlob` are an app asking what it is
8545
+ * looking at, not an operator administering anything. They sit under
8546
+ * `/admin-api/` for historical reasons, and a delegated session is refused
8547
+ * them (measured: `403`, while the same session reads context state through
8548
+ * `POST /contexts/{ctx}/query` and writes through `/intents` quite happily).
8549
+ *
8550
+ * So this is the one place where "the same code runs on both transports"
8551
+ * does not hold yet, and it is a missing surface rather than a missing
8552
+ * permission. A transport-independent read API is designed but not built; the
8553
+ * throw below is deliberate until it exists, because the alternative — quietly
8554
+ * reaching for a node the caller did not choose — is worse than a clear stop.
8555
+ */
8556
+ /**
8557
+ * The admin surface.
8558
+ *
8559
+ * On a node client this is the node's own. On a relay client it exists only
8560
+ * when a `session` was supplied, and it is **the relay's admin API
8561
+ * authenticated as the account** — so the caller-scoped reads answer:
8562
+ * `getContexts` and `listNamespaces` return this account's own, because
8563
+ * `account_proof` grants `context:list-own` and `namespace:list-own`.
8564
+ *
8565
+ * Mutations are not filtered out here, deliberately. A delegated session does
8566
+ * not carry the grants they require, so the node refuses them with a `403`
8567
+ * that names the missing permission — which is a better answer than a
8568
+ * client-side guess, and keeps the policy in the one place that owns it.
8569
+ */
8570
+ get admin() {
8571
+ if (this.nodeClient) return this.nodeClient.admin;
8572
+ if (this.relayAdmin) return this.relayAdmin;
8573
+ throw new Error(
8574
+ "this relay-transport client has no admin surface because it was given neither a `proof` nor a `session`. A warrant authorises one intent and is not a credential for reading. The caller-scoped reads (getContexts, listNamespaces) want one of the other two: pass `proof: { credential, deviceSecret }` \u2014 the device cert you already hold, which needs no login and no node key \u2014 or a `session` minted with `login()`. Context STATE reads work through execute/query either way."
8575
+ );
8576
+ }
8577
+ /** Auth API. Node transport only — a relay issues no node credential. */
8578
+ get auth() {
8579
+ return this.node.auth;
8580
+ }
8581
+ /**
8582
+ * SSE events — on **both** transports.
8583
+ *
8584
+ * On a relay client this is an `SseClient` against the relay node, fed by a
8585
+ * session minted with the author's account proof. The session is minted
8586
+ * lazily, on the first token the stream asks for, so reading this getter
8587
+ * performs no network call.
8588
+ *
8589
+ * Throws only when the relay client was given no node key: the one input
8590
+ * that cannot be inferred. See {@link missingNodeKey}.
8591
+ */
8592
+ get events() {
8593
+ if (this.nodeClient) return this.nodeClient.events;
8594
+ if (this.relayObserver) return this.relayObserver.events;
8595
+ if (this.proofAuthorizer && this.relayUrl) {
8596
+ this.proofEvents ?? (this.proofEvents = new SseClient({
8597
+ baseUrl: this.relayUrl,
8598
+ authorize: this.proofAuthorizer
8599
+ }));
8600
+ return this.proofEvents;
8601
+ }
8602
+ throw missingNodeKey("SSE events");
8603
+ }
8604
+ /** WebSocket events, on both transports. @experimental — prefer `events`. */
8605
+ get ws() {
8606
+ if (this.nodeClient) return this.nodeClient.ws;
8607
+ if (this.relayObserver) return this.relayObserver.ws;
8608
+ if (this.proofAuthorizer) {
8609
+ throw new Error(
8610
+ "this client authenticates with a request proof, which cannot open a WebSocket from a browser: `WebSocket` cannot set the X-Calimero-Proof header, and core's guard reads the proof from a header only (the ?token= query parameter is the session equivalent and has no proof counterpart). Use `client.events` \u2014 SSE is fetch-based, carries the header, and is the recommended transport anyway."
8611
+ );
8612
+ }
8613
+ throw missingNodeKey("WebSocket events");
8614
+ }
8615
+ /**
8616
+ * Ephemeral presence. Node transport only.
8617
+ *
8618
+ * Not extended to the relay even though a logged-in session could reach both
8619
+ * halves of it mechanically. Publishing resolves the author from an *owned
8620
+ * context identity on the node*, and whether a delegated session has one has
8621
+ * not been established — a presence client that silently published nothing,
8622
+ * or published as the wrong identity, is worse than one that says it is not
8623
+ * available. The read half is not lost: presence arrives on `client.events`
8624
+ * like any other context event, and can be filtered there.
8625
+ *
8626
+ * The rejected alternative was to expose it over the relay session and let
8627
+ * `set` fail at the node. That trades a clear construction-time answer for a
8628
+ * runtime failure inside a UI that has already rendered.
8629
+ */
8630
+ get ephemeral() {
8631
+ if (!this.nodeClient) {
8632
+ throw relayHasNo(
8633
+ "ephemeral presence",
8634
+ "publishing resolves the author from an owned context identity on the node, which a delegated session is not known to have \u2014 read presence off `client.events` instead"
8635
+ );
8636
+ }
8637
+ return this.nodeClient.ephemeral;
8638
+ }
8639
+ /**
8640
+ * Calimero Cloud.
8641
+ *
8642
+ * Available on both transports, because it is neither: the cloud session is
8643
+ * independent of the node credential and of the relay's signing key, and a
8644
+ * relay-transport app still needs it to list namespaces and manage billing.
8645
+ */
8646
+ get cloud() {
8647
+ if (!this.nodeClient) {
8648
+ throw relayHasNo(
8649
+ "bundled cloud client",
8650
+ "a relay client is built from a cloud connection rather than owning one \u2014 reach it through the `cloud` field that `connectCloud` returns, or construct a `CloudClient` directly"
8651
+ );
8652
+ }
8653
+ return this.nodeClient.cloud;
7322
8654
  }
7323
- };
7324
- function extractReason(bodyText) {
7325
- if (!bodyText) return "no reason given";
7326
- try {
7327
- const parsed = JSON.parse(bodyText);
7328
- for (const candidate of [parsed.error, parsed.message]) {
7329
- if (typeof candidate === "string" && candidate) return candidate;
7330
- }
7331
- } catch {
8655
+ /**
8656
+ * Release whatever the transport is holding.
8657
+ *
8658
+ * Safe on a relay client, where there is nothing open to close — a caller
8659
+ * that tears down on unmount should not have to know which transport it got.
8660
+ */
8661
+ close() {
8662
+ this.nodeClient?.close();
8663
+ this.relayObserver?.close();
8664
+ this.proofEvents?.close();
7332
8665
  }
7333
- return bodyText;
8666
+ };
8667
+ function createMeroClient(config) {
8668
+ return new MeroClient(config);
7334
8669
  }
7335
8670
 
7336
8671
  // src/relay/nonce-source.ts
@@ -7344,6 +8679,10 @@ function createMemoryNonceSource(start = 1) {
7344
8679
  const value = next;
7345
8680
  next += 1n;
7346
8681
  return Promise.resolve(value);
8682
+ },
8683
+ advanceTo(floor) {
8684
+ if (floor > next) next = floor;
8685
+ return Promise.resolve();
7347
8686
  }
7348
8687
  };
7349
8688
  }
@@ -7354,21 +8693,101 @@ function createLocalStorageNonceSource(key, storage) {
7354
8693
  "no localStorage in this runtime: pass a storage implementation, or use createMemoryNonceSource for a process that owns its whole sequence"
7355
8694
  );
7356
8695
  }
8696
+ const lastHandedOut = () => {
8697
+ const raw = store.getItem(key);
8698
+ if (raw === null || raw === "") return 0n;
8699
+ try {
8700
+ const parsed = BigInt(raw);
8701
+ return parsed > 0n ? parsed : 0n;
8702
+ } catch {
8703
+ return 0n;
8704
+ }
8705
+ };
7357
8706
  return {
7358
8707
  next() {
7359
- const raw = store.getItem(key);
7360
- let current = 0n;
7361
- if (raw !== null && raw !== "") {
7362
- try {
7363
- const parsed = BigInt(raw);
7364
- if (parsed > 0n) current = parsed;
7365
- } catch {
7366
- current = 0n;
7367
- }
7368
- }
7369
- const value = current + 1n;
8708
+ const value = lastHandedOut() + 1n;
7370
8709
  store.setItem(key, value.toString());
7371
8710
  return Promise.resolve(value);
8711
+ },
8712
+ advanceTo(floor) {
8713
+ const wanted = floor > 0n ? floor - 1n : 0n;
8714
+ if (wanted > lastHandedOut()) store.setItem(key, wanted.toString());
8715
+ return Promise.resolve();
8716
+ }
8717
+ };
8718
+ }
8719
+ function authorNonceLookup(options) {
8720
+ return {
8721
+ getWarrantNonce: (contextId, _authorDeviceKey) => options.client.getWarrantNonceAsAuthor(contextId, options.authorProof)
8722
+ };
8723
+ }
8724
+ var WarrantNonceExhaustedError = class extends Error {
8725
+ constructor(contextId, authorDeviceKey) {
8726
+ super(
8727
+ `author device ${authorDeviceKey} has spent every nonce in context ${contextId}; it must re-key to write again`
8728
+ );
8729
+ this.contextId = contextId;
8730
+ this.authorDeviceKey = authorDeviceKey;
8731
+ this.name = "WarrantNonceExhaustedError";
8732
+ }
8733
+ };
8734
+ function createRecoveringNonceSource(options) {
8735
+ const lookups = Array.isArray(options.lookups) ? options.lookups : [options.lookups];
8736
+ if (lookups.length === 0) {
8737
+ throw new Error("a recovering nonce source needs at least one node to ask");
8738
+ }
8739
+ const local = options.local ?? createMemoryNonceSource(1);
8740
+ const onLookupFailure = options.onLookupFailure ?? "throw";
8741
+ let floor = null;
8742
+ let pending = null;
8743
+ const resolveFloor = async () => {
8744
+ const answers = await Promise.allSettled(
8745
+ lookups.map(
8746
+ (lookup) => lookup.getWarrantNonce(options.contextId, options.authorDeviceKey)
8747
+ )
8748
+ );
8749
+ let highest = null;
8750
+ let exhausted = false;
8751
+ for (const answer of answers) {
8752
+ if (answer.status !== "fulfilled") continue;
8753
+ const state = answer.value;
8754
+ if (state.kind === "exhausted") {
8755
+ exhausted = true;
8756
+ continue;
8757
+ }
8758
+ if (highest === null || state.nextNonce > highest) highest = state.nextNonce;
8759
+ }
8760
+ if (highest === null && exhausted) {
8761
+ throw new WarrantNonceExhaustedError(options.contextId, options.authorDeviceKey);
8762
+ }
8763
+ if (highest === null) {
8764
+ const first = answers.find((a) => a.status === "rejected");
8765
+ const reason = first && first.status === "rejected" ? first.reason : void 0;
8766
+ if (onLookupFailure === "local") {
8767
+ floor = 0n;
8768
+ return;
8769
+ }
8770
+ throw reason instanceof Error ? reason : new Error(`no node could answer where the warrant-nonce sequence stands: ${String(reason)}`);
8771
+ }
8772
+ floor = highest;
8773
+ await local.advanceTo?.(highest);
8774
+ };
8775
+ return {
8776
+ async next() {
8777
+ if (floor === null) {
8778
+ pending ?? (pending = resolveFloor().finally(() => {
8779
+ pending = null;
8780
+ }));
8781
+ await pending;
8782
+ }
8783
+ const mine = await local.next();
8784
+ const value = floor !== null && floor > mine ? floor : mine;
8785
+ floor = value + 1n;
8786
+ return value;
8787
+ },
8788
+ async advanceTo(wanted) {
8789
+ if (floor === null || wanted > floor) floor = wanted;
8790
+ await local.advanceTo?.(wanted);
7372
8791
  }
7373
8792
  };
7374
8793
  }
@@ -7541,7 +8960,7 @@ var MAX_HANDSHAKE_REPLY = 1024;
7541
8960
  var RENEW_BEFORE_MS = 6e4;
7542
8961
  var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
7543
8962
  var NULL_BODY_STATUSES = [101, 204, 205, 304];
7544
- var utf8 = new TextEncoder();
8963
+ var utf82 = new TextEncoder();
7545
8964
  var StaleTransportKeyError = class extends Error {
7546
8965
  constructor() {
7547
8966
  super("The node no longer holds the attested transport key; attest again for the current one");
@@ -7549,18 +8968,18 @@ var StaleTransportKeyError = class extends Error {
7549
8968
  }
7550
8969
  };
7551
8970
  var SealedTransportError = class extends Error {
7552
- constructor(status, code, message, retryAfterMs) {
8971
+ constructor(status, code, message, retryAfterMs2) {
7553
8972
  super(message);
7554
8973
  this.status = status;
7555
8974
  this.code = code;
7556
- this.retryAfterMs = retryAfterMs;
8975
+ this.retryAfterMs = retryAfterMs2;
7557
8976
  this.name = "SealedTransportError";
7558
8977
  }
7559
8978
  };
7560
8979
  async function transportKeyBinding(inner, transportKey) {
7561
8980
  const digest = await crypto.subtle.digest(
7562
8981
  "SHA-256",
7563
- concat(utf8.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
8982
+ concat(utf82.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
7564
8983
  );
7565
8984
  return new Uint8Array(digest);
7566
8985
  }
@@ -7764,7 +9183,7 @@ function nonce(requestId, index) {
7764
9183
  return bytes;
7765
9184
  }
7766
9185
  function joinInner(head, body) {
7767
- const encoded = utf8.encode(JSON.stringify(head));
9186
+ const encoded = utf82.encode(JSON.stringify(head));
7768
9187
  const len = new Uint8Array(4);
7769
9188
  new DataView(len.buffer).setUint32(0, encoded.length, false);
7770
9189
  return concat(len, encoded, body);
@@ -7873,8 +9292,8 @@ async function envelopeRefusal(response) {
7873
9292
  }
7874
9293
  if (code === "stale_transport_key") return new StaleTransportKeyError();
7875
9294
  const retryAfter = Number(response.headers.get("retry-after"));
7876
- const retryAfterMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 5) * 1e3 : void 0;
7877
- return new SealedTransportError(response.status, code, message, retryAfterMs);
9295
+ const retryAfterMs2 = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 5) * 1e3 : void 0;
9296
+ return new SealedTransportError(response.status, code, message, retryAfterMs2);
7878
9297
  }
7879
9298
 
7880
9299
  // src/cloud/connect.ts
@@ -7882,6 +9301,7 @@ async function connectCloud(options) {
7882
9301
  if (!options.sessionToken && !options.googleIdToken) {
7883
9302
  throw new Error("connectCloud needs either a sessionToken or a googleIdToken");
7884
9303
  }
9304
+ const signer = await resolveSigner(options.deviceSecret, options.signer, "deviceSecret");
7885
9305
  const cloud = new CloudClient({
7886
9306
  cloudBaseUrl: options.cloudBaseUrl,
7887
9307
  sessionToken: options.sessionToken,
@@ -7907,8 +9327,8 @@ async function connectCloud(options) {
7907
9327
  executorAccount: relayInfo.executorAccount,
7908
9328
  authorAccount: options.authorAccount,
7909
9329
  authorProof: options.authorProof,
7910
- deviceSecret: options.deviceSecret,
7911
- nonces: options.nonces ?? await defaultNonceSource(options.deviceSecret),
9330
+ signer,
9331
+ nonces: options.nonces ?? defaultNonceSource(signer.publicKey),
7912
9332
  ttlSeconds: options.ttlSeconds,
7913
9333
  // Only the relay is sealed: a sealed fetch is bound to one node and refuses
7914
9334
  // any other URL, and the cloud calls above carry no intent.
@@ -7925,9 +9345,50 @@ async function connectCloud(options) {
7925
9345
  namespaceId: namespace.namespaceId,
7926
9346
  namespace,
7927
9347
  relayInfo,
7928
- execute: (contextId, method, argsJson) => relay.execute(contextId, method, argsJson)
9348
+ execute: (contextId, method, argsJson) => relay.execute(contextId, method, argsJson),
9349
+ // Built from the same `relay`, not a second one: one nonce sequence, one
9350
+ // warrant signer. Two clients over one relay would hand out the same nonce
9351
+ // twice and have the network refuse the loser as a replay.
9352
+ //
9353
+ // `observe` is always passed, with whatever key is known — the caller's if
9354
+ // they had one, otherwise the cloud's, which is `null` until mdma #312. One
9355
+ // code path rather than two, so the hosted case starts observing the day
9356
+ // the field appears, without a line changing here or at the call site.
9357
+ client: new MeroClient({
9358
+ transport: "relay",
9359
+ relay,
9360
+ observe: {
9361
+ nodeKey: options.relayNodeKey ?? relayInfo.nodeKey,
9362
+ audience: options.observeAudience,
9363
+ fetch: options.fetch,
9364
+ timeoutMs: options.timeoutMs
9365
+ }
9366
+ })
7929
9367
  };
7930
9368
  }
9369
+ async function connectCloudWithAccount(options) {
9370
+ const { root, authorAccount, ...rest } = options;
9371
+ const signer = await resolveSigner(rest.deviceSecret, rest.signer, "deviceSecret");
9372
+ const resolved = await resolveRoot(root);
9373
+ const cloud = new CloudClient({
9374
+ cloudBaseUrl: options.cloudBaseUrl,
9375
+ // Reported here as well as on the connection's own client: this is where
9376
+ // the session is first minted, and a caller persisting it would otherwise
9377
+ // only hear about the rolling refreshes of a token it never saw.
9378
+ onSession: options.onSession,
9379
+ fetch: options.fetch,
9380
+ timeoutMs: options.timeoutMs
9381
+ });
9382
+ const session = await cloud.signInWithAccount(resolved.signer);
9383
+ return connectCloud({
9384
+ ...rest,
9385
+ // Already resolved above, and passing both forms on would be refused.
9386
+ deviceSecret: void 0,
9387
+ signer,
9388
+ sessionToken: session.sessionToken,
9389
+ authorAccount: authorAccount ?? resolved.accountId
9390
+ });
9391
+ }
7931
9392
  function pickNamespace(namespaces, requested) {
7932
9393
  if (requested) {
7933
9394
  const found = namespaces.find((n) => n.namespaceId === requested);
@@ -7964,8 +9425,8 @@ function explainNoRelay(namespace, relays) {
7964
9425
  }
7965
9426
  return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none is ready to relay yet (a URL, an executor account and a recent heartbeat) \u2014 retry shortly`;
7966
9427
  }
7967
- async function defaultNonceSource(deviceSecret) {
7968
- const key = `mero-js:warrant-nonce:${hex(await derivePublicKey(deviceSecret))}`;
9428
+ function defaultNonceSource(devicePublicKey) {
9429
+ const key = `mero-js:warrant-nonce:${devicePublicKey}`;
7969
9430
  try {
7970
9431
  return createLocalStorageNonceSource(key);
7971
9432
  } catch {
@@ -7973,6 +9434,168 @@ async function defaultNonceSource(deviceSecret) {
7973
9434
  }
7974
9435
  }
7975
9436
 
9437
+ // src/cloud/link-redirect.ts
9438
+ var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
9439
+ function cloudLinkUrl(options) {
9440
+ if (!/^[0-9a-f]{64}$/.test(options.accountId)) {
9441
+ throw new Error("accountId must be 64 lowercase hex characters");
9442
+ }
9443
+ const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
9444
+ url.searchParams.set("link-account", options.accountId);
9445
+ url.searchParams.set("callback-url", options.returnTo);
9446
+ return url.toString();
9447
+ }
9448
+ function beginCloudLink(options) {
9449
+ window.location.assign(cloudLinkUrl(options));
9450
+ throw new Error("redirecting to the cloud to authorise this link");
9451
+ }
9452
+ function readCloudLinkCallback(location2 = window.location) {
9453
+ const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
9454
+ const error = params.get("error");
9455
+ if (error) {
9456
+ stripFragment();
9457
+ throw new Error(
9458
+ error === "denied" ? "the account link was declined at the cloud" : `the cloud refused this link: ${error}`
9459
+ );
9460
+ }
9461
+ const grant = params.get("grant");
9462
+ const account = params.get("account");
9463
+ if (!grant || !account) return null;
9464
+ stripFragment();
9465
+ return { grant, account };
9466
+ }
9467
+ function stripFragment() {
9468
+ if (typeof history === "undefined" || !history.replaceState) return;
9469
+ history.replaceState(null, "", location.pathname + location.search);
9470
+ }
9471
+ async function completeCloudLink(options) {
9472
+ const root = await resolveRoot(options.root);
9473
+ if (root.accountId !== options.account) {
9474
+ throw new Error(
9475
+ `this grant is for account ${options.account}, but the root here derives ${root.accountId} \u2014 it was minted for a different account`
9476
+ );
9477
+ }
9478
+ const cloud = new CloudClient({
9479
+ cloudBaseUrl: options.cloudBaseUrl,
9480
+ fetch: options.fetch
9481
+ });
9482
+ return cloud.linkAccountWithGrant({ grant: options.grant, signer: root.signer });
9483
+ }
9484
+
9485
+ // src/cloud/enrol-redirect.ts
9486
+ var DEVICE_PARAM = "enrol-device";
9487
+ var KEM_PARAM = "enrol-kem";
9488
+ var CALLBACK_PARAM = "callback-url";
9489
+ var STATE_PARAM = "state";
9490
+ function deviceEnrolmentUrl(options) {
9491
+ assertHex(options.devicePublicKey, "devicePublicKey");
9492
+ assertHex(options.kemPublicKey, "kemPublicKey");
9493
+ assertSafeReturnTo(options.returnTo);
9494
+ warnOnForeignCallback(options.returnTo);
9495
+ const url = new URL(options.walletUrl);
9496
+ url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
9497
+ url.searchParams.set(KEM_PARAM, options.kemPublicKey);
9498
+ url.searchParams.set(CALLBACK_PARAM, options.returnTo);
9499
+ if (options.state) url.searchParams.set(STATE_PARAM, options.state);
9500
+ return url.toString();
9501
+ }
9502
+ function beginDeviceEnrolment(options) {
9503
+ window.location.assign(deviceEnrolmentUrl(options));
9504
+ throw new Error("redirecting to the wallet to approve this device");
9505
+ }
9506
+ function readEnrolmentCallback(location2 = window.location) {
9507
+ const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
9508
+ const error = params.get("error");
9509
+ if (error) {
9510
+ stripFragment2();
9511
+ throw new Error(
9512
+ error === "denied" || error === "cancelled" ? "the device was not approved at the wallet" : `the wallet refused this enrolment: ${error}`
9513
+ );
9514
+ }
9515
+ const credential = params.get("credential");
9516
+ const account = params.get("account");
9517
+ const device = params.get("device");
9518
+ if (!credential || !account || !device) return null;
9519
+ stripFragment2();
9520
+ const state = params.get("state");
9521
+ return { credential, account, device, ...state ? { state } : {} };
9522
+ }
9523
+ function stripFragment2() {
9524
+ if (typeof history === "undefined" || !history.replaceState) return;
9525
+ history.replaceState(null, "", location.pathname + location.search);
9526
+ }
9527
+ async function completeDeviceEnrolment(options) {
9528
+ if (options.expectState !== void 0 && options.expectState !== options.state) {
9529
+ throw new Error(
9530
+ "this enrolment answers a request this app did not make \u2014 the state it carries is not the one sent"
9531
+ );
9532
+ }
9533
+ const certificate = await verifyDeviceCredential(options.credential);
9534
+ if (certificate.signPublicKey !== normaliseHex(options.devicePublicKey)) {
9535
+ throw new Error(
9536
+ `this credential certifies device key ${certificate.signPublicKey}, but this app holds ${normaliseHex(options.devicePublicKey)} \u2014 it would sign with a key the certificate does not cover`
9537
+ );
9538
+ }
9539
+ if (options.kemPublicKey !== void 0 && certificate.kemPublicKey !== normaliseHex(options.kemPublicKey)) {
9540
+ throw new Error(
9541
+ `this credential names delivery key ${certificate.kemPublicKey}, not the one this app asked about \u2014 group keys would be sealed to somebody else`
9542
+ );
9543
+ }
9544
+ if (certificate.account !== normaliseHex(options.account)) {
9545
+ throw new Error(
9546
+ `the wallet reported account ${normaliseHex(options.account)}, but the credential names ${certificate.account}`
9547
+ );
9548
+ }
9549
+ if (certificate.device !== normaliseHex(options.device)) {
9550
+ throw new Error(
9551
+ `the wallet reported device ${normaliseHex(options.device)}, but the credential names ${certificate.device}`
9552
+ );
9553
+ }
9554
+ return {
9555
+ credential: options.credential,
9556
+ account: certificate.account,
9557
+ device: certificate.device,
9558
+ certificate
9559
+ };
9560
+ }
9561
+ function assertHex(value, label2) {
9562
+ if (!/^[0-9a-f]{64}$/.test(value)) {
9563
+ throw new Error(`${label2} must be 64 lowercase hex characters`);
9564
+ }
9565
+ }
9566
+ function normaliseHex(value) {
9567
+ return value.trim().toLowerCase();
9568
+ }
9569
+ function callbackIsForeign(returnTo, pageOrigin) {
9570
+ const here = pageOrigin ?? (typeof window === "undefined" ? void 0 : window.location.origin);
9571
+ if (!here) return false;
9572
+ try {
9573
+ return new URL(returnTo).origin !== here;
9574
+ } catch {
9575
+ return false;
9576
+ }
9577
+ }
9578
+ function warnOnForeignCallback(returnTo) {
9579
+ if (!callbackIsForeign(returnTo)) return;
9580
+ console.warn(
9581
+ `[calimero] enrolling a device but sending the credential to ${new URL(returnTo).origin}, which is not this page's origin. Legitimate when deliberate; worth a second look otherwise.`
9582
+ );
9583
+ }
9584
+ function assertSafeReturnTo(returnTo) {
9585
+ let url;
9586
+ try {
9587
+ url = new URL(returnTo);
9588
+ } catch {
9589
+ throw new Error(`returnTo must be an absolute URL, got ${returnTo}`);
9590
+ }
9591
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
9592
+ if (url.protocol === "https:") return;
9593
+ if (url.protocol === "http:" && loopback) return;
9594
+ throw new Error(
9595
+ `returnTo must be https:, or http: on localhost \u2014 got ${url.protocol}//${url.hostname}`
9596
+ );
9597
+ }
9598
+
7976
9599
  // src/sealed/verify.ts
7977
9600
  var MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
7978
9601
  var REGISTERS = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
@@ -8341,7 +9964,7 @@ var FULCIO_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
8341
9964
  var SAN_URI = 134;
8342
9965
  var UTF8_STRING = 12;
8343
9966
  var SEQUENCE2 = 48;
8344
- var utf82 = new TextEncoder();
9967
+ var utf83 = new TextEncoder();
8345
9968
  var fromUtf8 = (bytes) => new TextDecoder().decode(bytes);
8346
9969
  async function verifyCosignBundle(body, bundle, signer, signature) {
8347
9970
  const { base64Signature, cert, rekorBundle } = cosignBundle(bundle);
@@ -8354,7 +9977,7 @@ async function verifyCosignBundle(body, bundle, signer, signature) {
8354
9977
  const leaf = parseCertificate(leafDer);
8355
9978
  const log = REKOR_LOGS.find((candidate) => candidate.logId === payload.logID.toLowerCase());
8356
9979
  if (!log) throw new Error(`The bundle names Rekor log ${payload.logID}, which the Sigstore trust root does not know`);
8357
- const canonical = utf82.encode(
9980
+ const canonical = utf83.encode(
8358
9981
  `{"body":${JSON.stringify(payload.body)},"integratedTime":${payload.integratedTime},"logID":${JSON.stringify(payload.logID)},"logIndex":${payload.logIndex}}`
8359
9982
  );
8360
9983
  const setVerifies = await verifyEcdsa(
@@ -8592,162 +10215,70 @@ function createSignedReleaseVerifier(options) {
8592
10215
  minReleaseVersion: options.minReleaseVersion,
8593
10216
  identity: options.identity,
8594
10217
  issuer: options.issuer
8595
- });
8596
- return createQuoteVerifier({
8597
- dcapVerify: options.dcapVerify,
8598
- fetchCollateral: options.fetchCollateral,
8599
- now: options.now,
8600
- ...trusted
8601
- })(args);
8602
- };
8603
- return Object.assign(verifier, { includeCollateral: true });
8604
- }
8605
- function createSignedReleaseSealedFetch(options) {
8606
- const {
8607
- baseUrl,
8608
- releaseSource,
8609
- releaseVersion: fallbackVersion,
8610
- applicationId,
8611
- applicationHash,
8612
- fetch: givenFetch,
8613
- ...verifierOptions
8614
- } = options;
8615
- if (fallbackVersion !== void 0) releaseVersion(fallbackVersion, "releaseVersion");
8616
- const baseFetch = givenFetch ?? ((input, init) => fetch(input, init));
8617
- const release = async () => {
8618
- const version = fallbackVersion === void 0 ? await fetchNodeReleaseVersion(baseUrl, { fetch: baseFetch }) : await readNodeReleaseVersion(baseUrl, baseFetch) ?? fallbackVersion;
8619
- return typeof releaseSource === "function" ? releaseSource({ baseUrl, version, fetch: baseFetch }) : fetchNodeRelease(cloudNodeReleaseUrl(releaseSource ?? DEFAULT_RELEASE_MIRROR, version), { fetch: baseFetch });
8620
- };
8621
- return createAttestedSealedFetch({
8622
- baseUrl,
8623
- verify: createSignedReleaseVerifier({ ...verifierOptions, release }),
8624
- applicationId,
8625
- applicationHash,
8626
- fetch: baseFetch
8627
- });
8628
- }
8629
- var RELEASE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
8630
- function releaseVersion(value, what) {
8631
- const match = typeof value === "string" ? RELEASE_VERSION.exec(value) : null;
8632
- const pre = match?.[4];
8633
- if (!match || pre !== void 0 && pre.split(".").some((id) => id === "")) {
8634
- throw new Error(`${what} is ${JSON.stringify(value)}, not a release version like 2.3.87`);
8635
- }
8636
- return value;
8637
- }
8638
- function compareReleaseVersions(a, b) {
8639
- const [aCore, aPre] = splitVersion2(a);
8640
- const [bCore, bPre] = splitVersion2(b);
8641
- for (let i = 0; i < 3; i += 1) {
8642
- if (aCore[i] !== bCore[i]) return aCore[i] < bCore[i] ? -1 : 1;
8643
- }
8644
- if (aPre === void 0 || bPre === void 0) return aPre === bPre ? 0 : aPre === void 0 ? 1 : -1;
8645
- const aIds = aPre.split(".");
8646
- const bIds = bPre.split(".");
8647
- for (let i = 0; i < Math.max(aIds.length, bIds.length); i += 1) {
8648
- const [x, y] = [aIds[i], bIds[i]];
8649
- if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
8650
- const [xNum, yNum] = [/^\d+$/.test(x), /^\d+$/.test(y)];
8651
- const order = xNum && yNum ? Math.sign(Number(x) - Number(y)) : xNum !== yNum ? xNum ? -1 : 1 : x < y ? -1 : x > y ? 1 : 0;
8652
- if (order !== 0) return order;
8653
- }
8654
- return 0;
8655
- }
8656
- function splitVersion2(version) {
8657
- const match = RELEASE_VERSION.exec(version);
8658
- return [[Number(match[1]), Number(match[2]), Number(match[3])], match[4]];
8659
- }
8660
-
8661
- // src/warrant/governance-op.ts
8662
- var ROLE = {
8663
- Admin: 0,
8664
- Member: 1,
8665
- ReadOnly: 2
8666
- };
8667
- var GROUP_OP = {
8668
- MemberAdded: 1,
8669
- MemberRemoved: 2,
8670
- MemberLeft: 3,
8671
- MemberRoleSet: 4
8672
- };
8673
- var ROOT_OP = {
8674
- GroupCreated: 0,
8675
- GroupReparented: 1,
8676
- GroupDeleted: 2
8677
- };
8678
- var CLEARED_HASH = new Uint8Array(32);
8679
- var EMPTY_VEC = u32le(0);
8680
- function role(value) {
8681
- const byte = ROLE[value];
8682
- if (byte === void 0) {
8683
- throw new Error(`role must be Admin, Member or ReadOnly, got ${String(value)}`);
8684
- }
8685
- return new Uint8Array([byte]);
8686
- }
8687
- function tag(byte) {
8688
- return new Uint8Array([byte]);
8689
- }
8690
- function memberAddedOp(member, memberRole) {
8691
- return {
8692
- kind: "group",
8693
- bytes: concat(tag(GROUP_OP.MemberAdded), fromHex(member, "member", 32), role(memberRole))
8694
- };
8695
- }
8696
- function memberRemovedOp(member) {
8697
- return {
8698
- kind: "group",
8699
- bytes: concat(
8700
- tag(GROUP_OP.MemberRemoved),
8701
- fromHex(member, "member", 32),
8702
- CLEARED_HASH,
8703
- EMPTY_VEC
8704
- )
8705
- };
8706
- }
8707
- function memberLeftOp(member) {
8708
- return {
8709
- kind: "group",
8710
- bytes: concat(
8711
- tag(GROUP_OP.MemberLeft),
8712
- fromHex(member, "member", 32),
8713
- CLEARED_HASH,
8714
- EMPTY_VEC
8715
- )
8716
- };
8717
- }
8718
- function memberRoleSetOp(member, memberRole) {
8719
- return {
8720
- kind: "group",
8721
- bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, "member", 32), role(memberRole))
8722
- };
8723
- }
8724
- function groupCreatedOp(input) {
8725
- return {
8726
- kind: "root",
8727
- bytes: concat(
8728
- tag(ROOT_OP.GroupCreated),
8729
- fromHex(input.groupId, "groupId", 32),
8730
- fromHex(input.parentId, "parentId", 32),
8731
- tag(input.restricted ? 1 : 0),
8732
- fromHex(input.admin, "admin", 32)
8733
- )
10218
+ });
10219
+ return createQuoteVerifier({
10220
+ dcapVerify: options.dcapVerify,
10221
+ fetchCollateral: options.fetchCollateral,
10222
+ now: options.now,
10223
+ ...trusted
10224
+ })(args);
8734
10225
  };
10226
+ return Object.assign(verifier, { includeCollateral: true });
8735
10227
  }
8736
- function groupReparentedOp(childGroupId, newParentId) {
8737
- return {
8738
- kind: "root",
8739
- bytes: concat(
8740
- tag(ROOT_OP.GroupReparented),
8741
- fromHex(childGroupId, "childGroupId", 32),
8742
- fromHex(newParentId, "newParentId", 32)
8743
- )
10228
+ function createSignedReleaseSealedFetch(options) {
10229
+ const {
10230
+ baseUrl,
10231
+ releaseSource,
10232
+ releaseVersion: fallbackVersion,
10233
+ applicationId,
10234
+ applicationHash,
10235
+ fetch: givenFetch,
10236
+ ...verifierOptions
10237
+ } = options;
10238
+ if (fallbackVersion !== void 0) releaseVersion(fallbackVersion, "releaseVersion");
10239
+ const baseFetch = givenFetch ?? ((input, init) => fetch(input, init));
10240
+ const release = async () => {
10241
+ const version = fallbackVersion === void 0 ? await fetchNodeReleaseVersion(baseUrl, { fetch: baseFetch }) : await readNodeReleaseVersion(baseUrl, baseFetch) ?? fallbackVersion;
10242
+ return typeof releaseSource === "function" ? releaseSource({ baseUrl, version, fetch: baseFetch }) : fetchNodeRelease(cloudNodeReleaseUrl(releaseSource ?? DEFAULT_RELEASE_MIRROR, version), { fetch: baseFetch });
8744
10243
  };
10244
+ return createAttestedSealedFetch({
10245
+ baseUrl,
10246
+ verify: createSignedReleaseVerifier({ ...verifierOptions, release }),
10247
+ applicationId,
10248
+ applicationHash,
10249
+ fetch: baseFetch
10250
+ });
8745
10251
  }
8746
- function groupDeletedOp(rootGroupId) {
8747
- return {
8748
- kind: "root",
8749
- bytes: concat(tag(ROOT_OP.GroupDeleted), fromHex(rootGroupId, "rootGroupId", 32), EMPTY_VEC, EMPTY_VEC)
8750
- };
10252
+ var RELEASE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
10253
+ function releaseVersion(value, what) {
10254
+ const match = typeof value === "string" ? RELEASE_VERSION.exec(value) : null;
10255
+ const pre = match?.[4];
10256
+ if (!match || pre !== void 0 && pre.split(".").some((id) => id === "")) {
10257
+ throw new Error(`${what} is ${JSON.stringify(value)}, not a release version like 2.3.87`);
10258
+ }
10259
+ return value;
10260
+ }
10261
+ function compareReleaseVersions(a, b) {
10262
+ const [aCore, aPre] = splitVersion2(a);
10263
+ const [bCore, bPre] = splitVersion2(b);
10264
+ for (let i = 0; i < 3; i += 1) {
10265
+ if (aCore[i] !== bCore[i]) return aCore[i] < bCore[i] ? -1 : 1;
10266
+ }
10267
+ if (aPre === void 0 || bPre === void 0) return aPre === bPre ? 0 : aPre === void 0 ? 1 : -1;
10268
+ const aIds = aPre.split(".");
10269
+ const bIds = bPre.split(".");
10270
+ for (let i = 0; i < Math.max(aIds.length, bIds.length); i += 1) {
10271
+ const [x, y] = [aIds[i], bIds[i]];
10272
+ if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
10273
+ const [xNum, yNum] = [/^\d+$/.test(x), /^\d+$/.test(y)];
10274
+ const order = xNum && yNum ? Math.sign(Number(x) - Number(y)) : xNum !== yNum ? xNum ? -1 : 1 : x < y ? -1 : x > y ? 1 : 0;
10275
+ if (order !== 0) return order;
10276
+ }
10277
+ return 0;
10278
+ }
10279
+ function splitVersion2(version) {
10280
+ const match = RELEASE_VERSION.exec(version);
10281
+ return [[Number(match[1]), Number(match[2]), Number(match[3])], match[4]];
8751
10282
  }
8752
10283
 
8753
10284
  // src/namespace-op/namespace-op.ts
@@ -8841,186 +10372,22 @@ async function signMemberJoinOp(input) {
8841
10372
  credential
8842
10373
  );
8843
10374
  const op = concat(new Uint8Array([NAMESPACE_OP_ROOT]), rootOp);
8844
- const key = await importSigningKey(input.deviceSecret);
8845
- const signer = await derivePublicKey(input.deviceSecret);
10375
+ const signingKey = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
10376
+ const signerPublicKey = fromHex(signingKey.publicKey, "signer.publicKey", 32);
8846
10377
  const signable = concat(
8847
10378
  new Uint8Array([SIGNED_NAMESPACE_OP_SCHEMA_VERSION]),
8848
10379
  fromHex(input.namespaceId, "namespaceId", 32),
8849
10380
  u32le(parents.length),
8850
10381
  ...parents.map((p, i) => fromHex(p, `parentOpHashes[${i}]`, 32)),
8851
- signer,
10382
+ signerPublicKey,
8852
10383
  u64le(input.nonce),
8853
10384
  op
8854
10385
  );
8855
- const signature = new Uint8Array(
8856
- await crypto.subtle.sign("Ed25519", key, concat(NAMESPACE_SIGN_DOMAIN, signable))
8857
- );
10386
+ const signature = await signingKey.sign(concat(NAMESPACE_SIGN_DOMAIN, signable));
8858
10387
  const unendorsed = new Uint8Array([0]);
8859
10388
  return hex(concat(signable, signature, unendorsed));
8860
10389
  }
8861
10390
 
8862
- // src/login/login.ts
8863
- var SIGN_DOMAIN5 = new TextEncoder().encode("calimero.auth.login.v1");
8864
- var AUDIENCE_TAG = {
8865
- webOrigin: 0,
8866
- codeSigningId: 1,
8867
- cli: 2
8868
- };
8869
- function audienceBody(audience) {
8870
- const encoder = new TextEncoder();
8871
- switch (audience.kind) {
8872
- case "webOrigin":
8873
- return encoder.encode(audience.origin);
8874
- case "codeSigningId":
8875
- return encoder.encode(audience.id);
8876
- case "cli":
8877
- return new Uint8Array(0);
8878
- }
8879
- }
8880
- function audienceSigningBytes(audience) {
8881
- return concat(
8882
- Uint8Array.from([AUDIENCE_TAG[audience.kind]]),
8883
- audienceBody(audience)
8884
- );
8885
- }
8886
- function audienceWire(audience) {
8887
- const tag2 = Uint8Array.from([AUDIENCE_TAG[audience.kind]]);
8888
- if (audience.kind === "cli") {
8889
- return tag2;
8890
- }
8891
- const body = audienceBody(audience);
8892
- return concat(tag2, u32le(body.length), body);
8893
- }
8894
- async function signLoginStatement(input) {
8895
- const node = fromHex(input.node, "node", 32);
8896
- const challenge = fromHex(input.challenge, "challenge", 32);
8897
- const sessionKey = fromHex(input.sessionKey, "sessionKey", 32);
8898
- const key = await importSigningKey(input.deviceSecret);
8899
- const deviceKey = await derivePublicKey(input.deviceSecret);
8900
- const issuedAt = u64le(input.issuedAt);
8901
- const expiresAt = u64le(input.expiresAt);
8902
- const preimage = await domainHash(SIGN_DOMAIN5, [
8903
- node,
8904
- audienceSigningBytes(input.audience),
8905
- challenge,
8906
- sessionKey,
8907
- deviceKey,
8908
- issuedAt,
8909
- expiresAt
8910
- ]);
8911
- const signature = new Uint8Array(
8912
- await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
8913
- );
8914
- return hex(
8915
- concat(
8916
- node,
8917
- audienceWire(input.audience),
8918
- challenge,
8919
- sessionKey,
8920
- deviceKey,
8921
- issuedAt,
8922
- expiresAt,
8923
- signature
8924
- )
8925
- );
8926
- }
8927
-
8928
- // src/login/session.ts
8929
- var DEFAULT_TTL_SECONDS2 = 300;
8930
- async function generateSessionKey() {
8931
- const secret = hex(crypto.getRandomValues(new Uint8Array(32)));
8932
- return { publicKey: hex(await derivePublicKey(secret)), secret };
8933
- }
8934
- function audienceLabel(audience) {
8935
- switch (audience.kind) {
8936
- case "webOrigin":
8937
- return audience.origin;
8938
- case "codeSigningId":
8939
- return audience.id;
8940
- case "cli":
8941
- return "cli";
8942
- }
8943
- }
8944
- async function login(config) {
8945
- const baseUrl = config.nodeUrl.replace(/\/+$/, "");
8946
- const timeoutMs = config.timeoutMs ?? 1e4;
8947
- const request = async (path, init) => {
8948
- const controller = new AbortController();
8949
- const timer = setTimeout(() => controller.abort(), timeoutMs);
8950
- let response;
8951
- try {
8952
- const requestInit = { ...init, signal: controller.signal };
8953
- response = config.fetch ? await config.fetch(`${baseUrl}${path}`, requestInit) : await globalThis.fetch(`${baseUrl}${path}`, requestInit);
8954
- } catch (err) {
8955
- throw new HTTPError(
8956
- 0,
8957
- err instanceof Error ? err.name : "NetworkError",
8958
- `${baseUrl}${path}`,
8959
- new Headers(),
8960
- err instanceof Error ? err.message : String(err)
8961
- );
8962
- } finally {
8963
- clearTimeout(timer);
8964
- }
8965
- const text = await response.text().catch(() => "");
8966
- if (!response.ok) {
8967
- throw new HTTPError(
8968
- response.status,
8969
- response.statusText,
8970
- `${baseUrl}${path}`,
8971
- response.headers,
8972
- text
8973
- );
8974
- }
8975
- return text ? JSON.parse(text) : {};
8976
- };
8977
- const challengeBody = await request("/auth/challenge", {
8978
- method: "GET",
8979
- headers: { Accept: "application/json" }
8980
- });
8981
- const challenge = challengeBody.challenge ?? challengeBody.data?.challenge;
8982
- if (!challenge) {
8983
- throw new Error("the node issued no challenge");
8984
- }
8985
- const session = await generateSessionKey();
8986
- const issuedAt = Math.floor(Date.now() / 1e3);
8987
- const statement = await signLoginStatement({
8988
- node: config.node,
8989
- audience: config.audience,
8990
- challenge,
8991
- sessionKey: session.publicKey,
8992
- issuedAt,
8993
- expiresAt: issuedAt + (config.ttlSeconds ?? DEFAULT_TTL_SECONDS2),
8994
- deviceSecret: config.deviceSecret
8995
- });
8996
- const tokenBody = await request("/auth/token", {
8997
- method: "POST",
8998
- headers: { "Content-Type": "application/json", Accept: "application/json" },
8999
- body: JSON.stringify({
9000
- auth_method: "account_proof",
9001
- public_key: session.publicKey,
9002
- client_name: config.clientName ?? audienceLabel(config.audience),
9003
- timestamp: issuedAt,
9004
- provider_data: {
9005
- challenge,
9006
- login_statement: statement,
9007
- account_proof: config.accountProof
9008
- }
9009
- })
9010
- });
9011
- const accessToken = tokenBody.access_token ?? tokenBody.data?.access_token;
9012
- const refreshToken = tokenBody.refresh_token ?? tokenBody.data?.refresh_token;
9013
- if (!accessToken) {
9014
- throw new Error("the node minted no session");
9015
- }
9016
- return {
9017
- accessToken,
9018
- refreshToken: refreshToken ?? "",
9019
- sessionKey: session.publicKey,
9020
- sessionSecret: session.secret
9021
- };
9022
- }
9023
-
9024
10391
  // src/nodeDiscovery.ts
9025
10392
  var DEFAULT_LOCAL_NODE_PORTS = [2428, 2429, 2528, 2529];
9026
10393
  var LOCAL_HOST = "localhost";
@@ -9073,4 +10440,100 @@ async function discoverLocalNodes(options = {}) {
9073
10440
  );
9074
10441
  return results.filter((url) => url !== null);
9075
10442
  }
10443
+
10444
+ // src/relay-attestation/attest-node-key.ts
10445
+ var KEY_BINDING_DOMAIN = new TextEncoder().encode("calimero.tee-attest.key-binding.v1");
10446
+ var MOCK_QUOTE_HEADER = new TextEncoder().encode("MOCK_TDX_QUOTE_V1");
10447
+ var TDX_REPORT_DATA_OFFSET = 48 + 520;
10448
+ function concat2(...parts) {
10449
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
10450
+ let at = 0;
10451
+ for (const p of parts) {
10452
+ out.set(p, at);
10453
+ at += p.length;
10454
+ }
10455
+ return out;
10456
+ }
10457
+ function equal2(a, b) {
10458
+ if (a.length !== b.length) return false;
10459
+ let diff = 0;
10460
+ for (let i = 0; i < a.length; i += 1) diff |= a[i] ^ b[i];
10461
+ return diff === 0;
10462
+ }
10463
+ function fromBase643(b64) {
10464
+ const bin = atob(b64);
10465
+ const out = new Uint8Array(bin.length);
10466
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
10467
+ return out;
10468
+ }
10469
+ function fromHex32(value, label2) {
10470
+ if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
10471
+ throw new Error(`${label2} must be 64 hex characters`);
10472
+ }
10473
+ const out = new Uint8Array(32);
10474
+ for (let i = 0; i < 32; i += 1) out[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16);
10475
+ return out;
10476
+ }
10477
+ async function attestKeyBinding(nodeKey) {
10478
+ const digest = await crypto.subtle.digest(
10479
+ "SHA-256",
10480
+ concat2(KEY_BINDING_DOMAIN, new Uint8Array(32), nodeKey)
10481
+ );
10482
+ return new Uint8Array(digest);
10483
+ }
10484
+ function reportDataOf(quote) {
10485
+ const mock = quote.length >= MOCK_QUOTE_HEADER.length && equal2(quote.subarray(0, MOCK_QUOTE_HEADER.length), MOCK_QUOTE_HEADER);
10486
+ const at = mock ? MOCK_QUOTE_HEADER.length : TDX_REPORT_DATA_OFFSET;
10487
+ if (quote.length < at + 64) {
10488
+ throw new Error(`the quote is too short to carry report data (${quote.length} bytes)`);
10489
+ }
10490
+ return { reportData: quote.slice(at, at + 64), mock };
10491
+ }
10492
+ async function attestRelayNodeKey(opts) {
10493
+ const nonce2 = crypto.getRandomValues(new Uint8Array(32));
10494
+ const url = `${opts.relayUrl.replace(/\/+$/, "")}/admin-api/tee/attest`;
10495
+ const body = JSON.stringify({
10496
+ nonce: hex(nonce2),
10497
+ bindNodeKey: true,
10498
+ ...opts.verify?.includeCollateral ? { includeCollateral: true } : {}
10499
+ });
10500
+ const init = { method: "POST", headers: { "Content-Type": "application/json" }, body };
10501
+ const response = opts.fetch ? await opts.fetch(url, init) : await globalThis.fetch(url, init);
10502
+ if (!response.ok) {
10503
+ const text = await response.text().catch(() => "");
10504
+ throw new Error(`the relay would not attest (HTTP ${response.status})${text ? `: ${text}` : ""}`);
10505
+ }
10506
+ const parsed = await response.json();
10507
+ const data = parsed.data ?? parsed;
10508
+ if (typeof data.quoteB64 !== "string") throw new Error("the relay returned no quote");
10509
+ const nodeKey = fromHex32(data.boundPublicKey, "boundPublicKey");
10510
+ const quote = fromBase643(data.quoteB64);
10511
+ const { reportData, mock } = reportDataOf(quote);
10512
+ if (mock) {
10513
+ if (!opts.allowMock) {
10514
+ throw new Error("the relay answered with a MOCK quote, which proves nothing about the hardware");
10515
+ }
10516
+ } else if (opts.verify) {
10517
+ const accepted = await opts.verify({
10518
+ quoteB64: data.quoteB64,
10519
+ nonce: hex(nonce2),
10520
+ reportDataSuffix: hex(await attestKeyBinding(nodeKey)),
10521
+ ...data.collateral ? { collateral: data.collateral } : {}
10522
+ });
10523
+ if (!accepted) {
10524
+ throw new Error("the relay's quote did not verify: not a trusted image, or not bound to this request");
10525
+ }
10526
+ } else if (opts.verifyQuote) {
10527
+ await opts.verifyQuote(quote);
10528
+ } else {
10529
+ throw new Error("a real TDX quote needs a verify (or verifyQuote) to check its signature and measurements");
10530
+ }
10531
+ if (!equal2(reportData.subarray(0, 32), nonce2)) {
10532
+ throw new Error("the quote does not carry our nonce: it was not made for this request");
10533
+ }
10534
+ if (!equal2(reportData.subarray(32, 64), await attestKeyBinding(nodeKey))) {
10535
+ throw new Error("the quote does not bind the key the relay named: refusing it");
10536
+ }
10537
+ return { nodeKey: hex(nodeKey), mock };
10538
+ }
9076
10539
  //# sourceMappingURL=index.cjs.map