@calimero-network/mero-js 26.10.0 → 27.0.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 (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 -12767
  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 -12767
  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 -686
  62. package/dist/admin-api/admin-client.js +0 -1225
  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 -2041
  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
@@ -0,0 +1,646 @@
1
+ /**
2
+ * Client for Calimero 1.0, the confidential cloud: one app = one replicated log on
3
+ * attested nodes; this client verifies the node, seals every request to it,
4
+ * signs people in with passkeys and machines with API keys.
5
+ *
6
+ * ```ts
7
+ * const cloud = new CalimeroClient({ baseUrl, policy: { release: { minReleaseVersion: '2.3.109' } } });
8
+ * await cloud.signIn(browserPasskey());
9
+ * const { result } = await cloud.call(appId, 'set', { key: 'a', value: '1' });
10
+ * ```
11
+ */
12
+ import { SealedTransportError, createSealedFetch } from '../sealed/sealed.js';
13
+ import { fetchAttestedKey } from './attestation.js';
14
+ import { CalimeroError, ConfirmCancelled, ConfirmRequired, UnavailableError, errorFrom } from './errors.js';
15
+ import { b64url, fromB64url } from './passkey.js';
16
+ export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, NotFound, TrapError, UnavailableError, errorFrom, } from './errors.js';
17
+ /** One URL path segment, so an id cannot reach another route. */
18
+ function seg(value) {
19
+ return encodeURIComponent(value);
20
+ }
21
+ /**
22
+ * The node refuses an envelope past its 64 MiB sealed cap with `413` in the
23
+ * clear, before opening it, so the refusal arrives as the transport's. To the
24
+ * caller it is the API's `TOO_LARGE`, as the Rust client reports it.
25
+ */
26
+ function sealedError(error) {
27
+ if (error instanceof SealedTransportError && error.status === 413) {
28
+ return errorFrom(413, {
29
+ error: 'TOO_LARGE',
30
+ message: error.message.replace(/^Sealed request refused: /, ''),
31
+ });
32
+ }
33
+ return error;
34
+ }
35
+ /** Read a refused response's body and map it ({@link errorFrom}). */
36
+ async function refusal(response) {
37
+ const text = await response.text().catch(() => '');
38
+ let body = {};
39
+ try {
40
+ body = text ? JSON.parse(text) : {};
41
+ }
42
+ catch {
43
+ body = { message: text };
44
+ }
45
+ return errorFrom(response.status, body, response.headers.get('retry-after'));
46
+ }
47
+ /** Server-sent event frames from a response body: `{ id, kind, data }` each, `data` parsed as JSON when it is. */
48
+ async function* sseFrames(body) {
49
+ const reader = body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let buffer = '';
52
+ for (;;) {
53
+ const { value, done } = await reader.read();
54
+ if (done)
55
+ return;
56
+ buffer += decoder.decode(value, { stream: true });
57
+ let cut;
58
+ while ((cut = buffer.indexOf('\n\n')) >= 0) {
59
+ const frame = buffer.slice(0, cut);
60
+ buffer = buffer.slice(cut + 2);
61
+ let id = '';
62
+ let kind = '';
63
+ let data = '';
64
+ for (const line of frame.split('\n')) {
65
+ if (line.startsWith('id:'))
66
+ id = line.slice(3).trim();
67
+ else if (line.startsWith('event:'))
68
+ kind = line.slice(6).trim();
69
+ else if (line.startsWith('data:'))
70
+ data += line.slice(5).trim();
71
+ }
72
+ if (!kind && !data)
73
+ continue;
74
+ let parsed = data;
75
+ try {
76
+ parsed = JSON.parse(data);
77
+ }
78
+ catch {
79
+ /* plain text */
80
+ }
81
+ yield { id, kind, data: parsed };
82
+ }
83
+ }
84
+ }
85
+ /** Bytes as lower-case hex. */
86
+ function toHex(bytes) {
87
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
88
+ }
89
+ /**
90
+ * A key for a call the caller gave none, made once so every retry shares it:
91
+ * `node:c-` and 16 random bytes in base64url without padding, as the Rust
92
+ * client mints it. It only has to outlive this client's own retries, so it
93
+ * takes the node's short-lived class: a key starting `node:` is kept 10
94
+ * minutes, a caller's own key 24 hours.
95
+ * `getRandomValues`, not `randomUUID`, which a page outside a secure context lacks.
96
+ */
97
+ function newIdempotencyKey() {
98
+ return `node:c-${b64url(crypto.getRandomValues(new Uint8Array(16)))}`;
99
+ }
100
+ export const DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2000 };
101
+ /** The backoff before retry `retry` (0 for the first): `baseDelayMs` doubling, at most `maxDelayMs`. */
102
+ export function retryDelayMs(retry, options) {
103
+ return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
104
+ }
105
+ export class CalimeroClient {
106
+ constructor(options) {
107
+ /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
108
+ this.networkFailures = new WeakSet();
109
+ this.base = options.baseUrl.replace(/\/+$/, '');
110
+ this.retry = { ...DEFAULT_RETRY, ...options.retry };
111
+ const given = options.fetch ?? ((input, init) => fetch(input, init));
112
+ // `fetch` rejects with a `TypeError` when it gets no answer: the connection
113
+ // was refused, reset or dropped before the response. The sealed fetch passes
114
+ // that rejection on unchanged, so it is recognised here, at its source, and
115
+ // not by its type alone (a bad URL is a `TypeError` too, and is not retried).
116
+ const raw = async (input, init) => {
117
+ try {
118
+ return await given(input, init);
119
+ }
120
+ catch (error) {
121
+ if (error instanceof TypeError)
122
+ this.networkFailures.add(error);
123
+ throw error;
124
+ }
125
+ };
126
+ this.raw = raw;
127
+ this.sealed = createSealedFetch({
128
+ baseUrl: this.base,
129
+ transportPublicKey: () => fetchAttestedKey(this.base, options.policy, raw),
130
+ fetch: raw,
131
+ });
132
+ }
133
+ /** Authenticate every request with an API key. */
134
+ useApiKey(key) {
135
+ this.auth = key;
136
+ this.current = undefined;
137
+ }
138
+ useSession(session) {
139
+ this.auth = session.sessionToken;
140
+ this.current = session;
141
+ }
142
+ get session() {
143
+ return this.current;
144
+ }
145
+ async request(method, path, body, raw, contentType = 'application/wasm') {
146
+ const headers = {};
147
+ if (this.auth)
148
+ headers.authorization = `Bearer ${this.auth}`;
149
+ let payload;
150
+ if (raw) {
151
+ headers['content-type'] = contentType;
152
+ payload = raw;
153
+ }
154
+ else if (body !== undefined) {
155
+ headers['content-type'] = 'application/json';
156
+ payload = JSON.stringify(body);
157
+ }
158
+ try {
159
+ return await this.sealed(`${this.base}/${path}`, { method, headers, body: payload });
160
+ }
161
+ catch (error) {
162
+ throw sealedError(error);
163
+ }
164
+ }
165
+ async json(method, path, body, raw, contentType) {
166
+ const response = await this.request(method, path, body, raw, contentType);
167
+ if (!response.ok)
168
+ throw await refusal(response);
169
+ const text = await response.text();
170
+ return (text ? JSON.parse(text) : {});
171
+ }
172
+ /**
173
+ * {@link json} for a call or a view (D70): while the node answers 503 or
174
+ * cannot be reached, the same request, with the same idempotency key, is sent
175
+ * again after {@link retryDelayMs} (or the pause the node asked for, if
176
+ * longer), at most `retries` times. Every other answer is final. The last
177
+ * failure surfaces as {@link UnavailableError}: the node's 503, or reason
178
+ * `network` for a connection that failed, whose `committed` is `'unknown'`.
179
+ * `retries` 0 sends the request exactly once.
180
+ */
181
+ async retried(method, path, body, retries = this.retry.retries) {
182
+ for (let attempt = 0;; attempt++) {
183
+ let unavailable;
184
+ try {
185
+ return await this.json(method, path, body);
186
+ }
187
+ catch (error) {
188
+ if (error instanceof UnavailableError)
189
+ unavailable = error;
190
+ else if (error instanceof TypeError && this.networkFailures.has(error)) {
191
+ unavailable = new UnavailableError('NETWORK', `the node could not be reached: ${error.message}`, {}, undefined, error);
192
+ }
193
+ else
194
+ throw error;
195
+ }
196
+ if (attempt >= retries)
197
+ throw unavailable;
198
+ const backoff = retryDelayMs(attempt, this.retry);
199
+ await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1000)));
200
+ }
201
+ }
202
+ post(path, body = {}) {
203
+ return this.json('POST', path, body);
204
+ }
205
+ get(path) {
206
+ return this.json('GET', path);
207
+ }
208
+ put(path, body = {}) {
209
+ return this.json('PUT', path, body);
210
+ }
211
+ delete(path, body) {
212
+ return this.json('DELETE', path, body);
213
+ }
214
+ // ---------------------------------------------------------------- identity
215
+ /** Create an account with a new passkey, and sign in. */
216
+ async signUp(passkey, name) {
217
+ const begin = await this.post('auth/passkey/register/begin', { name });
218
+ const registration = await passkey.create(begin.options);
219
+ const session = await this.post('auth/passkey/register/finish', {
220
+ challengeId: begin.challengeId,
221
+ registration,
222
+ });
223
+ this.useSession(session);
224
+ return session;
225
+ }
226
+ /** Sign in with an existing passkey. */
227
+ async signIn(passkey) {
228
+ const begin = await this.post('auth/passkey/login/begin');
229
+ const assertion = await passkey.get(fromB64url(begin.options.challenge), begin.options.rpId);
230
+ const session = await this.post('auth/passkey/login/finish', {
231
+ challengeId: begin.challengeId,
232
+ assertion,
233
+ });
234
+ this.useSession(session);
235
+ return session;
236
+ }
237
+ /** Swap the refresh token for a new session. */
238
+ async refresh() {
239
+ if (!this.current)
240
+ throw new Error('no session to refresh');
241
+ const session = await this.post('auth/refresh', { refreshToken: this.current.refreshToken });
242
+ this.useSession(session);
243
+ return session;
244
+ }
245
+ async signOut() {
246
+ await this.post('auth/logout');
247
+ this.auth = undefined;
248
+ this.current = undefined;
249
+ }
250
+ /** End every session of the signed-in account, on every device, this one too. */
251
+ async signOutEverywhere() {
252
+ await this.post('auth/logout-all');
253
+ this.auth = undefined;
254
+ this.current = undefined;
255
+ }
256
+ me() {
257
+ return this.get('auth/me');
258
+ }
259
+ /** Add another passkey (another device) to the signed-in account; answers its credential id. */
260
+ async addPasskey(passkey) {
261
+ const begin = await this.post('auth/passkeys/add/begin');
262
+ const registration = await passkey.create(begin.options);
263
+ const { credentialId } = await this.post('auth/passkeys/add/finish', {
264
+ challengeId: begin.challengeId,
265
+ registration,
266
+ });
267
+ return credentialId;
268
+ }
269
+ /** Remove one of the account's passkeys (not the last). */
270
+ async removePasskey(credentialId) {
271
+ await this.delete(`auth/passkeys/${seg(credentialId)}`);
272
+ }
273
+ /** Name one of the account's passkeys (a device label); `''` clears the name. */
274
+ async labelPasskey(credentialId, label) {
275
+ await this.post(`auth/passkeys/${seg(credentialId)}`, { label });
276
+ }
277
+ /** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
278
+ usage() {
279
+ return this.get('auth/usage');
280
+ }
281
+ /**
282
+ * The node's attestation document, unverified: for display only. Every
283
+ * request this client sends is sealed to a key it verified itself first.
284
+ */
285
+ async attestation() {
286
+ const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
287
+ const response = await this.raw(`${this.base}/attestation?nonce=${nonce}`);
288
+ if (!response.ok)
289
+ throw await refusal(response);
290
+ return (await response.json());
291
+ }
292
+ /** The account's API keys (never the keys themselves). */
293
+ async listApiKeys() {
294
+ const { keys } = await this.get('auth/apikeys');
295
+ return keys;
296
+ }
297
+ async revokeApiKey(keyId) {
298
+ await this.delete(`auth/apikeys/${seg(keyId)}`);
299
+ }
300
+ /** Create an API key for an agent or server. The key is returned once. */
301
+ createApiKey(options) {
302
+ return this.post('auth/apikeys', options);
303
+ }
304
+ // ---------------------------------------------------------------- apps
305
+ /** Upload a raw WASM bundle (development clusters only); answers its hash. */
306
+ async uploadBundle(wasm) {
307
+ const { hash } = await this.json('POST', 'bundles', undefined, wasm);
308
+ return hash;
309
+ }
310
+ /** Upload a blob for `app`: `blobId` is base58 (what apps print), `id` hex. */
311
+ uploadBlob(app, data) {
312
+ return this.json('POST', `apps/${seg(app)}/blobs`, undefined, data);
313
+ }
314
+ /** One of `app`'s blobs, by id (base58 or hex). */
315
+ async downloadBlob(app, id) {
316
+ const response = await this.request('GET', `apps/${seg(app)}/blobs/${seg(id)}`);
317
+ if (!response.ok)
318
+ throw await refusal(response);
319
+ return new Uint8Array(await response.arrayBuffer());
320
+ }
321
+ /** `app`'s blobs. */
322
+ async listBlobs(app) {
323
+ const { blobs } = await this.get(`apps/${seg(app)}/blobs`);
324
+ return blobs;
325
+ }
326
+ /** One of `app`'s blobs' id and size; `NotFound` if the app has no such blob. */
327
+ blobInfo(app, id) {
328
+ return this.get(`apps/${seg(app)}/blobs/${seg(id)}/info`);
329
+ }
330
+ /** Delete one of `app`'s blobs (its owner, or an admin of its organization). */
331
+ async deleteBlob(app, id) {
332
+ await this.delete(`apps/${seg(app)}/blobs/${seg(id)}`);
333
+ }
334
+ // ---------------------------------------------------------------- packages
335
+ /** Install a signed `.mpk` package file. */
336
+ uploadPackage(mpk) {
337
+ return this.json('POST', 'packages', undefined, mpk, 'application/octet-stream');
338
+ }
339
+ /** Install `package@version` from the cluster's registry; every node that runs it verifies it. */
340
+ installPackage(pkg, version) {
341
+ return this.post('packages/install', { package: pkg, version });
342
+ }
343
+ /** Installed packages and their versions. */
344
+ async packages() {
345
+ const { packages } = await this.get('packages');
346
+ return packages;
347
+ }
348
+ /** One installed package and its versions. */
349
+ package(pkg) {
350
+ return this.get(`packages/${seg(pkg)}`);
351
+ }
352
+ /** Uninstall one version, or the whole package. Refused while an app runs it. */
353
+ uninstallPackage(pkg, version) {
354
+ return this.delete(version === undefined ? `packages/${seg(pkg)}` : `packages/${seg(pkg)}/${seg(version)}`);
355
+ }
356
+ // ---------------------------------------------------------------- app instances
357
+ /** Apps this caller reaches, optionally only those of one package or bundle. */
358
+ async listApps(filter = {}) {
359
+ const q = new URLSearchParams();
360
+ if (filter.package)
361
+ q.set('package', filter.package);
362
+ if (filter.bundle)
363
+ q.set('bundle', filter.bundle);
364
+ const query = q.toString();
365
+ const { apps } = await this.get(query ? `apps?${query}` : 'apps');
366
+ return apps;
367
+ }
368
+ getApp(app) {
369
+ return this.get(`apps/${seg(app)}`);
370
+ }
371
+ /** Delete an app instance (its owner, or an admin of its organization). */
372
+ async deleteApp(app) {
373
+ await this.delete(`apps/${seg(app)}`);
374
+ }
375
+ /** Who may reach the app: `{ kind: 'owner' | 'org' | 'public' | 'accounts' | 'team', ... }`. */
376
+ async setAccess(app, access) {
377
+ await this.post(`apps/${seg(app)}/access`, { access });
378
+ }
379
+ /** Set the app instance's metadata (its owner, or an admin of its organization); at most 16 KiB. */
380
+ async setAppMetadata(app, metadata) {
381
+ await this.post(`apps/${seg(app)}/metadata`, { metadata });
382
+ }
383
+ /** What the app's storage holds: `{ keys, bytes, blobs, blobBytes }`. */
384
+ appStorage(app) {
385
+ return this.get(`apps/${seg(app)}/storage`);
386
+ }
387
+ /** The ABI the app's bundle embeds: `{ bundle, abi }`. */
388
+ appAbi(app) {
389
+ return this.get(`apps/${seg(app)}/abi`);
390
+ }
391
+ /**
392
+ * Hand the app to another account (an organization's app only to one of its
393
+ * members). The owner approves with `passkey`.
394
+ */
395
+ async transferApp(app, to, passkey) {
396
+ await this.postConfirmed(`apps/${seg(app)}/transfer`, { to }, passkey);
397
+ }
398
+ /**
399
+ * Move an app to another release of its package (`{ version }`), another
400
+ * package, or a bundle hash, running its migration. Answers the upgrade's
401
+ * entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
402
+ * Where the owner holds app upgrades, the node asks for the owner's approval
403
+ * (`412 CONFIRM_REQUIRED`), which `passkey` gives.
404
+ */
405
+ upgradeApp(app, target, passkey) {
406
+ const path = `apps/${seg(app)}/upgrade`;
407
+ return passkey ? this.postConfirmed(path, target, passkey) : this.post(path, target);
408
+ }
409
+ /**
410
+ * POST `body` to `path`, answering a `412 CONFIRM_REQUIRED` with `passkey`'s
411
+ * assertion over the node's challenge (deleting an org, approving a release, ...).
412
+ */
413
+ postConfirmed(path, body, passkey) {
414
+ return this.confirmed('POST', path, body, passkey);
415
+ }
416
+ /** {@link postConfirmed} for a DELETE (deleting an organization). */
417
+ deleteConfirmed(path, passkey, body = {}) {
418
+ return this.confirmed('DELETE', path, body, passkey);
419
+ }
420
+ async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b)) {
421
+ try {
422
+ return await send(body);
423
+ }
424
+ catch (error) {
425
+ if (!(error instanceof ConfirmRequired))
426
+ throw error;
427
+ const d = error.details;
428
+ let assertion;
429
+ try {
430
+ assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials);
431
+ }
432
+ catch (declined) {
433
+ // WebAuthn rejects a prompt the user dismissed with `NotAllowedError`
434
+ // (`AbortError` when it was aborted); anything else is a real failure.
435
+ const name = declined?.name;
436
+ if (name === 'NotAllowedError' || name === 'AbortError')
437
+ throw new ConfirmCancelled(declined);
438
+ throw declined;
439
+ }
440
+ return send({ ...body, confirm: { challengeId: d.challengeId, assertion } });
441
+ }
442
+ }
443
+ /**
444
+ * Create an app instance from a bundle hash, or from a package release
445
+ * (`{ package, version?, service? }`, installed from the registry first if
446
+ * the cluster lacks it).
447
+ */
448
+ async createApp(name, bundle, options = {}) {
449
+ const target = typeof bundle === 'string' ? { bundle } : bundle;
450
+ const { appId } = await this.post('apps', { name, ...target, ...options });
451
+ return appId;
452
+ }
453
+ /**
454
+ * Call a method. A retry with the same `idempotencyKey` is answered once.
455
+ * Without one, the client makes one, so its own retries of a 503 share it.
456
+ */
457
+ call(app, method, args = {}, idempotencyKey) {
458
+ const key = idempotencyKey ?? newIdempotencyKey();
459
+ return this.retried('POST', `apps/${seg(app)}/intents`, { method, args, idempotencyKey: key });
460
+ }
461
+ /**
462
+ * Call a method that needs the account holder's approval: the node returns a
463
+ * challenge over this exact call, which `passkey` signs. A declined prompt
464
+ * rejects with {@link ConfirmCancelled}.
465
+ */
466
+ async callConfirmed(app, method, args, passkey, idempotencyKey) {
467
+ const path = `apps/${seg(app)}/intents`;
468
+ const key = idempotencyKey ?? newIdempotencyKey();
469
+ // The confirmed attempt goes out exactly once: its challenge is single-use,
470
+ // so a resend would be refused on the challenge, not answered from the key.
471
+ return this.confirmed('POST', path, { method, args, idempotencyKey: key }, passkey, (body) => this.retried('POST', path, body, body.confirm === undefined ? this.retry.retries : 0));
472
+ }
473
+ /** Run a read-only method: strong by default, or stale from any replica. */
474
+ view(app, method, args = {}, options = {}) {
475
+ return this.retried('POST', `apps/${seg(app)}/views`, {
476
+ method,
477
+ args,
478
+ consistency: options.stale ? 'stale' : 'strong',
479
+ minIndex: options.minIndex ?? 0,
480
+ });
481
+ }
482
+ /** Committed events from index `from`, then live, until `signal` aborts. */
483
+ events(app, from = 0, signal) {
484
+ return this.stream(`apps/${seg(app)}/events?from=${from}`, signal);
485
+ }
486
+ /** A server-sent event stream on `path`, sealed like every other answer. */
487
+ async *stream(path, signal) {
488
+ const headers = {};
489
+ if (this.auth)
490
+ headers.authorization = `Bearer ${this.auth}`;
491
+ const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
492
+ if (!response.ok)
493
+ throw await refusal(response);
494
+ if (!response.body)
495
+ throw new CalimeroError(response.status, 'EVENTS', 'the event stream has no body');
496
+ yield* sseFrames(response.body);
497
+ }
498
+ // ---------------------------------------------------------------- presence
499
+ /**
500
+ * Publish this device's presence in `app`: `state` (at most 4 KiB) or `null`
501
+ * to retract it. The newest `seq` wins; send the same `seq` and state again
502
+ * as a heartbeat, since an entry not refreshed within 7 s is gone.
503
+ */
504
+ async publishPresence(app, seq, state) {
505
+ await this.post(`apps/${seg(app)}/presence`, { seq, state: state === null ? null : toHex(state) });
506
+ }
507
+ /** Who is present in `app` now. */
508
+ async presence(app) {
509
+ const { present } = await this.get(`apps/${seg(app)}/presence`);
510
+ return present;
511
+ }
512
+ /**
513
+ * Presence as a stream until `signal` aborts: a `snapshot` event (`data` the
514
+ * entries present), then a `presence` event (`data` one entry) per change.
515
+ */
516
+ presenceStream(app, signal) {
517
+ return this.stream(`apps/${seg(app)}/presence/stream`, signal);
518
+ }
519
+ // ---------------------------------------------------------------- aliases
520
+ /** Name an app (its id or context id) for this account. */
521
+ async setAlias(alias, app) {
522
+ await this.put(`auth/aliases/${seg(alias)}`, { app });
523
+ }
524
+ /** This account's aliases, alias → app id. */
525
+ async aliases() {
526
+ const { aliases } = await this.get('auth/aliases');
527
+ return aliases;
528
+ }
529
+ /** The app an alias names. */
530
+ async alias(alias) {
531
+ const { appId } = await this.get(`auth/aliases/${seg(alias)}`);
532
+ return appId;
533
+ }
534
+ async deleteAlias(alias) {
535
+ await this.delete(`auth/aliases/${seg(alias)}`);
536
+ }
537
+ // ---------------------------------------------------------------- organizations
538
+ async createOrg(name) {
539
+ const { orgId } = await this.post('orgs', { name });
540
+ return orgId;
541
+ }
542
+ /** The organization, with the teams this caller reaches or manages. */
543
+ getOrg(org) {
544
+ return this.get(`orgs/${seg(org)}`);
545
+ }
546
+ /**
547
+ * Delete an organization with its teams, invites and apps (its owner only).
548
+ * The node asks for the owner's passkey (`412 CONFIRM_REQUIRED`), which `passkey` gives.
549
+ */
550
+ async deleteOrg(org, passkey) {
551
+ await this.deleteConfirmed(`orgs/${seg(org)}`, passkey);
552
+ }
553
+ /** Set the organization's metadata (an admin, or a holder of `MANAGE_METADATA`); at most 16 KiB. */
554
+ async setOrgMetadata(org, metadata) {
555
+ await this.post(`orgs/${seg(org)}/metadata`, { metadata });
556
+ }
557
+ /** Set a member's metadata (theirs, or an admin's); `null` clears it. */
558
+ async setMemberMetadata(org, account, metadata) {
559
+ await this.post(`orgs/${seg(org)}/members/${seg(account)}/metadata`, { metadata });
560
+ }
561
+ /**
562
+ * Upgrade every app of `org` that runs `target.package` to `target.version`
563
+ * (optionally one team's, with `cascade` the teams below it). The org's owner,
564
+ * or a holder of `MANAGE_APPLICATION`, approves the whole run with `passkey`.
565
+ * Answers the run; {@link upgradeRun} follows it.
566
+ */
567
+ upgradeOrg(org, target, passkey) {
568
+ return this.postConfirmed(`orgs/${seg(org)}/upgrade`, { ...target }, passkey);
569
+ }
570
+ /** An organization-wide upgrade run: its apps, those done and those that failed. */
571
+ upgradeRun(org, run) {
572
+ return this.get(`orgs/${seg(org)}/upgrades/${seg(run)}`);
573
+ }
574
+ /** Run the apps that failed in `run` again, as a new run, approved with `passkey`. */
575
+ retryUpgradeRun(org, run, passkey) {
576
+ return this.postConfirmed(`orgs/${seg(org)}/upgrades/${seg(run)}/retry`, {}, passkey);
577
+ }
578
+ /**
579
+ * Approve a mero-tee release to hold the apps of `org`, or of this account
580
+ * outside any organization (`org` `null`); `approved: false` withdraws it.
581
+ * Until then, nodes on that release get none of those apps' data. The owner
582
+ * approves with `passkey`.
583
+ */
584
+ async approveRelease(org, version, passkey, approved = true) {
585
+ const path = org === null ? 'auth/releases' : `orgs/${seg(org)}/releases`;
586
+ return this.postConfirmed(path, { version, approved }, passkey);
587
+ }
588
+ /**
589
+ * Approve the URL prefixes apps from `pkg`'s publisher may call out to, for
590
+ * `org`'s apps or this account's own (`org` `null`). `prefixes` `'manifest'`
591
+ * (the default) approves every prefix the package's latest release declares;
592
+ * `replace` sets exactly `prefixes`, which is how one is revoked. Approved
593
+ * with `passkey`.
594
+ */
595
+ async approveOutbound(org, pkg, passkey, options = {}) {
596
+ const path = org === null ? 'auth/outbound' : `orgs/${seg(org)}/outbound`;
597
+ return this.postConfirmed(path, { package: pkg, prefixes: options.prefixes ?? 'manifest', replace: options.replace ?? false }, passkey);
598
+ }
599
+ /** Add a member, or change their role. */
600
+ async setMember(org, account, role) {
601
+ await this.post(`orgs/${seg(org)}/members`, { account, role });
602
+ }
603
+ removeMember(org, account) {
604
+ return this.delete(`orgs/${seg(org)}/members/${seg(account)}`);
605
+ }
606
+ /**
607
+ * Set capability bits ({@link ORG_CAPABILITIES}): for a member (`account`),
608
+ * for a member of one team (`account` and `team`), or the organization's
609
+ * default (neither).
610
+ */
611
+ async setCapabilities(org, capabilities, at = {}) {
612
+ await this.post(`orgs/${seg(org)}/capabilities`, { capabilities, ...at });
613
+ }
614
+ async createTeam(org, name, join, parent) {
615
+ const { teamId } = await this.post(`orgs/${seg(org)}/teams`, { name, join, parent });
616
+ return teamId;
617
+ }
618
+ getTeam(org, team) {
619
+ return this.get(`orgs/${seg(org)}/teams/${seg(team)}`);
620
+ }
621
+ /**
622
+ * Rename, change who may join, set the app's metadata, or move the team:
623
+ * `parent: null` moves it to the top, an absent `parent` leaves it.
624
+ */
625
+ async updateTeam(org, team, update) {
626
+ await this.post(`orgs/${seg(org)}/teams/${seg(team)}`, update);
627
+ }
628
+ /** Delete a team, the teams below it and their apps. */
629
+ async deleteTeam(org, team) {
630
+ await this.delete(`orgs/${seg(org)}/teams/${seg(team)}`);
631
+ }
632
+ /** Add a member to a team, or change their row: role, lead, capability bits. */
633
+ async setTeamMember(org, team, account, row = {}) {
634
+ await this.post(`orgs/${seg(org)}/teams/${seg(team)}/members`, { account, ...row });
635
+ }
636
+ async removeTeamMember(org, team, account) {
637
+ await this.delete(`orgs/${seg(org)}/teams/${seg(team)}/members/${seg(account)}`);
638
+ }
639
+ async invite(org, role, teams = [], options = {}) {
640
+ const { token } = await this.post(`orgs/${seg(org)}/invites`, { role, teams, ...options });
641
+ return token;
642
+ }
643
+ redeemInvite(token) {
644
+ return this.post(`invites/${seg(token)}/redeem`);
645
+ }
646
+ }