@calimero-network/mero-js 27.1.0 → 27.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cloud-v2/client.d.ts +67 -5
- package/dist/cloud-v2/client.js +151 -28
- package/dist/cloud-v2/errors.d.ts +26 -1
- package/dist/cloud-v2/errors.js +31 -1
- package/dist/cloud-v2/index.d.ts +1 -1
- package/dist/cloud-v2/index.js +1 -1
- package/dist/index.browser.mjs +5 -5
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +137 -22
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +137 -22
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,10 +79,13 @@ Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
|
|
|
79
79
|
- **Operator** (ops token): `opsApps`, `opsOrgs`, `setReleaseDeadline`, `liftReleaseDeadline`.
|
|
80
80
|
- **Any route**: `get`, `post`, `put`, `delete`; `postConfirmed` and `deleteConfirmed` answer a `412 CONFIRM_REQUIRED` with a passkey.
|
|
81
81
|
|
|
82
|
-
Refusals are typed (`LoginRequired`, `NotFound`, `ConfirmRequired`, `ConfirmCancelled`,
|
|
82
|
+
Refusals are typed (`LoginRequired`, `WrongNode`, `NotFound`, `ConfirmRequired`, `ConfirmCancelled`,
|
|
83
83
|
`ArgsTooLarge`, `IdempotencyMismatch`, `ReleaseDeadlineError`, `ConsentStale`, `AppError`, `TrapError`,
|
|
84
84
|
`UnavailableError`),
|
|
85
|
-
all extending `CalimeroError` with the node's `status` and `code`.
|
|
85
|
+
all extending `CalimeroError` with the node's `status` and `code`. A `401` once
|
|
86
|
+
the node that issued the session is replaced or out of reach is answered by one
|
|
87
|
+
refresh and a repeat; a `421 WRONG_NODE` by moving to the next of the `nodes` the client was
|
|
88
|
+
given. See the
|
|
86
89
|
[error model](https://calimero-network.github.io/mero-js/reference/error-model/).
|
|
87
90
|
|
|
88
91
|
The pieces it is built from are exported too: the sealed transport
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { type AttestationDoc, type CloudPolicy } from './attestation.js';
|
|
13
13
|
import { type PasskeyProvider } from './passkey.js';
|
|
14
14
|
import { type AppsReport, type ConsentRequest, type ConsentResult, type ConsentStarted, type OpsAppsReport, type OpsOrgsReport, type PageOptions, type PendingApprovals, type PublisherBindingInfo, type PublisherProof, type ReleaseRules } from './approvals.js';
|
|
15
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, errorFrom, } from './errors.js';
|
|
15
|
+
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
16
16
|
export type { Committed } from './errors.js';
|
|
17
17
|
export interface CalimeroSession {
|
|
18
18
|
accountId: string;
|
|
@@ -156,8 +156,15 @@ export declare function retryDelayMs(retry: number, options: {
|
|
|
156
156
|
maxDelayMs: number;
|
|
157
157
|
}): number;
|
|
158
158
|
export interface CalimeroClientOptions {
|
|
159
|
-
/** One node of the cluster. */
|
|
159
|
+
/** One node of the cluster: the one the client talks to first. */
|
|
160
160
|
baseUrl: string;
|
|
161
|
+
/**
|
|
162
|
+
* Other nodes of the same cluster, in the order to move to them when a node
|
|
163
|
+
* answers `421 WRONG_NODE` (its release may not serve this account). Each is
|
|
164
|
+
* attested under the same `policy` before anything is sent to it. Without
|
|
165
|
+
* them a `421` reaches the caller as {@link WrongNode}.
|
|
166
|
+
*/
|
|
167
|
+
nodes?: string[];
|
|
161
168
|
policy: CloudPolicy;
|
|
162
169
|
/** Fetch that carries attestation and envelopes. Defaults to global `fetch`. */
|
|
163
170
|
fetch?: typeof fetch;
|
|
@@ -165,21 +172,70 @@ export interface CalimeroClientOptions {
|
|
|
165
172
|
retry?: CloudRetryOptions;
|
|
166
173
|
}
|
|
167
174
|
export declare class CalimeroClient {
|
|
168
|
-
|
|
169
|
-
private readonly
|
|
175
|
+
/** Every node this client may use, `baseUrl` first. */
|
|
176
|
+
private readonly nodeUrls;
|
|
177
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
178
|
+
private at;
|
|
179
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
180
|
+
private readonly channels;
|
|
181
|
+
private readonly policy;
|
|
170
182
|
/** The unsealed fetch, for the one route served in the clear (`/attestation`). */
|
|
171
183
|
private readonly raw;
|
|
172
184
|
private auth;
|
|
173
185
|
private current;
|
|
186
|
+
/** The refresh under way, which every request that needs one shares. */
|
|
187
|
+
private refreshing;
|
|
174
188
|
private readonly retry;
|
|
175
189
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
176
190
|
private readonly networkFailures;
|
|
177
191
|
constructor(options: CalimeroClientOptions);
|
|
192
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
193
|
+
get nodeUrl(): string;
|
|
194
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
195
|
+
private channel;
|
|
196
|
+
/**
|
|
197
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
198
|
+
* next one. False when there is no other. A request that saw the same node
|
|
199
|
+
* refuse after another request moved on already does not move again.
|
|
200
|
+
*/
|
|
201
|
+
private moveOn;
|
|
178
202
|
/** Authenticate every request with an API key. */
|
|
179
203
|
useApiKey(key: string): void;
|
|
180
204
|
useSession(session: CalimeroSession): void;
|
|
181
205
|
get session(): CalimeroSession | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* Send one request to the node in use, with the credential held now, and
|
|
208
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
209
|
+
*/
|
|
182
210
|
private request;
|
|
211
|
+
/**
|
|
212
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
213
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
214
|
+
* typed error ({@link errorFrom}).
|
|
215
|
+
*
|
|
216
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
217
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
218
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
219
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
220
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
221
|
+
* refused surfaces as {@link LoginRequired}.
|
|
222
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
223
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
224
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
225
|
+
*
|
|
226
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
227
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
228
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
229
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
230
|
+
*/
|
|
231
|
+
private send;
|
|
232
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
233
|
+
private mayRefresh;
|
|
234
|
+
/**
|
|
235
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
236
|
+
* another request already made, or by one this request starts.
|
|
237
|
+
*/
|
|
238
|
+
private renew;
|
|
183
239
|
private json;
|
|
184
240
|
/**
|
|
185
241
|
* {@link json} for a call or a view (D70): while the node answers 503 or
|
|
@@ -199,8 +255,14 @@ export declare class CalimeroClient {
|
|
|
199
255
|
signUp(passkey: PasskeyProvider, name: string): Promise<CalimeroSession>;
|
|
200
256
|
/** Sign in with an existing passkey. */
|
|
201
257
|
signIn(passkey: PasskeyProvider): Promise<CalimeroSession>;
|
|
202
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
260
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
261
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
262
|
+
* same token again (which would revoke the session).
|
|
263
|
+
*/
|
|
203
264
|
refresh(): Promise<CalimeroSession>;
|
|
265
|
+
private exchangeRefresh;
|
|
204
266
|
signOut(): Promise<void>;
|
|
205
267
|
/** End every session of the signed-in account, on every device, this one too. */
|
|
206
268
|
signOutEverywhere(): Promise<void>;
|
package/dist/cloud-v2/client.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { SealedTransportError, createSealedFetch } from '../sealed/sealed.js';
|
|
13
13
|
import { fetchAttestedKey } from './attestation.js';
|
|
14
|
-
import { CalimeroError, ConfirmCancelled, ConfirmRequired, UnavailableError, errorFrom } from './errors.js';
|
|
14
|
+
import { CalimeroError, ConfirmCancelled, ConfirmRequired, LoginRequired, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
15
15
|
import { b64url, fromB64url } from './passkey.js';
|
|
16
16
|
import { pageQuery, pendingApprovalsFrom, } from './approvals.js';
|
|
17
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, errorFrom, } from './errors.js';
|
|
17
|
+
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
18
18
|
/** One URL path segment, so an id cannot reach another route. */
|
|
19
19
|
function seg(value) {
|
|
20
20
|
return encodeURIComponent(value);
|
|
@@ -103,11 +103,27 @@ export const DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2000 };
|
|
|
103
103
|
export function retryDelayMs(retry, options) {
|
|
104
104
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The ceremonies that mint a session, which {@link CalimeroClient.send} never
|
|
108
|
+
* repeats by itself: a sign-in or sign-up is started again by its caller (with
|
|
109
|
+
* the passkey), and a refresh moves through the nodes in
|
|
110
|
+
* {@link CalimeroClient.exchangeRefresh}. A node answers them `421 WRONG_NODE`
|
|
111
|
+
* before it spends the challenge or the refresh token (core #4774).
|
|
112
|
+
*/
|
|
113
|
+
function ceremony(path) {
|
|
114
|
+
return path === 'auth/refresh' || path.startsWith('auth/passkey/');
|
|
115
|
+
}
|
|
106
116
|
export class CalimeroClient {
|
|
107
117
|
constructor(options) {
|
|
118
|
+
/** The one in use: an index into {@link nodeUrls}. */
|
|
119
|
+
this.at = 0;
|
|
120
|
+
/** A sealed fetch per node, each attesting its own node on first use. */
|
|
121
|
+
this.channels = new Map();
|
|
108
122
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
109
123
|
this.networkFailures = new WeakSet();
|
|
110
|
-
|
|
124
|
+
const trim = (url) => url.replace(/\/+$/, '');
|
|
125
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...(options.nodes ?? [])].map(trim))];
|
|
126
|
+
this.policy = options.policy;
|
|
111
127
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
112
128
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
113
129
|
// `fetch` rejects with a `TypeError` when it gets no answer: the connection
|
|
@@ -125,11 +141,35 @@ export class CalimeroClient {
|
|
|
125
141
|
}
|
|
126
142
|
};
|
|
127
143
|
this.raw = raw;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
}
|
|
145
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` moved the client on. */
|
|
146
|
+
get nodeUrl() {
|
|
147
|
+
return this.nodeUrls[this.at];
|
|
148
|
+
}
|
|
149
|
+
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
150
|
+
channel(url) {
|
|
151
|
+
let sealed = this.channels.get(url);
|
|
152
|
+
if (!sealed) {
|
|
153
|
+
sealed = createSealedFetch({
|
|
154
|
+
baseUrl: url,
|
|
155
|
+
transportPublicKey: () => fetchAttestedKey(url, this.policy, this.raw),
|
|
156
|
+
fetch: this.raw,
|
|
157
|
+
});
|
|
158
|
+
this.channels.set(url, sealed);
|
|
159
|
+
}
|
|
160
|
+
return sealed;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
|
|
164
|
+
* next one. False when there is no other. A request that saw the same node
|
|
165
|
+
* refuse after another request moved on already does not move again.
|
|
166
|
+
*/
|
|
167
|
+
moveOn(from) {
|
|
168
|
+
if (this.nodeUrls.length < 2)
|
|
169
|
+
return false;
|
|
170
|
+
if (this.at === from)
|
|
171
|
+
this.at = (from + 1) % this.nodeUrls.length;
|
|
172
|
+
return true;
|
|
133
173
|
}
|
|
134
174
|
/** Authenticate every request with an API key. */
|
|
135
175
|
useApiKey(key) {
|
|
@@ -143,7 +183,11 @@ export class CalimeroClient {
|
|
|
143
183
|
get session() {
|
|
144
184
|
return this.current;
|
|
145
185
|
}
|
|
146
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Send one request to the node in use, with the credential held now, and
|
|
188
|
+
* answer the response, whatever its status. {@link send} judges it.
|
|
189
|
+
*/
|
|
190
|
+
async request(method, path, body, raw, contentType = 'application/wasm', signal) {
|
|
147
191
|
const headers = {};
|
|
148
192
|
if (this.auth)
|
|
149
193
|
headers.authorization = `Bearer ${this.auth}`;
|
|
@@ -156,17 +200,71 @@ export class CalimeroClient {
|
|
|
156
200
|
headers['content-type'] = 'application/json';
|
|
157
201
|
payload = JSON.stringify(body);
|
|
158
202
|
}
|
|
203
|
+
const url = this.nodeUrl;
|
|
159
204
|
try {
|
|
160
|
-
return await this.
|
|
205
|
+
return await this.channel(url)(`${url}/${path}`, { method, headers, body: payload, signal });
|
|
161
206
|
}
|
|
162
207
|
catch (error) {
|
|
163
208
|
throw sealedError(error);
|
|
164
209
|
}
|
|
165
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* {@link request}, answering what a lost issuing node or a node on the
|
|
213
|
+
* wrong release does to a session: a successful response, or the refusal as a
|
|
214
|
+
* typed error ({@link errorFrom}).
|
|
215
|
+
*
|
|
216
|
+
* - `401 UNAUTHENTICATED` to a request that carried a session: the node that
|
|
217
|
+
* signed the session may be gone, replaced, or out of reach of the node
|
|
218
|
+
* asked, which any node of the cluster answers with a refresh. The client
|
|
219
|
+
* refreshes once, shared by every request that needs it at the same time
|
|
220
|
+
* ({@link refresh}), and repeats the request once. A refresh that is
|
|
221
|
+
* refused surfaces as {@link LoginRequired}.
|
|
222
|
+
* - `421 WRONG_NODE`: this node's release may not serve the account. The
|
|
223
|
+
* client moves to the next node it knows and repeats the request once; with
|
|
224
|
+
* none, the {@link WrongNode} reaches the caller.
|
|
225
|
+
*
|
|
226
|
+
* Repeating is safe even for a request that is not idempotent: the node
|
|
227
|
+
* answers `401` and `421` when it judges the credential, before it reads what
|
|
228
|
+
* the request asks, so the first attempt ran nothing. The exceptions are the
|
|
229
|
+
* ceremonies that mint a session ({@link ceremony}), never repeated here.
|
|
230
|
+
*/
|
|
231
|
+
async send(method, path, body, raw, contentType, signal) {
|
|
232
|
+
let refreshed = false;
|
|
233
|
+
let moved = false;
|
|
234
|
+
for (;;) {
|
|
235
|
+
const [token, at] = [this.auth, this.at];
|
|
236
|
+
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
237
|
+
if (response.ok)
|
|
238
|
+
return response;
|
|
239
|
+
const error = await refusal(response);
|
|
240
|
+
if (error instanceof WrongNode && !moved) {
|
|
241
|
+
moved = true;
|
|
242
|
+
if (this.moveOn(at) && !ceremony(path))
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
|
|
246
|
+
refreshed = true;
|
|
247
|
+
await this.renew(token);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Whether a `401` to a request sent with `token` is one a refresh may answer. */
|
|
254
|
+
mayRefresh(token, path) {
|
|
255
|
+
return token !== undefined && this.current !== undefined && !ceremony(path);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* A fresh session in place of the one `token` belonged to: by the refresh
|
|
259
|
+
* another request already made, or by one this request starts.
|
|
260
|
+
*/
|
|
261
|
+
async renew(token) {
|
|
262
|
+
if (this.auth !== token)
|
|
263
|
+
return;
|
|
264
|
+
await this.refresh();
|
|
265
|
+
}
|
|
166
266
|
async json(method, path, body, raw, contentType) {
|
|
167
|
-
const response = await this.
|
|
168
|
-
if (!response.ok)
|
|
169
|
-
throw await refusal(response);
|
|
267
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
170
268
|
const text = await response.text();
|
|
171
269
|
return (text ? JSON.parse(text) : {});
|
|
172
270
|
}
|
|
@@ -235,12 +333,44 @@ export class CalimeroClient {
|
|
|
235
333
|
this.useSession(session);
|
|
236
334
|
return session;
|
|
237
335
|
}
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Swap the refresh token for a new session, on whichever node the client
|
|
338
|
+
* uses: any node of the cluster takes it. A refresh token is used once, so
|
|
339
|
+
* calls made while one refresh is under way share it rather than sending the
|
|
340
|
+
* same token again (which would revoke the session).
|
|
341
|
+
*/
|
|
342
|
+
refresh() {
|
|
343
|
+
if (!this.refreshing) {
|
|
344
|
+
const pending = this.exchangeRefresh();
|
|
345
|
+
this.refreshing = pending;
|
|
346
|
+
const clear = () => {
|
|
347
|
+
if (this.refreshing === pending)
|
|
348
|
+
this.refreshing = undefined;
|
|
349
|
+
};
|
|
350
|
+
pending.then(clear, clear);
|
|
351
|
+
}
|
|
352
|
+
return this.refreshing;
|
|
353
|
+
}
|
|
354
|
+
async exchangeRefresh() {
|
|
355
|
+
const current = this.current;
|
|
356
|
+
if (!current)
|
|
241
357
|
throw new Error('no session to refresh');
|
|
242
|
-
|
|
243
|
-
|
|
358
|
+
let session;
|
|
359
|
+
// A node that may not serve the account answers `421 WRONG_NODE` before it
|
|
360
|
+
// spends the refresh token (core #4774), so the same token goes to the next
|
|
361
|
+
// node (`send` moved there already): once per node, at most.
|
|
362
|
+
for (let tried = 1; session === undefined; tried++) {
|
|
363
|
+
try {
|
|
364
|
+
session = await this.post('auth/refresh', { refreshToken: current.refreshToken });
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length)
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// A sign-in or another session taken while this one was refreshing wins.
|
|
372
|
+
if (this.current === current)
|
|
373
|
+
this.useSession(session);
|
|
244
374
|
return session;
|
|
245
375
|
}
|
|
246
376
|
async signOut() {
|
|
@@ -285,7 +415,7 @@ export class CalimeroClient {
|
|
|
285
415
|
*/
|
|
286
416
|
async attestation() {
|
|
287
417
|
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
288
|
-
const response = await this.raw(`${this.
|
|
418
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce}`);
|
|
289
419
|
if (!response.ok)
|
|
290
420
|
throw await refusal(response);
|
|
291
421
|
return (await response.json());
|
|
@@ -314,9 +444,7 @@ export class CalimeroClient {
|
|
|
314
444
|
}
|
|
315
445
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
316
446
|
async downloadBlob(app, id) {
|
|
317
|
-
const response = await this.
|
|
318
|
-
if (!response.ok)
|
|
319
|
-
throw await refusal(response);
|
|
447
|
+
const response = await this.send('GET', `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
320
448
|
return new Uint8Array(await response.arrayBuffer());
|
|
321
449
|
}
|
|
322
450
|
/** `app`'s blobs. */
|
|
@@ -489,12 +617,7 @@ export class CalimeroClient {
|
|
|
489
617
|
}
|
|
490
618
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
491
619
|
async *stream(path, signal) {
|
|
492
|
-
const
|
|
493
|
-
if (this.auth)
|
|
494
|
-
headers.authorization = `Bearer ${this.auth}`;
|
|
495
|
-
const response = await this.sealed(`${this.base}/${path}`, { headers, signal });
|
|
496
|
-
if (!response.ok)
|
|
497
|
-
throw await refusal(response);
|
|
620
|
+
const response = await this.send('GET', path, undefined, undefined, undefined, signal);
|
|
498
621
|
if (!response.body)
|
|
499
622
|
throw new CalimeroError(response.status, 'EVENTS', 'the event stream has no body');
|
|
500
623
|
yield* sseFrames(response.body);
|
|
@@ -19,10 +19,35 @@ export declare class CalimeroError extends Error {
|
|
|
19
19
|
readonly details: Record<string, unknown>;
|
|
20
20
|
constructor(status: number, code: string, message: string, details?: Record<string, unknown>);
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* `401 UNAUTHENTICATED` that refreshing could not answer: sign in (again) and
|
|
24
|
+
* repeat the request.
|
|
25
|
+
*
|
|
26
|
+
* A session token is signed by the node that issued it, and other nodes take
|
|
27
|
+
* it only while they can vouch for that node's signing key: a `401` often
|
|
28
|
+
* means only that the issuer was replaced or cannot be reached, not that the
|
|
29
|
+
* user signed out. The client answers that itself when it holds a refresh
|
|
30
|
+
* token: it refreshes once (any node takes the refresh) and repeats the
|
|
31
|
+
* request. This error reaches a caller when there is no refresh token (an API
|
|
32
|
+
* key, say), or the refresh was refused.
|
|
33
|
+
*/
|
|
23
34
|
export declare class LoginRequired extends CalimeroError {
|
|
24
35
|
constructor(message: string, details?: Record<string, unknown>);
|
|
25
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* `421 WRONG_NODE`: this node runs a mero-tee release the account's owners have
|
|
39
|
+
* not approved, so it neither issues nor takes the account's sessions. Use
|
|
40
|
+
* another node of the cluster; refreshing here would not help.
|
|
41
|
+
*
|
|
42
|
+
* The client moves to the next node it was given (`nodes`) and repeats the
|
|
43
|
+
* request once by itself (a refresh: once per node). This error reaches a
|
|
44
|
+
* caller when it knows no other node, when the next one refused too (for a
|
|
45
|
+
* refresh, every node), or after a sign-in (which needs the passkey again:
|
|
46
|
+
* repeat it, and it goes to the next node).
|
|
47
|
+
*/
|
|
48
|
+
export declare class WrongNode extends CalimeroError {
|
|
49
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
50
|
+
}
|
|
26
51
|
/**
|
|
27
52
|
* `404 NOT_FOUND`: what the request names does not exist for this caller. For
|
|
28
53
|
* an app this does not say whether it exists: an account the app does not
|
package/dist/cloud-v2/errors.js
CHANGED
|
@@ -22,13 +22,41 @@ export class CalimeroError extends Error {
|
|
|
22
22
|
this.name = 'CalimeroError';
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* `401 UNAUTHENTICATED` that refreshing could not answer: sign in (again) and
|
|
27
|
+
* repeat the request.
|
|
28
|
+
*
|
|
29
|
+
* A session token is signed by the node that issued it, and other nodes take
|
|
30
|
+
* it only while they can vouch for that node's signing key: a `401` often
|
|
31
|
+
* means only that the issuer was replaced or cannot be reached, not that the
|
|
32
|
+
* user signed out. The client answers that itself when it holds a refresh
|
|
33
|
+
* token: it refreshes once (any node takes the refresh) and repeats the
|
|
34
|
+
* request. This error reaches a caller when there is no refresh token (an API
|
|
35
|
+
* key, say), or the refresh was refused.
|
|
36
|
+
*/
|
|
26
37
|
export class LoginRequired extends CalimeroError {
|
|
27
38
|
constructor(message, details = {}) {
|
|
28
39
|
super(401, 'UNAUTHENTICATED', message, details);
|
|
29
40
|
this.name = 'LoginRequired';
|
|
30
41
|
}
|
|
31
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* `421 WRONG_NODE`: this node runs a mero-tee release the account's owners have
|
|
45
|
+
* not approved, so it neither issues nor takes the account's sessions. Use
|
|
46
|
+
* another node of the cluster; refreshing here would not help.
|
|
47
|
+
*
|
|
48
|
+
* The client moves to the next node it was given (`nodes`) and repeats the
|
|
49
|
+
* request once by itself (a refresh: once per node). This error reaches a
|
|
50
|
+
* caller when it knows no other node, when the next one refused too (for a
|
|
51
|
+
* refresh, every node), or after a sign-in (which needs the passkey again:
|
|
52
|
+
* repeat it, and it goes to the next node).
|
|
53
|
+
*/
|
|
54
|
+
export class WrongNode extends CalimeroError {
|
|
55
|
+
constructor(message, details = {}) {
|
|
56
|
+
super(421, 'WRONG_NODE', message, details);
|
|
57
|
+
this.name = 'WrongNode';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
32
60
|
/**
|
|
33
61
|
* `404 NOT_FOUND`: what the request names does not exist for this caller. For
|
|
34
62
|
* an app this does not say whether it exists: an account the app does not
|
|
@@ -174,6 +202,8 @@ export function errorFrom(status, body, retryAfterHeader) {
|
|
|
174
202
|
switch (`${status} ${code}`) {
|
|
175
203
|
case '401 UNAUTHENTICATED':
|
|
176
204
|
return new LoginRequired(message, details);
|
|
205
|
+
case '421 WRONG_NODE':
|
|
206
|
+
return new WrongNode(message, details);
|
|
177
207
|
case '404 NOT_FOUND':
|
|
178
208
|
return new NotFound(message, details);
|
|
179
209
|
case '409 IDEMPOTENCY_MISMATCH':
|
package/dist/cloud-v2/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CalimeroClient } from './client.js';
|
|
2
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, errorFrom, } from './errors.js';
|
|
2
|
+
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
3
3
|
export type { Committed } from './errors.js';
|
|
4
4
|
export type { AppInfo, BlobInfo, CallResult, CalimeroClientOptions, CloudEvent, CalimeroSession, PackageInfo, PackageInstalled, CloudRetryOptions, OrgInfo, OrgUpgradeTarget, PresenceEntry, UpgradeRun, ViewResult, } from './client.js';
|
|
5
5
|
export { claimedRelease, fetchAttestedKey, MOCK_QUOTE_HEADER } from './attestation.js';
|
package/dist/cloud-v2/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CalimeroClient } from './client.js';
|
|
2
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, errorFrom, } from './errors.js';
|
|
2
|
+
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, ReleaseDeadlineError, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
3
3
|
export { claimedRelease, fetchAttestedKey, MOCK_QUOTE_HEADER } from './attestation.js';
|
|
4
4
|
export { browserPasskey, b64url, fromB64url } from './passkey.js';
|
|
5
5
|
export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
|