@calimero-network/mero-js 27.0.2 → 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 +10 -4
- package/dist/cloud-v2/approvals.d.ts +344 -0
- package/dist/cloud-v2/approvals.js +170 -0
- package/dist/cloud-v2/capabilities.d.ts +8 -1
- package/dist/cloud-v2/capabilities.js +8 -1
- package/dist/cloud-v2/client.d.ts +180 -11
- package/dist/cloud-v2/client.js +299 -33
- package/dist/cloud-v2/errors.d.ts +49 -1
- package/dist/cloud-v2/errors.js +62 -1
- package/dist/cloud-v2/index.d.ts +4 -2
- package/dist/cloud-v2/index.js +2 -1
- package/dist/index.browser.mjs +7 -3
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +431 -30
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +431 -30
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/dist/cloud-v2/client.js
CHANGED
|
@@ -11,9 +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, WrongNode, errorFrom, } from './errors.js';
|
|
17
18
|
/** One URL path segment, so an id cannot reach another route. */
|
|
18
19
|
function seg(value) {
|
|
19
20
|
return encodeURIComponent(value);
|
|
@@ -102,11 +103,27 @@ export const DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2000 };
|
|
|
102
103
|
export function retryDelayMs(retry, options) {
|
|
103
104
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
104
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
|
+
}
|
|
105
116
|
export class CalimeroClient {
|
|
106
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();
|
|
107
122
|
/** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
|
|
108
123
|
this.networkFailures = new WeakSet();
|
|
109
|
-
|
|
124
|
+
const trim = (url) => url.replace(/\/+$/, '');
|
|
125
|
+
this.nodeUrls = [...new Set([options.baseUrl, ...(options.nodes ?? [])].map(trim))];
|
|
126
|
+
this.policy = options.policy;
|
|
110
127
|
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
111
128
|
const given = options.fetch ?? ((input, init) => fetch(input, init));
|
|
112
129
|
// `fetch` rejects with a `TypeError` when it gets no answer: the connection
|
|
@@ -124,11 +141,35 @@ export class CalimeroClient {
|
|
|
124
141
|
}
|
|
125
142
|
};
|
|
126
143
|
this.raw = raw;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
173
|
}
|
|
133
174
|
/** Authenticate every request with an API key. */
|
|
134
175
|
useApiKey(key) {
|
|
@@ -142,7 +183,11 @@ export class CalimeroClient {
|
|
|
142
183
|
get session() {
|
|
143
184
|
return this.current;
|
|
144
185
|
}
|
|
145
|
-
|
|
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) {
|
|
146
191
|
const headers = {};
|
|
147
192
|
if (this.auth)
|
|
148
193
|
headers.authorization = `Bearer ${this.auth}`;
|
|
@@ -155,17 +200,71 @@ export class CalimeroClient {
|
|
|
155
200
|
headers['content-type'] = 'application/json';
|
|
156
201
|
payload = JSON.stringify(body);
|
|
157
202
|
}
|
|
203
|
+
const url = this.nodeUrl;
|
|
158
204
|
try {
|
|
159
|
-
return await this.
|
|
205
|
+
return await this.channel(url)(`${url}/${path}`, { method, headers, body: payload, signal });
|
|
160
206
|
}
|
|
161
207
|
catch (error) {
|
|
162
208
|
throw sealedError(error);
|
|
163
209
|
}
|
|
164
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
|
+
}
|
|
165
266
|
async json(method, path, body, raw, contentType) {
|
|
166
|
-
const response = await this.
|
|
167
|
-
if (!response.ok)
|
|
168
|
-
throw await refusal(response);
|
|
267
|
+
const response = await this.send(method, path, body, raw, contentType);
|
|
169
268
|
const text = await response.text();
|
|
170
269
|
return (text ? JSON.parse(text) : {});
|
|
171
270
|
}
|
|
@@ -234,12 +333,44 @@ export class CalimeroClient {
|
|
|
234
333
|
this.useSession(session);
|
|
235
334
|
return session;
|
|
236
335
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
240
357
|
throw new Error('no session to refresh');
|
|
241
|
-
|
|
242
|
-
|
|
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);
|
|
243
374
|
return session;
|
|
244
375
|
}
|
|
245
376
|
async signOut() {
|
|
@@ -284,7 +415,7 @@ export class CalimeroClient {
|
|
|
284
415
|
*/
|
|
285
416
|
async attestation() {
|
|
286
417
|
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
287
|
-
const response = await this.raw(`${this.
|
|
418
|
+
const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce}`);
|
|
288
419
|
if (!response.ok)
|
|
289
420
|
throw await refusal(response);
|
|
290
421
|
return (await response.json());
|
|
@@ -313,9 +444,7 @@ export class CalimeroClient {
|
|
|
313
444
|
}
|
|
314
445
|
/** One of `app`'s blobs, by id (base58 or hex). */
|
|
315
446
|
async downloadBlob(app, id) {
|
|
316
|
-
const response = await this.
|
|
317
|
-
if (!response.ok)
|
|
318
|
-
throw await refusal(response);
|
|
447
|
+
const response = await this.send('GET', `apps/${seg(app)}/blobs/${seg(id)}`);
|
|
319
448
|
return new Uint8Array(await response.arrayBuffer());
|
|
320
449
|
}
|
|
321
450
|
/** `app`'s blobs. */
|
|
@@ -399,8 +528,11 @@ export class CalimeroClient {
|
|
|
399
528
|
* Move an app to another release of its package (`{ version }`), another
|
|
400
529
|
* package, or a bundle hash, running its migration. Answers the upgrade's
|
|
401
530
|
* entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
|
|
402
|
-
* Where
|
|
403
|
-
* (`412 CONFIRM_REQUIRED`), which
|
|
531
|
+
* Where owners hold app upgrades, the node asks for the approval of
|
|
532
|
+
* whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
|
|
533
|
+
* `passkey` gives. Refused (`403`) to a session opened on an app's
|
|
534
|
+
* frontend: an app asks its user with {@link requestConsent} instead. Past
|
|
535
|
+
* a release deadline its approver has not met: {@link ReleaseDeadlineError}.
|
|
404
536
|
*/
|
|
405
537
|
upgradeApp(app, target, passkey) {
|
|
406
538
|
const path = `apps/${seg(app)}/upgrade`;
|
|
@@ -485,12 +617,7 @@ export class CalimeroClient {
|
|
|
485
617
|
}
|
|
486
618
|
/** A server-sent event stream on `path`, sealed like every other answer. */
|
|
487
619
|
async *stream(path, signal) {
|
|
488
|
-
const
|
|
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);
|
|
620
|
+
const response = await this.send('GET', path, undefined, undefined, undefined, signal);
|
|
494
621
|
if (!response.body)
|
|
495
622
|
throw new CalimeroError(response.status, 'EVENTS', 'the event stream has no body');
|
|
496
623
|
yield* sseFrames(response.body);
|
|
@@ -578,8 +705,11 @@ export class CalimeroClient {
|
|
|
578
705
|
/**
|
|
579
706
|
* Approve a mero-tee release to hold the apps of `org`, or of this account
|
|
580
707
|
* outside any organization (`org` `null`); `approved: false` withdraws it.
|
|
581
|
-
* Until then, nodes on that release get none of those apps' data.
|
|
582
|
-
* approves with `passkey
|
|
708
|
+
* Until then, nodes on that release get none of those apps' data. For an
|
|
709
|
+
* organization, a member holding `APPROVE_RELEASES` approves with `passkey`;
|
|
710
|
+
* the node binds the release's measurements itself. Refused (`403`) to a
|
|
711
|
+
* session opened on an app's frontend: an app asks its user with
|
|
712
|
+
* {@link requestConsent} instead.
|
|
583
713
|
*/
|
|
584
714
|
async approveRelease(org, version, passkey, approved = true) {
|
|
585
715
|
const path = org === null ? 'auth/releases' : `orgs/${seg(org)}/releases`;
|
|
@@ -590,12 +720,148 @@ export class CalimeroClient {
|
|
|
590
720
|
* `org`'s apps or this account's own (`org` `null`). `prefixes` `'manifest'`
|
|
591
721
|
* (the default) approves every prefix the package's latest release declares;
|
|
592
722
|
* `replace` sets exactly `prefixes`, which is how one is revoked. Approved
|
|
593
|
-
* with `passkey`.
|
|
723
|
+
* with `passkey`. Refused (`403`) to a session opened on an app's frontend:
|
|
724
|
+
* an app asks with {@link requestConsent} (`kind: 'outbound'`) instead.
|
|
594
725
|
*/
|
|
595
726
|
async approveOutbound(org, pkg, passkey, options = {}) {
|
|
596
727
|
const path = org === null ? 'auth/outbound' : `orgs/${seg(org)}/outbound`;
|
|
597
728
|
return this.postConfirmed(path, { package: pkg, prefixes: options.prefixes ?? 'manifest', replace: options.replace ?? false }, passkey);
|
|
598
729
|
}
|
|
730
|
+
// ---------------------------------------------------------------- approvals
|
|
731
|
+
/**
|
|
732
|
+
* One page of the report of a scope's apps: an organization's (any member;
|
|
733
|
+
* `404` to anyone else) or, with `org` `null`, the account's own outside any
|
|
734
|
+
* organization (a session bound to an app's frontend: that package's apps).
|
|
735
|
+
* The governance, the scope's approved releases, the releases the cluster's
|
|
736
|
+
* nodes run and whether each is approved, and per app its release, newer
|
|
737
|
+
* releases it may move to, last upgrade and replicas.
|
|
738
|
+
*/
|
|
739
|
+
appsReport(org, page = {}) {
|
|
740
|
+
const path = org === null ? 'auth/apps' : `orgs/${seg(org)}/apps`;
|
|
741
|
+
return this.get(`${path}${pageQuery(page)}`);
|
|
742
|
+
}
|
|
743
|
+
/** {@link appsReport}, every page, with the apps of all of them in `apps`. */
|
|
744
|
+
async appsReportAll(org) {
|
|
745
|
+
const first = await this.appsReport(org);
|
|
746
|
+
const apps = [...first.apps];
|
|
747
|
+
let next = first.next;
|
|
748
|
+
while (next !== null) {
|
|
749
|
+
const page = await this.appsReport(org, { after: next });
|
|
750
|
+
apps.push(...page.apps);
|
|
751
|
+
next = page.next;
|
|
752
|
+
}
|
|
753
|
+
return { ...first, apps, next: null };
|
|
754
|
+
}
|
|
755
|
+
/** `GET /releases`: the cluster's governance and every release deadline. Public. */
|
|
756
|
+
releaseRules() {
|
|
757
|
+
return this.get('releases');
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* What waits for a decision in a scope (`org`, or the account's own apps
|
|
761
|
+
* with `null`): mero-tee releases the cluster's nodes run that the scope has
|
|
762
|
+
* not approved, apps with newer releases of their package, and whether a
|
|
763
|
+
* passed deadline stops new work. Read over the sealed channel; act on an
|
|
764
|
+
* item by sending the user to the consent page ({@link requestConsent},
|
|
765
|
+
* `releaseConsent`, `upgradeConsent`).
|
|
766
|
+
*/
|
|
767
|
+
async pendingApprovals(org = null) {
|
|
768
|
+
const [report, rules] = await Promise.all([this.appsReportAll(org), this.releaseRules()]);
|
|
769
|
+
return pendingApprovalsFrom(report, rules);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Ask the node to ask the user: a release, upgrade or outbound approval
|
|
773
|
+
* that only the account holder may make, decided on the node's own consent
|
|
774
|
+
* page, never in the app. Needs a passkey session (a session bound to the
|
|
775
|
+
* app's frontend is fine). Send the person to the answer's `url`; the page
|
|
776
|
+
* sends them back to `returnTo?request_id=…&result=approved|declined`
|
|
777
|
+
* (`consentReturn`), and {@link consentResult} confirms the decision.
|
|
778
|
+
*/
|
|
779
|
+
requestConsent(request) {
|
|
780
|
+
return this.post('consent/requests', request);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* {@link requestConsent}, then send the browser to the consent page
|
|
784
|
+
* (`navigate`, by default `window.location.assign`). Answers the request
|
|
785
|
+
* once `navigate` returns: with a popup (`url => window.open(url)`), poll it
|
|
786
|
+
* with {@link waitForConsent}.
|
|
787
|
+
*/
|
|
788
|
+
async openConsentPage(request, navigate = (url) => {
|
|
789
|
+
const w = globalThis.window;
|
|
790
|
+
if (!w)
|
|
791
|
+
throw new Error('no window to open the consent page in; pass `navigate`');
|
|
792
|
+
w.location.assign(url);
|
|
793
|
+
}) {
|
|
794
|
+
const started = await this.requestConsent(request);
|
|
795
|
+
navigate(started.url);
|
|
796
|
+
return started;
|
|
797
|
+
}
|
|
798
|
+
/** The decision on a consent request (any node answers, for the account that asked). */
|
|
799
|
+
consentResult(requestId) {
|
|
800
|
+
return this.get(`consent/${seg(requestId)}/result`);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Poll {@link consentResult} until the request is decided, every
|
|
804
|
+
* `intervalMs` (2 s), until `signal` aborts or `expiresAt` (Unix seconds)
|
|
805
|
+
* passes, when it answers the last `pending` result.
|
|
806
|
+
*/
|
|
807
|
+
async waitForConsent(requestId, options = {}) {
|
|
808
|
+
const interval = options.intervalMs ?? 2000;
|
|
809
|
+
for (;;) {
|
|
810
|
+
const result = await this.consentResult(requestId);
|
|
811
|
+
if (result.status !== 'pending')
|
|
812
|
+
return result;
|
|
813
|
+
if (options.signal?.aborted)
|
|
814
|
+
return result;
|
|
815
|
+
if (options.expiresAt !== undefined && Date.now() / 1000 >= options.expiresAt)
|
|
816
|
+
return result;
|
|
817
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/** `GET /publishers/{signerId}`: the cluster name and binding generation a key signs to bind it next. */
|
|
821
|
+
publisherBinding(signerId) {
|
|
822
|
+
return this.get(`publishers/${seg(signerId)}`);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Bind a package signing key to `org`, which then approves the node images
|
|
826
|
+
* every instance of the key's packages runs on, and upgrades those whose
|
|
827
|
+
* package declares `upgrades: publisher`. `proof` is the key's signature
|
|
828
|
+
* over `publisherBindingMessage(cluster, org, signerId, generation)`; a
|
|
829
|
+
* member holding `APPROVE_RELEASES` sends it with `passkey`.
|
|
830
|
+
*/
|
|
831
|
+
bindPublisher(org, proof, passkey) {
|
|
832
|
+
return this.postConfirmed(`orgs/${seg(org)}/publishers`, { ...proof }, passkey);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Upgrade every instance of `pkg` whose release declares `upgrades:
|
|
836
|
+
* publisher`, whoever owns it, as its publisher `org` (a
|
|
837
|
+
* `MANAGE_APPLICATION` holder there), with one passkey approval. Answers
|
|
838
|
+
* the run, kept on the publisher.
|
|
839
|
+
*/
|
|
840
|
+
upgradePublished(org, pkg, target, passkey) {
|
|
841
|
+
return this.postConfirmed(`orgs/${seg(org)}/packages/${seg(pkg)}/upgrade`, { ...target }, passkey);
|
|
842
|
+
}
|
|
843
|
+
// ---------------------------------------------------------------- operator
|
|
844
|
+
/** `GET /ops/apps`: what every app runs, without names or accounts. Operator token (`useApiKey`). */
|
|
845
|
+
opsApps(page = {}) {
|
|
846
|
+
return this.get(`ops/apps${pageQuery(page)}`);
|
|
847
|
+
}
|
|
848
|
+
/** `GET /ops/orgs`: each organization's approved releases. Operator token. */
|
|
849
|
+
opsOrgs(page = {}) {
|
|
850
|
+
return this.get(`ops/orgs${pageQuery(page)}`);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Announce when work on images older than `version` ends: from `after` on,
|
|
854
|
+
* a scope that has not approved it (or newer) starts no new work. Operator
|
|
855
|
+
* token.
|
|
856
|
+
*/
|
|
857
|
+
async setReleaseDeadline(version, after) {
|
|
858
|
+
const at = typeof after === 'string' ? after : after.toISOString();
|
|
859
|
+
await this.post(`ops/releases/${seg(version)}/deadline`, { after: at });
|
|
860
|
+
}
|
|
861
|
+
/** Lift a release's deadline. Operator token. */
|
|
862
|
+
async liftReleaseDeadline(version) {
|
|
863
|
+
await this.delete(`ops/releases/${seg(version)}/deadline`);
|
|
864
|
+
}
|
|
599
865
|
/** Add a member, or change their role. */
|
|
600
866
|
async setMember(org, account, role) {
|
|
601
867
|
await this.post(`orgs/${seg(org)}/members`, { account, role });
|
|
@@ -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
|
|
@@ -52,6 +77,29 @@ export declare class ArgsTooLarge extends CalimeroError {
|
|
|
52
77
|
export declare class IdempotencyMismatch extends CalimeroError {
|
|
53
78
|
constructor(message: string, details?: Record<string, unknown>);
|
|
54
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* `409 RELEASE_DEADLINE`: a mero-tee release's deadline has passed and the
|
|
82
|
+
* set that governs this work (the app's publisher's, or its owner's) has not
|
|
83
|
+
* approved it or a newer one, so new work (creating an app, installing a
|
|
84
|
+
* release, upgrading an app) is refused. Apps already running keep running.
|
|
85
|
+
* Approving `release` lifts it at once.
|
|
86
|
+
*/
|
|
87
|
+
export declare class ReleaseDeadlineError extends CalimeroError {
|
|
88
|
+
/** The release to approve. */
|
|
89
|
+
readonly release: string;
|
|
90
|
+
/** Its deadline, RFC 3339 (`''` when the node did not say). */
|
|
91
|
+
readonly after: string;
|
|
92
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `409 CONSENT_STALE`: what a consent page showed (the app's bundle, the
|
|
96
|
+
* target release, the release's measurements) changed before the person
|
|
97
|
+
* approved. Nothing was approved; ask for a new consent request so they see
|
|
98
|
+
* the new facts.
|
|
99
|
+
*/
|
|
100
|
+
export declare class ConsentStale extends CalimeroError {
|
|
101
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
102
|
+
}
|
|
55
103
|
/**
|
|
56
104
|
* `422 APP_ERROR`: the app refused the call. `appCode` is the app's declared
|
|
57
105
|
* error (`Taken`, `Forbidden`, ...) and `fields` its named fields. The call was
|
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
|
|
@@ -73,6 +101,33 @@ export class IdempotencyMismatch extends CalimeroError {
|
|
|
73
101
|
this.name = 'IdempotencyMismatch';
|
|
74
102
|
}
|
|
75
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* `409 RELEASE_DEADLINE`: a mero-tee release's deadline has passed and the
|
|
106
|
+
* set that governs this work (the app's publisher's, or its owner's) has not
|
|
107
|
+
* approved it or a newer one, so new work (creating an app, installing a
|
|
108
|
+
* release, upgrading an app) is refused. Apps already running keep running.
|
|
109
|
+
* Approving `release` lifts it at once.
|
|
110
|
+
*/
|
|
111
|
+
export class ReleaseDeadlineError extends CalimeroError {
|
|
112
|
+
constructor(message, details = {}) {
|
|
113
|
+
super(409, 'RELEASE_DEADLINE', message, details);
|
|
114
|
+
this.name = 'ReleaseDeadlineError';
|
|
115
|
+
this.release = typeof details.release === 'string' ? details.release : '';
|
|
116
|
+
this.after = typeof details.after === 'string' ? details.after : '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `409 CONSENT_STALE`: what a consent page showed (the app's bundle, the
|
|
121
|
+
* target release, the release's measurements) changed before the person
|
|
122
|
+
* approved. Nothing was approved; ask for a new consent request so they see
|
|
123
|
+
* the new facts.
|
|
124
|
+
*/
|
|
125
|
+
export class ConsentStale extends CalimeroError {
|
|
126
|
+
constructor(message, details = {}) {
|
|
127
|
+
super(409, 'CONSENT_STALE', message, details);
|
|
128
|
+
this.name = 'ConsentStale';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
76
131
|
/**
|
|
77
132
|
* `422 APP_ERROR`: the app refused the call. `appCode` is the app's declared
|
|
78
133
|
* error (`Taken`, `Forbidden`, ...) and `fields` its named fields. The call was
|
|
@@ -147,10 +202,16 @@ export function errorFrom(status, body, retryAfterHeader) {
|
|
|
147
202
|
switch (`${status} ${code}`) {
|
|
148
203
|
case '401 UNAUTHENTICATED':
|
|
149
204
|
return new LoginRequired(message, details);
|
|
205
|
+
case '421 WRONG_NODE':
|
|
206
|
+
return new WrongNode(message, details);
|
|
150
207
|
case '404 NOT_FOUND':
|
|
151
208
|
return new NotFound(message, details);
|
|
152
209
|
case '409 IDEMPOTENCY_MISMATCH':
|
|
153
210
|
return new IdempotencyMismatch(message, details);
|
|
211
|
+
case '409 RELEASE_DEADLINE':
|
|
212
|
+
return new ReleaseDeadlineError(message, details);
|
|
213
|
+
case '409 CONSENT_STALE':
|
|
214
|
+
return new ConsentStale(message, details);
|
|
154
215
|
case '412 CONFIRM_REQUIRED':
|
|
155
216
|
return new ConfirmRequired(message, details);
|
|
156
217
|
case '413 ARGS_TOO_LARGE':
|
package/dist/cloud-v2/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { CalimeroClient } from './client.js';
|
|
2
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, NotFound, 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
|
-
export type { AppInfo, BlobInfo, CallResult, CalimeroClientOptions, CloudEvent, CalimeroSession, PackageInfo, PackageInstalled, CloudRetryOptions, OrgUpgradeTarget, PresenceEntry, UpgradeRun, ViewResult, } from './client.js';
|
|
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';
|
|
6
6
|
export type { AttestationDoc, CloudPolicy, CloudReleasePolicy } from './attestation.js';
|
|
7
7
|
export { browserPasskey, b64url, fromB64url } from './passkey.js';
|
|
8
8
|
export type { AssertionJSON, PasskeyProvider, RegistrationJSON } from './passkey.js';
|
|
9
|
+
export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
|
|
10
|
+
export type { ActorKind, AppsReport, Authority, ClusterRelease, ConsentReleaseModel, ConsentRequest, ConsentResult, ConsentScope, ConsentStarted, ConsentStatus, Governance, LastUpgrade, OpsAppsReport, OpsOrgsReport, PageOptions, PendingApprovals, PendingRelease, PendingUpgrade, PublisherBindingInfo, PublisherProof, ReleaseDeadline, ReleaseRules, ReportScope, ReportedApp, ReportedBundle, ReportedRelease, ReportedReplica, UpgradeAuthority, } from './approvals.js';
|
|
9
11
|
export { DEFAULT_ORG_CAPABILITIES, ORG_CAPABILITIES, orgCapabilities } from './capabilities.js';
|
|
10
12
|
export type { OrgCapability } from './capabilities.js';
|
package/dist/cloud-v2/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { CalimeroClient } from './client.js';
|
|
2
|
-
export { AppError, ArgsTooLarge, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, NotFound, 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
|
+
export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
|
|
5
6
|
export { DEFAULT_ORG_CAPABILITIES, ORG_CAPABILITIES, orgCapabilities } from './capabilities.js';
|