@calimero-network/mero-js 26.11.0 → 27.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -288
- package/dist/cloud-v2/attestation.d.ts +91 -0
- package/dist/cloud-v2/attestation.js +135 -0
- package/dist/cloud-v2/capabilities.d.ts +30 -0
- package/dist/cloud-v2/capabilities.js +31 -0
- package/dist/cloud-v2/client.d.ts +426 -0
- package/dist/cloud-v2/client.js +646 -0
- package/dist/cloud-v2/errors.d.ts +98 -0
- package/dist/cloud-v2/errors.js +165 -0
- package/dist/cloud-v2/index.d.ts +10 -0
- package/dist/cloud-v2/index.js +5 -0
- package/dist/cloud-v2/passkey.d.ts +27 -0
- package/dist/cloud-v2/passkey.js +73 -0
- package/dist/crypto/internal.d.ts +3 -37
- package/dist/crypto/internal.js +3 -72
- package/dist/index.browser.mjs +4 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1750 -12772
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +3 -56
- package/dist/index.js +14 -75
- package/dist/index.mjs +1750 -12772
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +4 -4
- package/dist/sealed/index.js +2 -2
- package/dist/sealed/release.d.ts +21 -76
- package/dist/sealed/release.js +24 -108
- package/dist/sealed/sealed.d.ts +8 -56
- package/dist/sealed/sealed.js +11 -89
- package/dist/sealed/verify.d.ts +7 -13
- package/dist/sealed/verify.js +3 -7
- package/package.json +8 -13
- package/dist/account/account-admin.d.ts +0 -80
- package/dist/account/account-admin.js +0 -346
- package/dist/account/account.d.ts +0 -196
- package/dist/account/account.js +0 -269
- package/dist/account/application-id.d.ts +0 -50
- package/dist/account/application-id.js +0 -83
- package/dist/account/bootstrap-from-invitation.d.ts +0 -110
- package/dist/account/bootstrap-from-invitation.js +0 -80
- package/dist/account/create-context.d.ts +0 -103
- package/dist/account/create-context.js +0 -267
- package/dist/account/govern.d.ts +0 -24
- package/dist/account/govern.js +0 -50
- package/dist/account/index.d.ts +0 -26
- package/dist/account/index.js +0 -15
- package/dist/account/join-as-account.d.ts +0 -23
- package/dist/account/join-as-account.js +0 -34
- package/dist/account/join-with-node.d.ts +0 -115
- package/dist/account/join-with-node.js +0 -161
- package/dist/account/node-admin.d.ts +0 -20
- package/dist/account/node-admin.js +0 -33
- package/dist/account/relay-from-invitation.d.ts +0 -137
- package/dist/account/relay-from-invitation.js +0 -180
- package/dist/account/relay-login-error.d.ts +0 -32
- package/dist/account/relay-login-error.js +0 -34
- package/dist/account/session.d.ts +0 -266
- package/dist/account/session.js +0 -915
- package/dist/account/wordlist.d.ts +0 -16
- package/dist/account/wordlist.js +0 -273
- package/dist/admin-api/admin-client.d.ts +0 -688
- package/dist/admin-api/admin-client.js +0 -1230
- package/dist/admin-api/admin-factory.d.ts +0 -14
- package/dist/admin-api/admin-factory.js +0 -22
- package/dist/admin-api/admin-types.d.ts +0 -2046
- package/dist/admin-api/admin-types.js +0 -12
- package/dist/admin-api/index.d.ts +0 -4
- package/dist/admin-api/index.js +0 -5
- package/dist/admin-api/redeem-invitation.d.ts +0 -101
- package/dist/admin-api/redeem-invitation.js +0 -200
- package/dist/auth/index.d.ts +0 -34
- package/dist/auth/index.js +0 -67
- package/dist/auth-api/auth-client.d.ts +0 -31
- package/dist/auth-api/auth-client.js +0 -117
- package/dist/auth-api/auth-factory.d.ts +0 -7
- package/dist/auth-api/auth-factory.js +0 -41
- package/dist/auth-api/auth-types.d.ts +0 -130
- package/dist/auth-api/auth-types.js +0 -2
- package/dist/auth-api/index.d.ts +0 -3
- package/dist/auth-api/index.js +0 -4
- package/dist/capabilities.d.ts +0 -84
- package/dist/capabilities.js +0 -117
- package/dist/cloud/account-ownership.d.ts +0 -63
- package/dist/cloud/account-ownership.js +0 -87
- package/dist/cloud/cloud-client.d.ts +0 -949
- package/dist/cloud/cloud-client.js +0 -1068
- package/dist/cloud/connect.d.ts +0 -213
- package/dist/cloud/connect.js +0 -260
- package/dist/cloud/enrol-redirect.d.ts +0 -181
- package/dist/cloud/enrol-redirect.js +0 -244
- package/dist/cloud/index.d.ts +0 -12
- package/dist/cloud/index.js +0 -10
- package/dist/cloud/link-redirect.d.ts +0 -95
- package/dist/cloud/link-redirect.js +0 -121
- package/dist/cloud/link-state.d.ts +0 -36
- package/dist/cloud/link-state.js +0 -104
- package/dist/cloud/routing-proof.d.ts +0 -113
- package/dist/cloud/routing-proof.js +0 -96
- package/dist/device-cert/device-cert.d.ts +0 -189
- package/dist/device-cert/device-cert.js +0 -239
- package/dist/device-cert/device-scope.d.ts +0 -58
- package/dist/device-cert/device-scope.js +0 -76
- package/dist/device-cert/index.d.ts +0 -4
- package/dist/device-cert/index.js +0 -3
- package/dist/ephemeral/codec.d.ts +0 -4
- package/dist/ephemeral/codec.js +0 -12
- package/dist/ephemeral/index.d.ts +0 -57
- package/dist/ephemeral/index.js +0 -101
- package/dist/ephemeral/types.d.ts +0 -27
- package/dist/ephemeral/types.js +0 -1
- package/dist/errors/classify.d.ts +0 -65
- package/dist/errors/classify.js +0 -117
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -1
- package/dist/events/group.d.ts +0 -95
- package/dist/events/group.js +0 -10
- package/dist/events/index.d.ts +0 -5
- package/dist/events/index.js +0 -2
- package/dist/events/sse.d.ts +0 -127
- package/dist/events/sse.js +0 -469
- package/dist/events/ws.d.ts +0 -57
- package/dist/events/ws.js +0 -212
- package/dist/http-client/api-response.d.ts +0 -15
- package/dist/http-client/api-response.js +0 -1
- package/dist/http-client/http-factory.d.ts +0 -71
- package/dist/http-client/http-factory.js +0 -68
- package/dist/http-client/http-types.d.ts +0 -87
- package/dist/http-client/http-types.js +0 -1
- package/dist/http-client/index.d.ts +0 -7
- package/dist/http-client/index.js +0 -11
- package/dist/http-client/path-segment.d.ts +0 -1
- package/dist/http-client/path-segment.js +0 -8
- package/dist/http-client/retry.d.ts +0 -5
- package/dist/http-client/retry.js +0 -82
- package/dist/http-client/signal-utils.d.ts +0 -2
- package/dist/http-client/signal-utils.js +0 -41
- package/dist/http-client/web-client.d.ts +0 -80
- package/dist/http-client/web-client.js +0 -557
- package/dist/invitation/index.d.ts +0 -2
- package/dist/invitation/index.js +0 -2
- package/dist/invitation/invitation.d.ts +0 -106
- package/dist/invitation/invitation.js +0 -104
- package/dist/login/index.d.ts +0 -4
- package/dist/login/index.js +0 -3
- package/dist/login/login.d.ts +0 -111
- package/dist/login/login.js +0 -110
- package/dist/login/session.d.ts +0 -124
- package/dist/login/session.js +0 -141
- package/dist/mero-js.d.ts +0 -231
- package/dist/mero-js.js +0 -469
- package/dist/namespace-op/index.d.ts +0 -2
- package/dist/namespace-op/index.js +0 -3
- package/dist/namespace-op/namespace-op.d.ts +0 -150
- package/dist/namespace-op/namespace-op.js +0 -191
- package/dist/nodeDiscovery.d.ts +0 -47
- package/dist/nodeDiscovery.js +0 -101
- package/dist/owner-op/index.d.ts +0 -2
- package/dist/owner-op/index.js +0 -3
- package/dist/owner-op/owner-op.d.ts +0 -119
- package/dist/owner-op/owner-op.js +0 -214
- package/dist/presence/relay-presence.d.ts +0 -56
- package/dist/presence/relay-presence.js +0 -107
- package/dist/presence/statement.d.ts +0 -12
- package/dist/presence/statement.js +0 -17
- package/dist/relay/index.d.ts +0 -4
- package/dist/relay/index.js +0 -2
- package/dist/relay/nonce-source.d.ts +0 -72
- package/dist/relay/nonce-source.js +0 -111
- package/dist/relay/relay-client.d.ts +0 -472
- package/dist/relay/relay-client.js +0 -518
- package/dist/relay-attestation/attest-node-key.d.ts +0 -66
- package/dist/relay-attestation/attest-node-key.js +0 -110
- package/dist/relay-attestation/index.d.ts +0 -1
- package/dist/relay-attestation/index.js +0 -1
- package/dist/request/caller-proof.d.ts +0 -43
- package/dist/request/caller-proof.js +0 -36
- package/dist/request/index.d.ts +0 -3
- package/dist/request/index.js +0 -3
- package/dist/request/proof-signer.d.ts +0 -49
- package/dist/request/proof-signer.js +0 -59
- package/dist/request/request-sig.d.ts +0 -99
- package/dist/request/request-sig.js +0 -95
- package/dist/request-proof/authorizer.d.ts +0 -41
- package/dist/request-proof/authorizer.js +0 -60
- package/dist/request-proof/index.d.ts +0 -4
- package/dist/request-proof/index.js +0 -2
- package/dist/request-proof/request-proof.d.ts +0 -53
- package/dist/request-proof/request-proof.js +0 -95
- package/dist/rpc/index.d.ts +0 -64
- package/dist/rpc/index.js +0 -78
- package/dist/signer/index.d.ts +0 -2
- package/dist/signer/index.js +0 -3
- package/dist/signer/signer.d.ts +0 -92
- package/dist/signer/signer.js +0 -147
- package/dist/token-store/index.d.ts +0 -27
- package/dist/token-store/index.js +0 -62
- package/dist/transport/index.d.ts +0 -6
- package/dist/transport/index.js +0 -3
- package/dist/transport/mero-client.d.ts +0 -294
- package/dist/transport/mero-client.js +0 -386
- package/dist/transport/relay-observer.d.ts +0 -152
- package/dist/transport/relay-observer.js +0 -173
- package/dist/transport/relay-transport.d.ts +0 -73
- package/dist/transport/relay-transport.js +0 -94
- package/dist/transport/types.d.ts +0 -100
- package/dist/transport/types.js +0 -32
- package/dist/types/api-response.d.ts +0 -15
- package/dist/types/api-response.js +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/warrant/creation-warrant.d.ts +0 -163
- package/dist/warrant/creation-warrant.js +0 -194
- package/dist/warrant/governance-op.d.ts +0 -202
- package/dist/warrant/governance-op.js +0 -355
- package/dist/warrant/governance-warrant.d.ts +0 -124
- package/dist/warrant/governance-warrant.js +0 -164
- package/dist/warrant/index.d.ts +0 -8
- package/dist/warrant/index.js +0 -5
- package/dist/warrant/warrant.d.ts +0 -188
- 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
|
+
}
|