@calimero-network/mero-js 26.11.0 → 27.0.1

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 (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12772
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12772
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -688
  62. package/dist/admin-api/admin-client.js +0 -1230
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2046
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,518 +0,0 @@
1
- /**
2
- * Write to a context through a relay, holding nothing but a signing key.
3
- *
4
- * # What this replaces
5
- *
6
- * `sdk.rpc.execute` asks a node to run a method *as itself*: it needs a node,
7
- * that node needs to be a member, and the caller needs a credential on it. For
8
- * a browser tab, a phone, or an agent, none of those three is available — the
9
- * runtime cannot compile WASM against materialized state, never received the
10
- * scope key that seals the deltas, and has no account on anybody's node.
11
- *
12
- * This client is the other path. The caller signs a warrant locally, the relay
13
- * runs the method as the *author's* principal, and the resulting delta is
14
- * attributed to the author's account and device — their replica slot, their
15
- * membership, their name in the app. The relay's key signs the envelope and
16
- * nothing else.
17
- *
18
- * # The three preconditions, and why they are checked in this order
19
- *
20
- * `describe()` answers the first two in one call, before anything is signed:
21
- * the relay's executor account and signing key and the release the context's
22
- * group names (which the warrant must name), and whether it holds
23
- * `CAN_AUTHOR_ON_BEHALF` on the owning group. The third,
24
- * that the author's account is a member, only the group knows, and it surfaces
25
- * as a refusal.
26
- *
27
- * Checking before signing is not politeness. A warrant consumes a nonce from a
28
- * monotonic per-device sequence, and one minted against the wrong executor is
29
- * unspendable: the number is gone and the write never happened.
30
- */
31
- import { releaseVersionBytes, signWarrant } from '../warrant/warrant.js';
32
- import { resolveSigner } from '../signer/signer.js';
33
- import { signCreationWarrant } from '../warrant/creation-warrant.js';
34
- import { signGovernanceWarrant } from '../warrant/governance-warrant.js';
35
- import { defaultCapabilitiesSetOp, targetApplicationSetOp, foundedNamespaceId, namespaceCreatedOp, } from '../warrant/governance-op.js';
36
- import { HTTPError } from '../http-client/web-client.js';
37
- import { pathSegment } from '../http-client/path-segment.js';
38
- import { fromHex, hex } from '../crypto/internal.js';
39
- /** How long a freshly minted warrant stays presentable, in seconds. */
40
- const DEFAULT_TTL_SECONDS = 300;
41
- /**
42
- * Thrown when a relay refuses an intent, carrying which precondition failed.
43
- *
44
- * The distinction is the whole reason this type exists. A `403` from this
45
- * endpoint means one of several unrelated things — no grant on the relay, the
46
- * relay is a TEE replica (`ReadOnlyTee`) or a `ReadOnly` member (neither ever
47
- * relays), the author is
48
- * not a member or is read-only in the context, or the nonce was already spent
49
- * — and they send a caller somewhere completely different: ask an admin, pick
50
- * a relay the namespace admits in `relay` mode, ask for an invitation, or just
51
- * retry. Collapsing them into "forbidden" makes a retryable replay look
52
- * like a permissions bug.
53
- */
54
- export class IntentRefusedError extends Error {
55
- constructor(
56
- /** The relay's own explanation, verbatim. */
57
- reason,
58
- /** `true` when re-presenting the same intent under a *fresh* warrant may work. */
59
- retryable, status) {
60
- super(`relay refused the intent (HTTP ${status}): ${reason}`);
61
- this.reason = reason;
62
- this.retryable = retryable;
63
- this.status = status;
64
- this.name = 'IntentRefusedError';
65
- }
66
- }
67
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
68
- /**
69
- * `Retry-After`, as milliseconds — seconds or an HTTP date, per RFC 9110.
70
- *
71
- * Returns `null` when the header is absent or unusable, so the caller falls back
72
- * to its own backoff rather than treating a malformed header as "retry now".
73
- */
74
- function retryAfterMs(headers) {
75
- const raw = headers?.get?.('Retry-After');
76
- if (!raw)
77
- return null;
78
- const seconds = Number.parseInt(raw, 10);
79
- if (Number.isFinite(seconds) && seconds >= 0)
80
- return seconds * 1000;
81
- const at = Date.parse(raw);
82
- if (Number.isFinite(at))
83
- return Math.max(0, at - Date.now());
84
- return null;
85
- }
86
- export class RelayClient {
87
- constructor(config) {
88
- this.config = config;
89
- this.baseUrl = config.relayUrl.replace(/\/+$/, '');
90
- const injected = config.fetch;
91
- this.fetchImpl = injected
92
- ? (input, init) => injected(input, init)
93
- : (input, init) => globalThis.fetch(input, init);
94
- this.timeoutMs = config.timeoutMs ?? 10000;
95
- this.rateLimitRetries = config.rateLimitRetries ?? 3;
96
- }
97
- /**
98
- * The relay's origin, trailing slash stripped.
99
- *
100
- * Exposed because a relay **is a node**: the same origin that answers the
101
- * intents routes also answers `/auth/challenge`, `/auth/token`, `/sse` and
102
- * `/ws`. Anything building an observing session over this relay needs the
103
- * URL, and asking the caller to pass it a second time is how the two drift.
104
- */
105
- get relayUrl() {
106
- return this.baseUrl;
107
- }
108
- /** The author's account, hex — whose consent every warrant here carries. */
109
- get authorAccount() {
110
- return this.config.authorAccount;
111
- }
112
- /**
113
- * The author's `AccountProof<DeviceCert>`, hex.
114
- *
115
- * The same credential `login()` wants, which is why it is readable: a caller
116
- * that already handed it to this client should not have to hand an identical
117
- * copy to the login as well, where the two could disagree.
118
- */
119
- get authorProof() {
120
- return this.config.authorProof;
121
- }
122
- /**
123
- * The author device's signer.
124
- *
125
- * A {@link Signer} and never the secret: this returns something that can
126
- * *sign*, so no caller of a `RelayClient` gains the ability to read a key
127
- * that was passed in as `deviceSecret`. That asymmetry is the point — the
128
- * login needs a signature, not key material.
129
- */
130
- async authorSigner() {
131
- return resolveSigner(this.config.deviceSecret, this.config.signer, 'deviceSecret');
132
- }
133
- /**
134
- * Ask the relay what it can do in `contextId`.
135
- *
136
- * Cheap and unauthenticated on a relay that serves delegated execution
137
- * publicly, which is what makes it usable as a precondition check rather than
138
- * as diagnostics after a failure.
139
- */
140
- async describe(contextId) {
141
- const body = await this.json('GET', `/admin-api/contexts/${pathSegment(contextId)}/intents`);
142
- return body.data;
143
- }
144
- /**
145
- * Mint a warrant for `method(args)` and present it to the relay.
146
- *
147
- * One intent, once: the warrant authorizes exactly this method and these
148
- * arguments in exactly this context, and its nonce is spent by the network on
149
- * apply. Nothing accumulates and nothing is reusable.
150
- */
151
- async execute(contextId, method, argsJson = {}) {
152
- // Asked on every write: the release moves with each upgrade of the group, and a
153
- // warrant pinning the one before is refused. All of it is checked before the nonce.
154
- const described = await this.describe(contextId);
155
- const executor = this.checkedExecutor(described);
156
- const releaseBytecodeId = hex(fromHex(described.releaseBytecodeId ?? '', "the relay's releaseBytecodeId", 32));
157
- const releaseVersion = described.releaseVersion ?? '';
158
- releaseVersionBytes(releaseVersion);
159
- // Resolved before the nonce, so a misconfigured key burns none.
160
- const signer = await this.authorSigner();
161
- const nonce = await this.config.nonces.next();
162
- const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
163
- const warrant = await signWarrant({
164
- context: contextId,
165
- authorAccount: this.config.authorAccount,
166
- executor: executor.executorAccount,
167
- executorKey: executor.executorKey,
168
- releaseBytecodeId,
169
- releaseVersion,
170
- method,
171
- argsJson,
172
- nonce,
173
- notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
174
- signer,
175
- });
176
- const body = await this.json('POST', `/admin-api/contexts/${pathSegment(contextId)}/intents`, { method, argsJson, warrant, authorProof: this.config.authorProof });
177
- return { rootHash: body.data.rootHash, returns: body.data.returns ?? null };
178
- }
179
- /**
180
- * Publish one presence statement through the relay (`presence-intents`).
181
- *
182
- * Not retried on 429: presence resends every 2.5 s anyway, and a retry would
183
- * only queue behind the next resend. A refusal throws with its `status`.
184
- */
185
- async presenceIntent(contextId, body) {
186
- await this.jsonOnce('POST', `/admin-api/contexts/${pathSegment(contextId)}/presence-intents`, body);
187
- }
188
- /**
189
- * Ask the relay what it can do about creating contexts in `groupId`.
190
- *
191
- * Pass `author` (an account, hex) to also learn whether that account may
192
- * create here — `authorMayCreate` is present only then.
193
- */
194
- async describeCreation(groupId, opts = {}) {
195
- const query = opts.author ? `?author=${encodeURIComponent(opts.author)}` : '';
196
- const body = await this.json('GET', `/admin-api/groups/${pathSegment(groupId)}/context-intents${query}`);
197
- return body.data;
198
- }
199
- /**
200
- * Create a context in a group through the relay, as the author.
201
- *
202
- * Always asks {@link describeCreation} first — with the author, so both
203
- * standing checks happen before anything is signed. A relay without standing
204
- * in the group, or an author without `CAN_CREATE_CONTEXT`, is refused here as
205
- * an {@link IntentRefusedError} *before* a nonce is taken: the nonce source
206
- * is monotonic, and a warrant nobody will spend burns one for nothing.
207
- *
208
- * The creation warrant's nonce is spent in the new context's per-device
209
- * ledger — the same one later `execute` calls in that context draw from — so
210
- * it comes from the configured {@link NonceSource} like any other warrant.
211
- *
212
- * Sealing: nothing here is plaintext-specific. The request goes through the
213
- * injected `fetch`, so a `createAttestedSealedFetch` seals it exactly as it
214
- * seals `execute`. Whether the relay *accepts* it sealed is the node's call:
215
- * a proxy-auth relay only admits sealed requests for routes `merod` serves
216
- * without a credential, so `/admin-api/groups/:id/context-intents` must be on
217
- * that list on the core side or the request comes back
218
- * `403 sealed_route_unguarded`.
219
- */
220
- async createContext(input) {
221
- const described = await this.describeCreation(input.groupId, {
222
- author: this.config.authorAccount,
223
- });
224
- if (!described.canCreateOnBehalf) {
225
- throw new IntentRefusedError(`the relay (${described.executorAccount}) has no standing to act for members of group ` +
226
- `${described.groupId}; an admin must admit it as a relay or grant it CAN_AUTHOR_ON_BEHALF ` +
227
- '(checked before signing — no nonce was spent)', false, 403);
228
- }
229
- if (described.authorMayCreate === false) {
230
- throw new IntentRefusedError(`the author (${this.config.authorAccount}) may not create contexts in group ` +
231
- `${described.groupId}; it needs CAN_CREATE_CONTEXT or admin ` +
232
- '(checked before signing — no nonce was spent)', false, 403);
233
- }
234
- const executor = this.checkedExecutor(described);
235
- const initArgs = input.initArgs ?? {};
236
- // Resolved before the nonce, so a misconfigured key burns none.
237
- const signer = await this.authorSigner();
238
- const nonce = await this.config.nonces.next();
239
- const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
240
- const { warrant } = await signCreationWarrant({
241
- group: input.groupId,
242
- seed: input.seed,
243
- authorAccount: this.config.authorAccount,
244
- executor: executor.executorAccount,
245
- executorKey: executor.executorKey,
246
- applicationId: input.applicationId,
247
- serviceName: input.serviceName,
248
- name: input.name,
249
- initArgs,
250
- nonce,
251
- notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
252
- signer,
253
- });
254
- const body = await this.json('POST', `/admin-api/groups/${pathSegment(input.groupId)}/context-intents`, { warrant, authorProof: this.config.authorProof, initArgs });
255
- return {
256
- contextId: body.data.contextId,
257
- groupId: body.data.groupId,
258
- memberPublicKey: body.data.memberPublicKey,
259
- };
260
- }
261
- /**
262
- * Ask the relay whether it may publish governance ops for members of
263
- * `groupId`, and which account and key a warrant must name.
264
- */
265
- async describeGovernance(groupId) {
266
- const body = await this.json('GET', `/admin-api/groups/${pathSegment(groupId)}/governance-intents`);
267
- return body.data;
268
- }
269
- /**
270
- * Publish one governance op through the relay, as the author.
271
- *
272
- * The warrant commits to the op's exact bytes and is scoped to `groupId`, so
273
- * the relay can neither change what the op does nor spend it elsewhere. Every
274
- * peer applies the op as the author, so the author's own rights decide
275
- * (`MANAGE_MEMBERS` to add a member, `CAN_CREATE_SUBGROUP` to create a
276
- * subgroup, and so on); a lack of them comes back as an
277
- * {@link IntentRefusedError} from the relay.
278
- *
279
- * Always asks {@link describeGovernance} first and refuses *before* taking a
280
- * nonce when the relay has no standing, for the reason `createContext` does.
281
- * The nonce comes from the configured {@link NonceSource}; it is spent in a
282
- * per-group ledger, and a monotonic source is correct for it as for every
283
- * other warrant.
284
- */
285
- async govern(input) {
286
- const described = await this.describeGovernance(input.groupId);
287
- if (!described.canActOnBehalf) {
288
- throw new IntentRefusedError(`the relay (${described.executorAccount}) has no standing to act for members of group ` +
289
- `${described.groupId}; an admin must admit it as a relay or grant it CAN_AUTHOR_ON_BEHALF ` +
290
- '(checked before signing, no nonce was spent)', false, 403);
291
- }
292
- const executor = this.checkedExecutor(described);
293
- // Resolved before the nonce, so a misconfigured key burns none.
294
- const signer = await this.authorSigner();
295
- const nonce = await this.config.nonces.next();
296
- const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
297
- const warrant = await signGovernanceWarrant({
298
- scope: input.groupId,
299
- op: input.op,
300
- authorAccount: this.config.authorAccount,
301
- executor: executor.executorAccount,
302
- executorKey: executor.executorKey,
303
- nonce,
304
- notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
305
- signer,
306
- });
307
- const data = await this.postGovernance(input.groupId, input.op, warrant);
308
- return { groupId: data.groupId };
309
- }
310
- /**
311
- * Found a namespace through the relay, with the author as its founder, owner
312
- * and admin. For an account with no node, this is how it gets a namespace at
313
- * all.
314
- *
315
- * The author signs the exact genesis (`NamespaceCreatedV2`, carrying the
316
- * author's own credential and the salt) under a root-plane governance
317
- * warrant scoped to the new namespace's id, which is derived from the
318
- * author's account and the salt, so the relay can neither choose another id
319
- * nor found it for anyone else. The relay takes an identity in the new
320
- * namespace, mints its key, publishes the genesis, and is seated as the
321
- * founding relay (a `Member` holding `CAN_AUTHOR_ON_BEHALF`), so later
322
- * `govern`, `createContext` and `execute` calls in the namespace work
323
- * through it straight away. A TEE relay then admits itself as the
324
- * namespace's first TEE; `teeEnabled` reports whether it did.
325
- *
326
- * The executor cannot be learned from the relay first: the namespace does not
327
- * exist, so `describeGovernance` has nothing to answer about. It comes from
328
- * `input.executor` only, and without it this throws before taking a nonce.
329
- * The nonce is spent in the new namespace's ledger.
330
- *
331
- * With `defaultCapabilities`, the founder then sets the namespace's default
332
- * capability mask through the same relay (a second warrant, a second nonce)
333
- * and `defaultCapabilitiesSet` reports whether it landed. A failure there
334
- * does not throw, since the namespace is already founded.
335
- *
336
- * Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
337
- * that names an existing namespace is a `409`, an `HTTPError`.
338
- */
339
- async foundNamespace(input) {
340
- if (!input?.executor) {
341
- throw new Error("foundNamespace needs the relay's account and signing key: pass executor " +
342
- '(checked before signing, no nonce was spent)');
343
- }
344
- const executor = this.checkedExecutor(input.executor);
345
- const salt = input.salt
346
- ? hex(fromHex(input.salt, 'salt', 32))
347
- : hex(crypto.getRandomValues(new Uint8Array(32)));
348
- const namespaceId = await foundedNamespaceId(this.config.authorAccount, salt);
349
- const op = namespaceCreatedOp({
350
- founder: this.config.authorAccount,
351
- credential: this.config.authorProof,
352
- salt,
353
- });
354
- // Encoded (and so validated) before anything is signed: a mask the relay
355
- // would refuse must not cost the founding a nonce.
356
- const capabilitiesOp = input.defaultCapabilities === undefined
357
- ? undefined
358
- : defaultCapabilitiesSetOp(input.defaultCapabilities);
359
- const applicationOp = input.application ? targetApplicationSetOp(input.application) : undefined;
360
- // Resolved before the nonce, so a misconfigured key burns none.
361
- const signer = await this.authorSigner();
362
- const nonce = await this.config.nonces.next();
363
- const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
364
- const warrant = await signGovernanceWarrant({
365
- scope: namespaceId,
366
- op,
367
- authorAccount: this.config.authorAccount,
368
- executor: executor.executorAccount,
369
- executorKey: executor.executorKey,
370
- nonce,
371
- notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
372
- signer,
373
- });
374
- const data = await this.postGovernance(namespaceId, op, warrant);
375
- const founded = {
376
- namespaceId: data.groupId,
377
- salt,
378
- teeEnabled: data.teeEnabled ?? false,
379
- ...(data.teeError ? { teeError: data.teeError } : {}),
380
- };
381
- // Follow-up ops, each under its own warrant, on the namespace root: the
382
- // founder is its admin and the relay was seated to act for members, so they
383
- // go through the ordinary `govern` path. The namespace exists whatever
384
- // happens here, so a failure is reported rather than thrown. The
385
- // application first: without it the namespace can hold no context at all.
386
- let result = founded;
387
- const message = (err) => (err instanceof Error ? err.message : String(err));
388
- if (applicationOp) {
389
- try {
390
- await this.govern({ groupId: founded.namespaceId, op: applicationOp });
391
- result = { ...result, applicationSet: true };
392
- }
393
- catch (err) {
394
- result = { ...result, applicationSet: false, applicationError: message(err) };
395
- }
396
- }
397
- if (capabilitiesOp) {
398
- try {
399
- await this.govern({ groupId: founded.namespaceId, op: capabilitiesOp });
400
- result = { ...result, defaultCapabilitiesSet: true };
401
- }
402
- catch (err) {
403
- result = { ...result, defaultCapabilitiesSet: false, defaultCapabilitiesError: message(err) };
404
- }
405
- }
406
- return result;
407
- }
408
- async postGovernance(groupId, op, warrant) {
409
- const body = await this.json('POST', `/admin-api/groups/${pathSegment(groupId)}/governance-intents`, {
410
- warrant,
411
- authorProof: this.config.authorProof,
412
- op: hex(op.bytes),
413
- });
414
- return body.data;
415
- }
416
- /**
417
- * The relay's answer, checked against a configured account.
418
- * Decoded here so a relay naming no key is refused before a nonce is taken.
419
- */
420
- checkedExecutor(answered) {
421
- const executor = {
422
- executorAccount: hex(fromHex(answered.executorAccount ?? '', "the relay's executorAccount", 32)),
423
- executorKey: hex(fromHex(answered.executorKey ?? '', "the relay's executorKey", 32)),
424
- };
425
- const configured = this.config.executorAccount;
426
- if (configured && configured.toLowerCase() !== executor.executorAccount) {
427
- throw new Error(`configured executorAccount ${configured} is not the relay's account ` +
428
- `${executor.executorAccount}; a warrant naming it would be unspendable`);
429
- }
430
- return executor;
431
- }
432
- /**
433
- * One request, waiting out a rate limit rather than failing the caller.
434
- *
435
- * The relay's ingress limits the delegated-execution routes to 20 requests a
436
- * second per source IP, burst 40 — tighter than everything else it serves,
437
- * because those two routes are the ones it answers to anonymous callers.
438
- *
439
- * Retrying a 429 is safe here in a way retrying most refusals is not: the
440
- * limiter sits *first* in the middleware chain, so a throttled request never
441
- * reached the node and its warrant nonce was never spent. The same bytes can
442
- * go again. Contrast the spent-nonce 403 below, which needs a fresh warrant.
443
- *
444
- * `Retry-After` is honoured when the ingress sends one; otherwise the wait
445
- * doubles from a little over the limiter's own window, which is the shortest
446
- * delay that can actually clear.
447
- */
448
- async json(method, path, body) {
449
- let wait = 250;
450
- for (let attempt = 0;; attempt += 1) {
451
- try {
452
- return await this.jsonOnce(method, path, body);
453
- }
454
- catch (err) {
455
- const limited = err instanceof HTTPError && err.status === 429;
456
- if (!limited || attempt >= this.rateLimitRetries)
457
- throw err;
458
- const after = retryAfterMs(err.headers);
459
- await sleep(after ?? wait);
460
- wait *= 2;
461
- }
462
- }
463
- }
464
- async jsonOnce(method, path, body) {
465
- const headers = { Accept: 'application/json' };
466
- if (body !== undefined)
467
- headers['Content-Type'] = 'application/json';
468
- const controller = new AbortController();
469
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
470
- let response;
471
- try {
472
- response = await this.fetchImpl(`${this.baseUrl}${path}`, {
473
- method,
474
- headers,
475
- body: body === undefined ? undefined : JSON.stringify(body),
476
- signal: controller.signal,
477
- });
478
- }
479
- catch (err) {
480
- throw new HTTPError(0, err instanceof Error ? err.name : 'NetworkError', `${this.baseUrl}${path}`, new Headers(), err instanceof Error ? err.message : String(err));
481
- }
482
- finally {
483
- clearTimeout(timer);
484
- }
485
- if (!response.ok) {
486
- const text = await response.text().catch(() => '');
487
- // 400 and 403 are both caller preconditions and core separates them
488
- // deliberately: 400 means re-sending the same bytes cannot help, 403
489
- // means the bytes are genuine but authority is missing. Only the spent
490
- // nonce is worth retrying, and only under a fresh warrant.
491
- if (response.status === 400 || response.status === 403) {
492
- const reason = extractReason(text);
493
- throw new IntentRefusedError(reason, /nonce/i.test(reason), response.status);
494
- }
495
- throw new HTTPError(response.status, response.statusText, `${this.baseUrl}${path}`, response.headers, text);
496
- }
497
- // `presence-intents` answers an accepted update with no body.
498
- if (response.status === 204)
499
- return undefined;
500
- return (await response.json());
501
- }
502
- }
503
- /** Pull the relay's explanation out of its error body, or fall back to the raw text. */
504
- function extractReason(bodyText) {
505
- if (!bodyText)
506
- return 'no reason given';
507
- try {
508
- const parsed = JSON.parse(bodyText);
509
- for (const candidate of [parsed.error, parsed.message]) {
510
- if (typeof candidate === 'string' && candidate)
511
- return candidate;
512
- }
513
- }
514
- catch {
515
- // Not JSON — the text itself is the best available explanation.
516
- }
517
- return bodyText;
518
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Learn a relay's node signing key from its TEE attestation.
3
- *
4
- * `login()` binds a session to the node key the client names, and that key must
5
- * never be taken from the node being logged in to on its word — the answering
6
- * party would choose what the device signs about. A TEE quote is different: the
7
- * node cannot put a key into it that the TEE did not bind. With `bindNodeKey`,
8
- * core commits the node's signing key into the quote's report data:
9
- *
10
- * report_data = nonce (32) ‖ SHA-256("calimero.tee-attest.key-binding.v1" ‖ 0^32 ‖ key)
11
- *
12
- * (the 32 zero bytes stand for "no application", core's `attest_key_binding`).
13
- * So the client sends a fresh nonce, reads the report data out of the RAW quote
14
- * bytes — never the server's parsed rendering of them — and accepts the returned
15
- * key only if both halves match.
16
- *
17
- * That proves the key came from whatever produced the quote. Whether that is a
18
- * genuine TDX guest running a trusted image is the quote's signature and
19
- * measurements, which this module does not judge: a real quote needs a
20
- * `verifyQuote` supplied by the caller, and a mock quote (a dev-only format with
21
- * no signature at all) is refused unless `allowMock` is set.
22
- */
23
- import type { VerifyTransportQuote } from '../sealed/sealed.js';
24
- export interface AttestedNodeKey {
25
- /** The relay's node signing key, hex (32 bytes), as the quote binds it. */
26
- readonly nodeKey: string;
27
- /** True when the quote was a mock — no TEE vouched for anything. */
28
- readonly mock: boolean;
29
- }
30
- export interface AttestRelayNodeKeyOptions {
31
- readonly relayUrl: string;
32
- /**
33
- * Accept a mock quote. DEV/TEST ONLY: a mock quote carries no signature, so it
34
- * proves nothing about the hardware — only that the node bound this key into
35
- * something it produced for our nonce.
36
- */
37
- readonly allowMock?: boolean;
38
- /**
39
- * Verify a real quote's signature chain and measurements; throw to refuse.
40
- * Required for a non-mock quote: without it the binding would be checked
41
- * against bytes nothing has authenticated.
42
- */
43
- readonly verifyQuote?: (quote: Uint8Array) => Promise<void>;
44
- /**
45
- * Verify a real quote the way a sealed transport does: a
46
- * {@link VerifyTransportQuote}, such as `createSignedReleaseVerifier` (the
47
- * relay's signed mero-tee release, Intel's chain, every register of its
48
- * image). Called with our nonce and the node-key binding as the expected
49
- * report data, and with the relay's collateral when it asks for it. Takes
50
- * precedence over `verifyQuote`.
51
- */
52
- readonly verify?: VerifyTransportQuote;
53
- readonly fetch?: typeof fetch;
54
- }
55
- /** Core's `attest_key_binding(None, key)`. */
56
- export declare function attestKeyBinding(nodeKey: Uint8Array): Promise<Uint8Array>;
57
- /** The 64 report-data bytes of a quote, read from the quote itself. */
58
- export declare function reportDataOf(quote: Uint8Array): {
59
- reportData: Uint8Array;
60
- mock: boolean;
61
- };
62
- /**
63
- * Ask a relay for a quote binding its node key to a fresh nonce, check the
64
- * binding, and return the key.
65
- */
66
- export declare function attestRelayNodeKey(opts: AttestRelayNodeKeyOptions): Promise<AttestedNodeKey>;