@calimero-network/mero-js 29.14.0 → 29.16.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.
@@ -0,0 +1,10 @@
1
+ export { DEFAULT_RETRY, NodeTransport, directoryTransport, newIdempotencyKey, queryString, refusal, retryDelayMs, routePath, sseFrames, } from './transport.js';
2
+ export type { CalimeroSession, CloudEvent, CloudRetryOptions, DirectorySource, HttpMethod, NodeTransportOptions, QueryValue, RequestOptions, Transport, } from './transport.js';
3
+ export { claimedRelease, fetchAttestedKey, freshCollateral, MOCK_QUOTE_HEADER } from '../cloud-v2/attestation.js';
4
+ export type { AttestationDoc, CloudPolicy, CloudReleasePolicy } from '../cloud-v2/attestation.js';
5
+ export { DEFAULT_DIRECTORY, DirectoryError } from '../cloud-v2/directory.js';
6
+ export type { DirectoryFailure } from '../cloud-v2/directory.js';
7
+ export { browserPasskey, b64url, fromB64url } from '../cloud-v2/passkey.js';
8
+ export type { AssertionJSON, PasskeyCreateOptions, PasskeyGetOptions, PasskeyProvider, PasskeyUserVerification, RegistrationJSON, } from '../cloud-v2/passkey.js';
9
+ export * from '../cloud-v2/errors.js';
10
+ export * from '../sealed/index.js';
@@ -0,0 +1,21 @@
1
+ // The mero-js runtime (`@calimero-network/mero-js/runtime`): everything a
2
+ // route layer sends through, and nothing that is a route.
3
+ //
4
+ // `Transport.request(method, path, options)` reaches one node route over the
5
+ // attested, sealed channel with the credential held now; the runtime handles
6
+ // attestation, sealing, the session and its refresh, `421 WRONG_NODE` moves,
7
+ // D70 retries, the `412 CONFIRM_REQUIRED` passkey step-up, error mapping and
8
+ // server-sent events. `CalimeroClient` (the package root) is the hand-written
9
+ // route layer on top; `client.transport` is its runtime, which a generated
10
+ // route layer can share.
11
+ export { DEFAULT_RETRY, NodeTransport, directoryTransport, newIdempotencyKey, queryString, refusal, retryDelayMs, routePath, sseFrames, } from './transport.js';
12
+ // Attestation: what a node must prove before anything is sealed to it.
13
+ export { claimedRelease, fetchAttestedKey, freshCollateral, MOCK_QUOTE_HEADER } from '../cloud-v2/attestation.js';
14
+ // The node directory a cluster's node list is read from.
15
+ export { DEFAULT_DIRECTORY, DirectoryError } from '../cloud-v2/directory.js';
16
+ // Passkeys (WebAuthn) for the step-up and the ceremonies.
17
+ export { browserPasskey, b64url, fromB64url } from '../cloud-v2/passkey.js';
18
+ // A refusal as a typed error.
19
+ export * from '../cloud-v2/errors.js';
20
+ // The sealed channel and the quote and release checks under it.
21
+ export * from '../sealed/index.js';
@@ -0,0 +1,311 @@
1
+ /**
2
+ * The runtime every route of a Calimero node is sent through: the part of
3
+ * this client that is not a route.
4
+ *
5
+ * A route layer (the hand-written {@link CalimeroClient} today; one generated
6
+ * from the node's OpenAPI document later) knows a method, a path and what the
7
+ * route takes and answers. Everything else is here, behind one call,
8
+ * {@link Transport.request}:
9
+ *
10
+ * - the sealed channel to each node, opened only after the node's attestation
11
+ * passed the caller's policy (`../sealed`, `../cloud-v2/attestation`);
12
+ * - the credential (an API key or a session), and the session's refresh when a
13
+ * node answers `401` to it;
14
+ * - moving to another node of the cluster on `421 WRONG_NODE`, and failing
15
+ * over when one stays unavailable to a call or a view (D70 retries);
16
+ * - the `412 CONFIRM_REQUIRED` step-up: the passkey's assertion over the
17
+ * node's challenge, sent with the same body again;
18
+ * - error mapping (a refusal is a typed {@link CalimeroError}) and
19
+ * server-sent event decoding.
20
+ *
21
+ * Paths are relative to the node (`apps/{app}/intents`, a leading `/` is
22
+ * dropped); {@link routePath} fills a path template with encoded segments.
23
+ */
24
+ import { type CloudPolicy } from '../cloud-v2/attestation.js';
25
+ import { CalimeroError } from '../cloud-v2/errors.js';
26
+ import { type PasskeyProvider, type PasskeyUserVerification } from '../cloud-v2/passkey.js';
27
+ /** A signed-in session: what sign-up, sign-in and a refresh answer. */
28
+ export interface CalimeroSession {
29
+ accountId: string;
30
+ sessionToken: string;
31
+ expiresAt: number;
32
+ refreshToken: string;
33
+ }
34
+ /** One server-sent event: `data` parsed as JSON when it is. */
35
+ export interface CloudEvent {
36
+ id: string;
37
+ kind: string;
38
+ data: unknown;
39
+ }
40
+ /**
41
+ * How a call or a view is sent again while the node is unavailable (D70): with
42
+ * the same key, after a backoff that starts at `baseDelayMs` and doubles up to
43
+ * `maxDelayMs`, no jitter, at most `retries` times. The defaults are the Rust
44
+ * client's: 6 retries, 200 ms doubling, at most 2 s (about 9 s in all, past a
45
+ * leader election).
46
+ */
47
+ export interface CloudRetryOptions {
48
+ /** Retries after the first attempt. Default 6. */
49
+ retries?: number;
50
+ /** The first backoff; each retry doubles it. Default 200 ms. */
51
+ baseDelayMs?: number;
52
+ /** The longest backoff. Default 2 s. */
53
+ maxDelayMs?: number;
54
+ }
55
+ export declare const DEFAULT_RETRY: {
56
+ readonly retries: 6;
57
+ readonly baseDelayMs: 200;
58
+ readonly maxDelayMs: 2000;
59
+ };
60
+ /** The backoff before retry `retry` (0 for the first): `baseDelayMs` doubling, at most `maxDelayMs`. */
61
+ export declare function retryDelayMs(retry: number, options: {
62
+ baseDelayMs: number;
63
+ maxDelayMs: number;
64
+ }): number;
65
+ /** The HTTP methods the node's routes use. */
66
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
67
+ /** A query parameter's value; `undefined` and `null` leave it out, an array repeats it. */
68
+ export type QueryValue = string | number | boolean | null | undefined;
69
+ /** What a request carries beyond its method and path. */
70
+ export interface RequestOptions {
71
+ /** Query parameters, appended to the path (encoded here). */
72
+ query?: Record<string, QueryValue | QueryValue[]>;
73
+ /** A JSON body. */
74
+ body?: unknown;
75
+ /** A raw body instead of JSON (a bundle, a package, a blob). */
76
+ bytes?: Uint8Array;
77
+ /** The raw body's content type. Default `application/wasm`. */
78
+ contentType?: string;
79
+ /**
80
+ * Answers a `412 CONFIRM_REQUIRED`: the passkey asserts over the node's
81
+ * challenge and the same JSON body is sent again with `confirm`. Without
82
+ * one the `412` reaches the caller as {@link ConfirmRequired}.
83
+ */
84
+ passkey?: PasskeyProvider;
85
+ /** The user verification the step-up asks for when the node names none. Default `required`. */
86
+ userVerification?: PasskeyUserVerification;
87
+ /**
88
+ * D70 retries for a call or a view: `true` sends it again while the node
89
+ * answers `503` or cannot be reached, then fails over to the next node;
90
+ * `'once'` sends it exactly once (a confirmed attempt, whose challenge is
91
+ * single-use). Only for a JSON body that carries its own idempotency key,
92
+ * or a read.
93
+ */
94
+ retry?: boolean | 'once';
95
+ /** Aborts the request (and a stream), and the step-up's passkey prompt. */
96
+ signal?: AbortSignal;
97
+ /** Aborts only the step-up's passkey prompt, not the requests around it. Default `signal`. */
98
+ promptSignal?: AbortSignal;
99
+ }
100
+ /**
101
+ * What a route layer calls. Every method sends through the attested, sealed
102
+ * channel of the node in use, with the credential held now. A refusal (any
103
+ * status other than 2xx) is thrown as a typed {@link CalimeroError}
104
+ * ({@link errorFrom}); a transport failure as the error that caused it.
105
+ */
106
+ export interface Transport {
107
+ /** The response to `method path`, status 2xx; its body is not read. */
108
+ request(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
109
+ /** {@link request}'s JSON body (`{}` for an empty one). */
110
+ json<T = Record<string, unknown>>(method: HttpMethod, path: string, options?: RequestOptions): Promise<T>;
111
+ /** A server-sent event stream on `GET path`, until the stream ends or `signal` aborts. */
112
+ events(path: string, options?: Pick<RequestOptions, 'query' | 'signal'>): AsyncGenerator<CloudEvent>;
113
+ }
114
+ /** How {@link NodeTransport} reaches a cluster's nodes. */
115
+ export interface NodeTransportOptions {
116
+ /** One node of the cluster: the one the transport talks to first. */
117
+ baseUrl: string;
118
+ /** Other nodes of the same cluster, in the order to move to them. */
119
+ nodes?: string[];
120
+ /** What every node must attest to before anything is sent to it. */
121
+ policy: CloudPolicy;
122
+ /** Fetch that carries attestation and envelopes. Defaults to global `fetch`. */
123
+ fetch?: typeof fetch;
124
+ /** How calls and views retry a 503 or a failed connection. */
125
+ retry?: CloudRetryOptions;
126
+ }
127
+ /** Where a transport whose nodes came from the directory re-reads its node list. */
128
+ export interface DirectorySource {
129
+ directory: string;
130
+ cluster: string;
131
+ zone: string | undefined;
132
+ random: () => number;
133
+ /** When the listing is due to be read again (ms since the epoch). */
134
+ due: number;
135
+ /** The re-read under way, if any. */
136
+ reading?: Promise<void>;
137
+ }
138
+ /** Read a refused response's body and map it ({@link errorFrom}). */
139
+ export declare function refusal(response: Response): Promise<CalimeroError>;
140
+ /** Server-sent event frames from a response body: `{ id, kind, data }` each, `data` parsed as JSON when it is. */
141
+ export declare function sseFrames(body: ReadableStream<Uint8Array>): AsyncGenerator<CloudEvent>;
142
+ /**
143
+ * A key for a call the caller gave none, made once so every retry shares it:
144
+ * `node:c-` and 16 random bytes in base64url without padding, as the Rust
145
+ * client mints it. It only has to outlive this client's own retries, so it
146
+ * takes the node's short-lived class: a key starting `node:` is kept 10
147
+ * minutes, a caller's own key 24 hours.
148
+ * `getRandomValues`, not `randomUUID`, which a page outside a secure context lacks.
149
+ */
150
+ export declare function newIdempotencyKey(): string;
151
+ /**
152
+ * `template` (`apps/{app}/blobs/{id}`) with each `{name}` replaced by
153
+ * `params[name]`, encoded as one path segment so an id cannot reach another
154
+ * route. A name `params` lacks throws.
155
+ */
156
+ export declare function routePath(template: string, params?: Record<string, string | number>): string;
157
+ /** `query` as a search string (`?a=1&b=2`), or `''` when it sets nothing. */
158
+ export declare function queryString(query: RequestOptions['query']): string;
159
+ /**
160
+ * The {@link Transport} to one Calimero cluster: its nodes, each attested
161
+ * under one policy before its first request, the credential, and the session's
162
+ * refresh. {@link CalimeroClient} is built on one (`client.transport`); a
163
+ * generated route layer takes one too.
164
+ */
165
+ export declare class NodeTransport implements Transport {
166
+ /** Every node this transport may use, `baseUrl` first (or as the directory last ordered them). */
167
+ private nodeUrls;
168
+ /** The one in use: an index into {@link nodeUrls}. */
169
+ private at;
170
+ /** A sealed fetch per node, each attesting its own node on first use. */
171
+ private readonly channels;
172
+ private readonly policy;
173
+ /** The unsealed fetch, for the one route served in the clear (`/attestation`). */
174
+ readonly raw: typeof fetch;
175
+ private auth;
176
+ private current;
177
+ /** The refresh under way, which every request that needs one shares. */
178
+ private refreshing;
179
+ private readonly retry;
180
+ /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
181
+ private readonly networkFailures;
182
+ /** The directory the node list came from, if it came from one. */
183
+ private source;
184
+ constructor(options: NodeTransportOptions);
185
+ /** Re-read the node list from `source` when it is due, and after a failover. */
186
+ useDirectory(source: DirectorySource): void;
187
+ /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved on. */
188
+ get nodeUrl(): string;
189
+ /** Every node this transport may use, in the order it moves through them. */
190
+ get nodes(): string[];
191
+ /** Authenticate every request with an API key. */
192
+ useApiKey(key: string): void;
193
+ /** Authenticate every request with `session`, refreshed when a node answers `401` to it. */
194
+ useSession(session: CalimeroSession): void;
195
+ /** Drop the credential: no request after this carries it. */
196
+ signOut(): void;
197
+ get session(): CalimeroSession | undefined;
198
+ /** The sealed channel to `url`, attested under the policy before its first request. */
199
+ private channel;
200
+ /**
201
+ * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
202
+ * next one. False when there is no other. A request that saw the same node
203
+ * refuse after another request moved on already does not move again.
204
+ */
205
+ private moveOn;
206
+ /**
207
+ * Fail over from node `from`, where a request's retries ran out, to the next
208
+ * node it has not `tried`. False when it has tried them all. A request whose
209
+ * node another request moved away from already goes to that node, if untried.
210
+ */
211
+ private failOver;
212
+ /**
213
+ * Read the node list again from the directory, in the background: the node
214
+ * in use stays first, then the directory's order; a node no longer listed is
215
+ * dropped. A failed read is ignored. One read at a time.
216
+ */
217
+ private reread;
218
+ /**
219
+ * Send one request to the node in use, with the credential held now, and
220
+ * answer the response, whatever its status. {@link send} judges it.
221
+ */
222
+ private once;
223
+ /**
224
+ * {@link once}, answering what a lost issuing node or a node on the wrong
225
+ * release does to a session: a successful response, or the refusal as a
226
+ * typed error ({@link errorFrom}).
227
+ *
228
+ * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
229
+ * signed the session may be gone, replaced, or out of reach of the node
230
+ * asked, which any node of the cluster answers with a refresh. The
231
+ * transport refreshes once, shared by every request that needs it at the
232
+ * same time ({@link refresh}), and repeats the request once. A refresh that
233
+ * is refused surfaces as {@link LoginRequired}.
234
+ * - `421 WRONG_NODE`: this node's release may not serve the account. The
235
+ * transport moves to the next node it knows and repeats the request once;
236
+ * with none, the {@link WrongNode} reaches the caller.
237
+ *
238
+ * Repeating is safe even for a request that is not idempotent: the node
239
+ * answers `401` and `421` when it judges the credential, before it reads what
240
+ * the request asks, so the first attempt ran nothing. The exceptions are the
241
+ * ceremonies that mint a session ({@link ceremony}), never repeated here.
242
+ */
243
+ private send;
244
+ /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
245
+ private mayRefresh;
246
+ /**
247
+ * A fresh session in place of the one `token` belonged to: by the refresh
248
+ * another request already made, or by one this request starts.
249
+ */
250
+ private renew;
251
+ private read;
252
+ /**
253
+ * {@link read} for a call or a view (D70): while the node answers 503 or
254
+ * cannot be reached, the same request, with the same idempotency key, is sent
255
+ * again after {@link retryDelayMs} (or the pause the node asked for, if
256
+ * longer), at most `retries` times. Every other answer is final.
257
+ *
258
+ * When a node's retries run out, the request fails over to the next node the
259
+ * transport knows and its retries start again there, with the same key: the
260
+ * key is kept in the app's replicated log, so the cluster answers it once
261
+ * whichever node it reaches. Each node is tried once per request. The last
262
+ * failure surfaces as {@link UnavailableError}: the node's 503, or reason
263
+ * `network` for a connection that failed, whose `committed` is `'unknown'`.
264
+ *
265
+ * `once` sends the request exactly once, to the node in use (a confirmed
266
+ * attempt, whose challenge is single-use).
267
+ */
268
+ private retried;
269
+ /**
270
+ * One JSON request, retried as `options.retry` says. The confirmed attempt
271
+ * of a step-up (`body.confirm` set) goes out exactly once.
272
+ */
273
+ private attempt;
274
+ /**
275
+ * {@link attempt}, answering a `412 CONFIRM_REQUIRED` with `passkey`'s
276
+ * assertion over the node's challenge: the same body again, with `confirm`.
277
+ * Without a passkey the `412` reaches the caller as {@link ConfirmRequired}.
278
+ */
279
+ private confirmed;
280
+ request(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
281
+ json<T = Record<string, unknown>>(method: HttpMethod, path: string, options?: RequestOptions): Promise<T>;
282
+ events(path: string, options?: Pick<RequestOptions, 'query' | 'signal'>): AsyncGenerator<CloudEvent>;
283
+ /**
284
+ * `GET path` in the clear, from the node in use: only for the route a node
285
+ * serves unsealed (`attestation`), and never with the credential.
286
+ */
287
+ unsealed(path: string, options?: Pick<RequestOptions, 'query'>): Promise<Response>;
288
+ /**
289
+ * Swap the refresh token for a new session, on whichever node the transport
290
+ * uses: any node of the cluster takes it. A refresh token is used once, so
291
+ * requests made while one refresh is under way share it rather than sending
292
+ * the same token again (which would revoke the session).
293
+ */
294
+ refresh(): Promise<CalimeroSession>;
295
+ private exchangeRefresh;
296
+ }
297
+ /**
298
+ * A {@link NodeTransport} to the cluster named `cluster`, its node list read
299
+ * from the node directory (`directory`), those in `zone` first, each group in
300
+ * weighted-random order, and read again in the background after a failover
301
+ * and once the listing's `refreshAfterSeconds` passed.
302
+ */
303
+ export declare function directoryTransport(options: {
304
+ cluster: string;
305
+ policy: CloudPolicy;
306
+ directory: string;
307
+ zone?: string;
308
+ fetch?: typeof fetch;
309
+ retry?: CloudRetryOptions;
310
+ random?: () => number;
311
+ }): Promise<NodeTransport>;