@shardflux/sdk 0.8.0 → 0.9.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/CHANGELOG.md +199 -0
- package/README.md +235 -6
- package/dist/account.d.ts +469 -0
- package/dist/account.js +620 -0
- package/dist/cell.d.ts +197 -8
- package/dist/cell.js +449 -31
- package/dist/client.d.ts +76 -5
- package/dist/client.js +114 -6
- package/dist/errors.d.ts +62 -3
- package/dist/errors.js +65 -1
- package/dist/executions.d.ts +120 -0
- package/dist/executions.js +99 -0
- package/dist/feedback.d.ts +67 -0
- package/dist/feedback.js +39 -0
- package/dist/generated/app-api.d.ts +12323 -8072
- package/dist/generated/cell-api.d.ts +463 -8
- package/dist/http.d.ts +7 -1
- package/dist/http.js +26 -7
- package/dist/index.d.ts +17 -6
- package/dist/index.js +5 -1
- package/dist/lifecycle.d.ts +27 -2
- package/dist/lifecycle.js +5 -0
- package/dist/progress.js +4 -2
- package/dist/templates.js +2 -2
- package/dist/tools.d.ts +28 -1
- package/dist/tools.js +153 -21
- package/dist/version-check.d.ts +101 -0
- package/dist/version-check.js +191 -0
- package/dist/workspace.d.ts +77 -5
- package/dist/workspace.js +164 -12
- package/package.json +1 -1
package/dist/account.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { BillingApi, WorkspacesApi } from "./client.js";
|
|
2
|
+
import { ShardfluxProtocolError } from "./errors.js";
|
|
3
|
+
import { HttpClient, SDK_VERSION, defaultFetch, defaultSleep, randomId } from "./http.js";
|
|
4
|
+
import { AuditApi, auditQuery } from "./audit.js";
|
|
5
|
+
import { CaptureRegistry } from "./capture.js";
|
|
6
|
+
import { EgressPolicyApi } from "./egress.js";
|
|
7
|
+
import { sendFeedback } from "./feedback.js";
|
|
8
|
+
import { SecretsApi } from "./secrets.js";
|
|
9
|
+
import { TemplatesApi } from "./templates.js";
|
|
10
|
+
import { UsageApi } from "./usage.js";
|
|
11
|
+
import { VolumesApi } from "./volumes.js";
|
|
12
|
+
import { DEFAULT_BASE_URL, versionCheckHook } from "./version-check.js";
|
|
13
|
+
/** The shape of a CLI session token. */
|
|
14
|
+
export const SESSION_TOKEN_PATTERN = /^sfu_[A-Za-z0-9_-]{43}$/;
|
|
15
|
+
/** True when `value` has the shape of a CLI session token (`sfu_` + 43 base64url characters). */
|
|
16
|
+
export function isSessionToken(value) {
|
|
17
|
+
return typeof value === 'string' && SESSION_TOKEN_PATTERN.test(value);
|
|
18
|
+
}
|
|
19
|
+
function tokenParam(query, name) {
|
|
20
|
+
for (const pair of query.split('&')) {
|
|
21
|
+
const eq = pair.indexOf('=');
|
|
22
|
+
const key = eq < 0 ? pair : pair.slice(0, eq);
|
|
23
|
+
if (key !== name)
|
|
24
|
+
continue;
|
|
25
|
+
const raw = eq < 0 ? '' : pair.slice(eq + 1);
|
|
26
|
+
let value;
|
|
27
|
+
try {
|
|
28
|
+
value = decodeURIComponent(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
value = raw;
|
|
32
|
+
}
|
|
33
|
+
return value.trim();
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The token of an emailed link (`<app>/auth/verify-email#token=...`, `/auth/reset-password#token=...`,
|
|
39
|
+
* `/auth/confirm-email-change#token=...`, `/invitations/accept#token=...`): the `token` of its `#` fragment, else of
|
|
40
|
+
* its `?` query, percent-decoded. Input that is not a link is the token itself (trimmed). Throws for an empty input
|
|
41
|
+
* and for a link without a token.
|
|
42
|
+
*/
|
|
43
|
+
export function parseEmailToken(input) {
|
|
44
|
+
const s = typeof input === 'string' ? input.trim() : '';
|
|
45
|
+
if (s.length === 0)
|
|
46
|
+
throw new Error('parseEmailToken: empty input; pass the link from the email or its token');
|
|
47
|
+
const hash = s.indexOf('#');
|
|
48
|
+
const q = s.indexOf('?');
|
|
49
|
+
const looksLikeLink = hash >= 0 || q >= 0 || /^[a-z][a-z0-9+.-]*:\/\//i.test(s) || s.includes('/');
|
|
50
|
+
if (!looksLikeLink)
|
|
51
|
+
return s;
|
|
52
|
+
const fragment = hash >= 0 ? s.slice(hash + 1) : '';
|
|
53
|
+
const query = q >= 0 && (hash < 0 || q < hash) ? s.slice(q + 1, hash >= 0 ? hash : undefined) : '';
|
|
54
|
+
const token = tokenParam(fragment, 'token') || tokenParam(query, 'token');
|
|
55
|
+
if (!token)
|
|
56
|
+
throw new Error('parseEmailToken: the link has no token (expected #token=... or ?token=...); pass the whole link from the email or the token itself');
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
const enc = encodeURIComponent;
|
|
60
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : v);
|
|
61
|
+
function json(core, method, path, init = {}) {
|
|
62
|
+
const c = core.ctx();
|
|
63
|
+
return c.http.json(method, path, init, c.authorization);
|
|
64
|
+
}
|
|
65
|
+
async function text(core, path, init = {}) {
|
|
66
|
+
const c = core.ctx();
|
|
67
|
+
const res = await c.http.raw('GET', path, init, c.authorization);
|
|
68
|
+
return res.text();
|
|
69
|
+
}
|
|
70
|
+
const pageQuery = (p) => ({ query: { limit: p?.limit, cursor: p?.cursor } });
|
|
71
|
+
/**
|
|
72
|
+
* Races the sleep against the signal. The default sleep's timer is cleared on abort (so it does not hold the process
|
|
73
|
+
* open); an injected sleep may not be abortable and is only raced.
|
|
74
|
+
*/
|
|
75
|
+
function abortableSleep(sleep, ms, signal) {
|
|
76
|
+
if (!signal)
|
|
77
|
+
return sleep(ms);
|
|
78
|
+
if (signal.aborted)
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
let timer;
|
|
82
|
+
const done = () => {
|
|
83
|
+
if (timer !== undefined)
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
signal.removeEventListener('abort', done);
|
|
86
|
+
resolve();
|
|
87
|
+
};
|
|
88
|
+
signal.addEventListener('abort', done, { once: true });
|
|
89
|
+
if (sleep === defaultSleep)
|
|
90
|
+
timer = setTimeout(done, ms);
|
|
91
|
+
else
|
|
92
|
+
void sleep(ms).then(done, done);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const abortReason = (signal) => (signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
96
|
+
// ---- Namespaces ----
|
|
97
|
+
export class AccountTotpApi {
|
|
98
|
+
#core;
|
|
99
|
+
constructor(core) {
|
|
100
|
+
this.#core = core;
|
|
101
|
+
}
|
|
102
|
+
/** Starts TOTP enrollment: the `secret` and `otpauth_uri` for an authenticator app (confirm with a code). */
|
|
103
|
+
enroll() {
|
|
104
|
+
return json(this.#core, 'POST', '/v1/auth/mfa/totp/enroll');
|
|
105
|
+
}
|
|
106
|
+
/** Confirms enrollment with a current code: returns the recovery codes (shown once). Rotates the session token. */
|
|
107
|
+
async confirm(code) {
|
|
108
|
+
return this.#core.adopt(await json(this.#core, 'POST', '/v1/auth/mfa/totp/confirm', { json: { code } }));
|
|
109
|
+
}
|
|
110
|
+
/** Turns TOTP off (needs a recent step-up). Rotates the session token. */
|
|
111
|
+
async disable() {
|
|
112
|
+
return this.#core.adopt(await json(this.#core, 'POST', '/v1/auth/mfa/totp/disable'));
|
|
113
|
+
}
|
|
114
|
+
/** New recovery codes (the old ones stop working; needs a recent step-up). */
|
|
115
|
+
regenerateRecoveryCodes() {
|
|
116
|
+
return json(this.#core, 'POST', '/v1/auth/mfa/recovery-codes');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class AccountAuthApi {
|
|
120
|
+
#core;
|
|
121
|
+
/** Authenticator-app second factor. */
|
|
122
|
+
totp;
|
|
123
|
+
constructor(core) {
|
|
124
|
+
this.#core = core;
|
|
125
|
+
this.totp = new AccountTotpApi(core);
|
|
126
|
+
}
|
|
127
|
+
/** The session's state (`anonymous`, `mfa_required`, `authenticated`), its user and expiry. */
|
|
128
|
+
session() {
|
|
129
|
+
return json(this.#core, 'GET', '/v1/auth/session');
|
|
130
|
+
}
|
|
131
|
+
/** Completes a login that answered `mfa_required`, with a TOTP code or a recovery code. Rotates the session token. */
|
|
132
|
+
async completeMfa(params) {
|
|
133
|
+
const body = 'code' in params ? { code: params.code } : { recovery_code: params.recoveryCode };
|
|
134
|
+
return this.#core.adopt(await json(this.#core, 'POST', '/v1/auth/mfa/challenge', { json: body }));
|
|
135
|
+
}
|
|
136
|
+
/** Ends this session (the token stops working; `sessionToken` keeps the revoked value). */
|
|
137
|
+
logout() {
|
|
138
|
+
return json(this.#core, 'POST', '/v1/auth/logout');
|
|
139
|
+
}
|
|
140
|
+
/** Ends every session of the user, this one included. */
|
|
141
|
+
logoutAll() {
|
|
142
|
+
return json(this.#core, 'POST', '/v1/auth/logout-all');
|
|
143
|
+
}
|
|
144
|
+
/** The user's sessions (browser and CLI; `current` marks this one). */
|
|
145
|
+
sessions() {
|
|
146
|
+
return json(this.#core, 'GET', '/v1/auth/sessions');
|
|
147
|
+
}
|
|
148
|
+
revokeSession(sessionId) {
|
|
149
|
+
return json(this.#core, 'DELETE', `/v1/auth/sessions/${enc(sessionId)}`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Confirms the password (and a code when MFA is on) for the calls that need a recent check (403
|
|
153
|
+
* `step_up_required`). Rotates the session token.
|
|
154
|
+
*/
|
|
155
|
+
async stepUp(params) {
|
|
156
|
+
const body = { password: params.password };
|
|
157
|
+
if (params.code !== undefined)
|
|
158
|
+
body.code = params.code;
|
|
159
|
+
if (params.recoveryCode !== undefined)
|
|
160
|
+
body.recovery_code = params.recoveryCode;
|
|
161
|
+
return this.#core.adopt(await json(this.#core, 'POST', '/v1/auth/step-up', { json: body }));
|
|
162
|
+
}
|
|
163
|
+
/** Changes the password; every other session is revoked. Rotates the session token. */
|
|
164
|
+
async changePassword(params) {
|
|
165
|
+
const body = { current_password: params.currentPassword, new_password: params.newPassword };
|
|
166
|
+
return this.#core.adopt(await json(this.#core, 'POST', '/v1/auth/password/change', { json: body }));
|
|
167
|
+
}
|
|
168
|
+
/** Sends a confirmation link to the new address (confirm with ShardfluxAccount.confirmEmailChange). */
|
|
169
|
+
changeEmail(newEmail) {
|
|
170
|
+
return json(this.#core, 'POST', '/v1/auth/email/change', { json: { new_email: newEmail } });
|
|
171
|
+
}
|
|
172
|
+
/** Sends the verification email again. */
|
|
173
|
+
resendVerification() {
|
|
174
|
+
return json(this.#core, 'POST', '/v1/auth/verify-email/resend');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export class OrganizationExportsApi {
|
|
178
|
+
#core;
|
|
179
|
+
constructor(core) {
|
|
180
|
+
this.#core = core;
|
|
181
|
+
}
|
|
182
|
+
/** Builds a JSON export of the organization (owners/admins; needs a recent step-up). */
|
|
183
|
+
create(organizationId) {
|
|
184
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/exports`);
|
|
185
|
+
}
|
|
186
|
+
get(organizationId, exportId) {
|
|
187
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/exports/${enc(exportId)}`);
|
|
188
|
+
}
|
|
189
|
+
/** The export document (JSON text). */
|
|
190
|
+
download(organizationId, exportId, opts = {}) {
|
|
191
|
+
return text(this.#core, `/v1/organizations/${enc(organizationId)}/exports/${enc(exportId)}/download`, opts.signal ? { signal: opts.signal } : {});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export class AccountOrganizationsApi {
|
|
195
|
+
#core;
|
|
196
|
+
/** Data exports of an organization. */
|
|
197
|
+
exports;
|
|
198
|
+
constructor(core) {
|
|
199
|
+
this.#core = core;
|
|
200
|
+
this.exports = new OrganizationExportsApi(core);
|
|
201
|
+
}
|
|
202
|
+
/** One page of the user's organizations (with the user's `role`). */
|
|
203
|
+
list(params) {
|
|
204
|
+
return json(this.#core, 'GET', '/v1/organizations', pageQuery(params));
|
|
205
|
+
}
|
|
206
|
+
/** Every organization of the user, following next_cursor. */
|
|
207
|
+
async *listAll(params = {}) {
|
|
208
|
+
let cursor;
|
|
209
|
+
do {
|
|
210
|
+
const page = await this.list({ ...params, ...(cursor === undefined ? {} : { cursor }) });
|
|
211
|
+
yield* page.data;
|
|
212
|
+
cursor = page.next_cursor ?? undefined;
|
|
213
|
+
} while (cursor !== undefined);
|
|
214
|
+
}
|
|
215
|
+
/** Creates an organization; the user becomes its owner. */
|
|
216
|
+
create(params) {
|
|
217
|
+
return json(this.#core, 'POST', '/v1/organizations', { json: { name: params.name, ...(params.slug === undefined ? {} : { slug: params.slug }) } });
|
|
218
|
+
}
|
|
219
|
+
get(organizationId) {
|
|
220
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}`);
|
|
221
|
+
}
|
|
222
|
+
/** The organization's plan, allowances and limits. */
|
|
223
|
+
entitlements(organizationId) {
|
|
224
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/entitlements`);
|
|
225
|
+
}
|
|
226
|
+
/** The deletion state: a scheduled request, what blocks it (an active subscription) and what would be removed. */
|
|
227
|
+
deletion(organizationId) {
|
|
228
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/deletion`);
|
|
229
|
+
}
|
|
230
|
+
/** Deletes the organization (owners; needs a recent step-up). `confirmation` is the organization's slug. */
|
|
231
|
+
delete(organizationId, params) {
|
|
232
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/deletion`, { json: { confirmation: params.confirmation } });
|
|
233
|
+
}
|
|
234
|
+
/** One page of the organization's workspaces (views, across its projects unless `projectId`). */
|
|
235
|
+
workspaces(organizationId, params = {}) {
|
|
236
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/workspaces`, {
|
|
237
|
+
query: {
|
|
238
|
+
limit: params.limit,
|
|
239
|
+
cursor: params.cursor,
|
|
240
|
+
project_id: params.projectId,
|
|
241
|
+
state: params.state,
|
|
242
|
+
desired_state: params.desiredState,
|
|
243
|
+
key_prefix: params.keyPrefix,
|
|
244
|
+
include_deleted: params.includeDeleted,
|
|
245
|
+
lifetime: params.lifetime,
|
|
246
|
+
purpose: params.purpose,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export class AccountProjectsApi {
|
|
252
|
+
#core;
|
|
253
|
+
constructor(core) {
|
|
254
|
+
this.#core = core;
|
|
255
|
+
}
|
|
256
|
+
list(organizationId, params) {
|
|
257
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/projects`, pageQuery(params));
|
|
258
|
+
}
|
|
259
|
+
/** Every project of the organization, following next_cursor. */
|
|
260
|
+
async *listAll(organizationId, params = {}) {
|
|
261
|
+
let cursor;
|
|
262
|
+
do {
|
|
263
|
+
const page = await this.list(organizationId, { ...params, ...(cursor === undefined ? {} : { cursor }) });
|
|
264
|
+
yield* page.data;
|
|
265
|
+
cursor = page.next_cursor ?? undefined;
|
|
266
|
+
} while (cursor !== undefined);
|
|
267
|
+
}
|
|
268
|
+
create(organizationId, params) {
|
|
269
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/projects`, { json: { name: params.name, ...(params.slug === undefined ? {} : { slug: params.slug }) } });
|
|
270
|
+
}
|
|
271
|
+
get(projectId) {
|
|
272
|
+
return json(this.#core, 'GET', `/v1/projects/${enc(projectId)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Every tool permission an API key can carry (pass it as `toolPermissions` for a key with every tool). */
|
|
276
|
+
export const API_KEY_TOOL_PERMISSIONS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
|
|
277
|
+
export class AccountApiKeysApi {
|
|
278
|
+
#core;
|
|
279
|
+
constructor(core) {
|
|
280
|
+
this.#core = core;
|
|
281
|
+
}
|
|
282
|
+
list(projectId, params) {
|
|
283
|
+
return json(this.#core, 'GET', `/v1/projects/${enc(projectId)}/api-keys`, pageQuery(params));
|
|
284
|
+
}
|
|
285
|
+
/** Creates a project API key: `secret` is the `sfk_...` key, shown once (an Idempotency-Key replay within 24 h returns it again). */
|
|
286
|
+
create(projectId, params) {
|
|
287
|
+
const body = { name: params.name, tool_permissions: [...(params.toolPermissions ?? [])] };
|
|
288
|
+
const expiresAt = iso(params.expiresAt);
|
|
289
|
+
if (expiresAt !== undefined)
|
|
290
|
+
body.expires_at = expiresAt;
|
|
291
|
+
return json(this.#core, 'POST', `/v1/projects/${enc(projectId)}/api-keys`, { json: body, idempotencyKey: params.idempotencyKey ?? randomId('api-key-') });
|
|
292
|
+
}
|
|
293
|
+
revoke(projectId, apiKeyId) {
|
|
294
|
+
return json(this.#core, 'DELETE', `/v1/projects/${enc(projectId)}/api-keys/${enc(apiKeyId)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
export class AccountMembersApi {
|
|
298
|
+
#core;
|
|
299
|
+
constructor(core) {
|
|
300
|
+
this.#core = core;
|
|
301
|
+
}
|
|
302
|
+
list(organizationId, params) {
|
|
303
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/members`, pageQuery(params));
|
|
304
|
+
}
|
|
305
|
+
update(organizationId, userId, params) {
|
|
306
|
+
return json(this.#core, 'PATCH', `/v1/organizations/${enc(organizationId)}/members/${enc(userId)}`, { json: { role: params.role } });
|
|
307
|
+
}
|
|
308
|
+
remove(organizationId, userId) {
|
|
309
|
+
return json(this.#core, 'DELETE', `/v1/organizations/${enc(organizationId)}/members/${enc(userId)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export class AccountInvitationsApi {
|
|
313
|
+
#core;
|
|
314
|
+
constructor(core) {
|
|
315
|
+
this.#core = core;
|
|
316
|
+
}
|
|
317
|
+
list(organizationId, params) {
|
|
318
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/invitations`, pageQuery(params));
|
|
319
|
+
}
|
|
320
|
+
/** Invites an email address (the invitee gets a link; accept with invitations.accept). */
|
|
321
|
+
create(organizationId, params) {
|
|
322
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/invitations`, { json: { email: params.email, role: params.role } });
|
|
323
|
+
}
|
|
324
|
+
revoke(organizationId, invitationId) {
|
|
325
|
+
return json(this.#core, 'DELETE', `/v1/organizations/${enc(organizationId)}/invitations/${enc(invitationId)}`);
|
|
326
|
+
}
|
|
327
|
+
/** Accepts an invitation with the emailed link (or its token): returns the organization joined. */
|
|
328
|
+
accept(linkOrToken) {
|
|
329
|
+
return json(this.#core, 'POST', '/v1/invitations/accept', { json: { token: parseEmailToken(linkOrToken) } });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** Checkout statuses after which the checkout will never activate a subscription. */
|
|
333
|
+
const CHECKOUT_TERMINAL = new Set(['expired', 'canceled', 'failed']);
|
|
334
|
+
/**
|
|
335
|
+
* waitForCheckout gave up (0.9.0): nobody paid within `timeoutMs`, or the payment's webhook has not activated the
|
|
336
|
+
* subscription yet. The checkout stays usable; `checkout` is its last status.
|
|
337
|
+
*/
|
|
338
|
+
export class CheckoutTimeoutError extends Error {
|
|
339
|
+
checkout;
|
|
340
|
+
waitedMs;
|
|
341
|
+
constructor(checkout, waitedMs) {
|
|
342
|
+
super(`checkout ${checkout.id} is ${checkout.status} and the subscription is not active after ${Math.round(waitedMs / 1000)} s`);
|
|
343
|
+
this.name = 'CheckoutTimeoutError';
|
|
344
|
+
this.checkout = checkout;
|
|
345
|
+
this.waitedMs = waitedMs;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Billing with a user session: the catalog and subscription (as for API keys) plus Checkout, the Stripe portal,
|
|
350
|
+
* invoices and usage alert thresholds (owner/billing members).
|
|
351
|
+
*/
|
|
352
|
+
export class AccountBillingApi extends BillingApi {
|
|
353
|
+
#core;
|
|
354
|
+
constructor(core) {
|
|
355
|
+
super(core.ctx);
|
|
356
|
+
this.#core = core;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Starts (or reuses) a Stripe Checkout for `planKey`: a person pays at `url`. 409 `subscription_exists` when the
|
|
360
|
+
* organization already has a subscription (change plans in the portal).
|
|
361
|
+
*/
|
|
362
|
+
checkout(organizationId, params) {
|
|
363
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/billing/checkout-sessions`, { json: { plan_key: params.planKey } });
|
|
364
|
+
}
|
|
365
|
+
checkoutStatus(organizationId, checkoutId) {
|
|
366
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/billing/checkout-sessions/${enc(checkoutId)}`);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Polls the checkout until the subscription is active (`subscription_active`) or the checkout ended without one
|
|
370
|
+
* (`expired`, `canceled`, `failed`) and returns that final status. Throws CheckoutTimeoutError after `timeoutMs`,
|
|
371
|
+
* and the signal's reason when aborted.
|
|
372
|
+
*/
|
|
373
|
+
async waitForCheckout(organizationId, checkoutId, opts = {}) {
|
|
374
|
+
const timeoutMs = opts.timeoutMs ?? 900_000;
|
|
375
|
+
const intervalMs = opts.intervalMs ?? 2_000;
|
|
376
|
+
const { sleep } = this.#core.ctx();
|
|
377
|
+
const started = Date.now();
|
|
378
|
+
for (;;) {
|
|
379
|
+
if (opts.signal?.aborted)
|
|
380
|
+
throw abortReason(opts.signal);
|
|
381
|
+
const status = await json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/billing/checkout-sessions/${enc(checkoutId)}`, opts.signal ? { signal: opts.signal } : {});
|
|
382
|
+
if (status.subscription_active || CHECKOUT_TERMINAL.has(status.status))
|
|
383
|
+
return status;
|
|
384
|
+
const waited = Date.now() - started;
|
|
385
|
+
if (waited >= timeoutMs)
|
|
386
|
+
throw new CheckoutTimeoutError(status, waited);
|
|
387
|
+
await abortableSleep(sleep, Math.max(1, Math.min(intervalMs, timeoutMs - waited)), opts.signal);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** A Stripe customer portal link (plan changes, payment methods, cancellation). */
|
|
391
|
+
portal(organizationId) {
|
|
392
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/billing/portal-sessions`);
|
|
393
|
+
}
|
|
394
|
+
invoices(organizationId, params) {
|
|
395
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/billing/invoices`, pageQuery(params));
|
|
396
|
+
}
|
|
397
|
+
/** Usage alert thresholds. */
|
|
398
|
+
spendPolicy(organizationId) {
|
|
399
|
+
return json(this.#core, 'GET', `/v1/organizations/${enc(organizationId)}/spend-policy`);
|
|
400
|
+
}
|
|
401
|
+
/** Sets the usage alert thresholds (percent of the allowance, 1..100, at most 5; [] turns alerts off). */
|
|
402
|
+
setSpendPolicy(organizationId, params) {
|
|
403
|
+
return json(this.#core, 'PUT', `/v1/organizations/${enc(organizationId)}/spend-policy`, { json: { alert_thresholds_percent: params.alertThresholdsPercent } });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export class AccountExportsApi {
|
|
407
|
+
#core;
|
|
408
|
+
constructor(core) {
|
|
409
|
+
this.#core = core;
|
|
410
|
+
}
|
|
411
|
+
/** Builds a JSON export of the user's own data (needs a recent step-up). */
|
|
412
|
+
create() {
|
|
413
|
+
return json(this.#core, 'POST', '/v1/account/exports');
|
|
414
|
+
}
|
|
415
|
+
get(exportId) {
|
|
416
|
+
return json(this.#core, 'GET', `/v1/account/exports/${enc(exportId)}`);
|
|
417
|
+
}
|
|
418
|
+
/** The export document (JSON text; needs a recent step-up). */
|
|
419
|
+
download(exportId, opts = {}) {
|
|
420
|
+
return text(this.#core, `/v1/account/exports/${enc(exportId)}/download`, opts.signal ? { signal: opts.signal } : {});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** The signed-in person: account deletion and data export. */
|
|
424
|
+
export class AccountUserApi {
|
|
425
|
+
#core;
|
|
426
|
+
exports;
|
|
427
|
+
constructor(core) {
|
|
428
|
+
this.#core = core;
|
|
429
|
+
this.exports = new AccountExportsApi(core);
|
|
430
|
+
}
|
|
431
|
+
/** The deletion state: a scheduled request, the organizations that block it, the API keys that survive it. */
|
|
432
|
+
deletion() {
|
|
433
|
+
return json(this.#core, 'GET', '/v1/account/deletion');
|
|
434
|
+
}
|
|
435
|
+
/** Schedules deletion of the account after the grace period (needs a recent step-up). `confirmation` is the user's email. */
|
|
436
|
+
scheduleDeletion(params) {
|
|
437
|
+
return json(this.#core, 'POST', '/v1/account/deletion', { json: { confirmation: params.confirmation } });
|
|
438
|
+
}
|
|
439
|
+
cancelDeletion() {
|
|
440
|
+
return json(this.#core, 'DELETE', '/v1/account/deletion');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Organization templates with a user session: everything of TemplatesApi (pass `organizationId`) plus publish/archive. */
|
|
444
|
+
export class AccountTemplatesApi extends TemplatesApi {
|
|
445
|
+
#core;
|
|
446
|
+
constructor(core) {
|
|
447
|
+
super(core.ctx);
|
|
448
|
+
this.#core = core;
|
|
449
|
+
}
|
|
450
|
+
/** Publishes a registered version (owners/admins): `open` may use it. */
|
|
451
|
+
publishVersion(organizationId, slug, version) {
|
|
452
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/templates/${enc(slug)}/versions/${version}/publish`);
|
|
453
|
+
}
|
|
454
|
+
/** Archives a version (owners/admins): new opens stop using it; existing workspaces keep it. */
|
|
455
|
+
archiveVersion(organizationId, slug, version) {
|
|
456
|
+
return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/templates/${enc(slug)}/versions/${version}/archive`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** The organization audit log (owners/admins) plus its CSV/NDJSON export. */
|
|
460
|
+
export class AccountAuditApi extends AuditApi {
|
|
461
|
+
#core;
|
|
462
|
+
constructor(core) {
|
|
463
|
+
super(core.ctx);
|
|
464
|
+
this.#core = core;
|
|
465
|
+
}
|
|
466
|
+
/** The export as text: NDJSON (one AuditEvent per line) or CSV with a header row. */
|
|
467
|
+
export(organizationId, params = {}) {
|
|
468
|
+
const { format = 'ndjson', signal, timeoutMs, ...filters } = params;
|
|
469
|
+
return text(this.#core, `/v1/organizations/${enc(organizationId)}/audit-events/export`, {
|
|
470
|
+
query: { format, ...auditQuery(filters) },
|
|
471
|
+
accept: `${format === 'csv' ? 'text/csv' : 'application/x-ndjson'}, application/json`,
|
|
472
|
+
timeoutMs: timeoutMs ?? Math.max(this.#core.ctx().http.opts.timeoutMs, 120_000),
|
|
473
|
+
...(signal ? { signal } : {}),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A user (CLI) session on the Shardflux API: accounts, organizations, projects, API keys, members, invitations,
|
|
479
|
+
* billing, audit, data export and deletion, plus the project namespaces (usage, secrets, egress, volumes) with explicit
|
|
480
|
+
* organization/project ids. See the module comment for the token lifecycle.
|
|
481
|
+
*/
|
|
482
|
+
export class ShardfluxAccount {
|
|
483
|
+
auth;
|
|
484
|
+
organizations;
|
|
485
|
+
projects;
|
|
486
|
+
apiKeys;
|
|
487
|
+
members;
|
|
488
|
+
invitations;
|
|
489
|
+
billing;
|
|
490
|
+
/** The signed-in person (account deletion, data export). */
|
|
491
|
+
user;
|
|
492
|
+
templates;
|
|
493
|
+
audit;
|
|
494
|
+
usage;
|
|
495
|
+
secrets;
|
|
496
|
+
egress;
|
|
497
|
+
volumes;
|
|
498
|
+
/** Workspace routes with the session (pass `projectId` where a route needs a project, e.g. open). */
|
|
499
|
+
workspaces;
|
|
500
|
+
#token;
|
|
501
|
+
#onSessionToken;
|
|
502
|
+
#ctx;
|
|
503
|
+
constructor(opts = {}) {
|
|
504
|
+
if (opts.sessionToken !== undefined && !isSessionToken(opts.sessionToken))
|
|
505
|
+
throw new Error('sessionToken must be a Shardflux CLI session token (sfu_<43 base64url characters>)');
|
|
506
|
+
this.#token = opts.sessionToken ?? null;
|
|
507
|
+
this.#onSessionToken = opts.onSessionToken;
|
|
508
|
+
const f = opts.fetch ?? defaultFetch();
|
|
509
|
+
const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
|
|
510
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
511
|
+
const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
512
|
+
const ctx = () => this.#ctx;
|
|
513
|
+
const core = { ctx, adopt: (body) => this.#adopt(body) };
|
|
514
|
+
this.workspaces = new WorkspacesApi(ctx);
|
|
515
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
516
|
+
const self = this;
|
|
517
|
+
this.#ctx = {
|
|
518
|
+
http: new HttpClient({ baseUrl, fetch: f, userAgent, timeoutMs: opts.timeoutMs ?? 30_000, maxRetries: opts.maxRetries ?? 2, source: 'api', sleep, onSuccess: versionCheckHook(opts.versionCheck, baseUrl, f, userAgent) }),
|
|
519
|
+
// Read on every request: a rotation applies to the next call of every namespace.
|
|
520
|
+
get authorization() {
|
|
521
|
+
return self.#token === null ? '' : `Bearer ${self.#token}`;
|
|
522
|
+
},
|
|
523
|
+
fetch: f,
|
|
524
|
+
userAgent,
|
|
525
|
+
sleep,
|
|
526
|
+
workspaces: this.workspaces,
|
|
527
|
+
onProgress: opts.onProgress,
|
|
528
|
+
captures: new CaptureRegistry(),
|
|
529
|
+
};
|
|
530
|
+
this.auth = new AccountAuthApi(core);
|
|
531
|
+
this.organizations = new AccountOrganizationsApi(core);
|
|
532
|
+
this.projects = new AccountProjectsApi(core);
|
|
533
|
+
this.apiKeys = new AccountApiKeysApi(core);
|
|
534
|
+
this.members = new AccountMembersApi(core);
|
|
535
|
+
this.invitations = new AccountInvitationsApi(core);
|
|
536
|
+
this.billing = new AccountBillingApi(core);
|
|
537
|
+
this.user = new AccountUserApi(core);
|
|
538
|
+
this.templates = new AccountTemplatesApi(core);
|
|
539
|
+
this.audit = new AccountAuditApi(core);
|
|
540
|
+
this.usage = new UsageApi(ctx);
|
|
541
|
+
this.secrets = new SecretsApi(ctx);
|
|
542
|
+
this.egress = new EgressPolicyApi(ctx);
|
|
543
|
+
this.volumes = new VolumesApi(ctx);
|
|
544
|
+
}
|
|
545
|
+
/** The current session token (rotations replace it), or null before login. */
|
|
546
|
+
get sessionToken() {
|
|
547
|
+
return this.#token;
|
|
548
|
+
}
|
|
549
|
+
async #adopt(body) {
|
|
550
|
+
const b = body;
|
|
551
|
+
if (b && typeof b === 'object' && typeof b.session_token === 'string') {
|
|
552
|
+
if (!isSessionToken(b.session_token))
|
|
553
|
+
throw new ShardfluxProtocolError('the API returned a session_token that is not a CLI session token (sfu_...)', 200);
|
|
554
|
+
this.#token = b.session_token;
|
|
555
|
+
const expiresAt = typeof b.session_expires_at === 'string' ? b.session_expires_at : '';
|
|
556
|
+
await this.#onSessionToken?.({ token: b.session_token, expiresAt });
|
|
557
|
+
}
|
|
558
|
+
return body;
|
|
559
|
+
}
|
|
560
|
+
/** The signed-in user and their memberships. */
|
|
561
|
+
me() {
|
|
562
|
+
return this.#ctx.http.json('GET', '/v1/me', {}, this.#ctx.authorization);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Send feedback straight to the Shardflux founder as the signed-in user (0.9.0+; POST /v1/feedback), optionally about
|
|
566
|
+
* one of your organizations (`organizationId`). Otherwise the same as `Shardflux.sendFeedback`: use it while you work,
|
|
567
|
+
* returns `{ id, receivedAt, duplicate }`, never retried, 429 `rate_limited` per user.
|
|
568
|
+
*/
|
|
569
|
+
sendFeedback(params) {
|
|
570
|
+
return sendFeedback(this.#ctx, params);
|
|
571
|
+
}
|
|
572
|
+
/** Raw access to any /v1 endpoint with the session's authentication and the SDK's error handling. */
|
|
573
|
+
request(method, path, init = {}) {
|
|
574
|
+
// A raw call to a rotating /v1/auth route (step-up, ...) is adopted like the typed one.
|
|
575
|
+
return this.#ctx.http.json(method, path, init, this.#ctx.authorization).then((b) => this.#adopt(b));
|
|
576
|
+
}
|
|
577
|
+
// ---- Before a session exists ----
|
|
578
|
+
static #client(opts) {
|
|
579
|
+
return new ShardfluxAccount(opts);
|
|
580
|
+
}
|
|
581
|
+
static #post(opts, path, body) {
|
|
582
|
+
const account = ShardfluxAccount.#client(opts);
|
|
583
|
+
return account.#ctx.http.json('POST', path, body === undefined ? {} : { json: body });
|
|
584
|
+
}
|
|
585
|
+
/** Creates an account; a verification link is emailed (`{ status: 'accepted' }` also when the email is taken). */
|
|
586
|
+
static register(params) {
|
|
587
|
+
const { email, password, displayName, ...opts } = params;
|
|
588
|
+
return ShardfluxAccount.#post(opts, '/v1/auth/register', { email, password, ...(displayName === undefined ? {} : { display_name: displayName }) });
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Verifies the email with the emailed link (or its token). `password_reset_required` (with `reset_token`): the
|
|
592
|
+
* account must set a password first (confirmPasswordReset with that token).
|
|
593
|
+
*/
|
|
594
|
+
static verifyEmail(linkOrToken, opts = {}) {
|
|
595
|
+
return ShardfluxAccount.#post(opts, '/v1/auth/verify-email', { token: parseEmailToken(linkOrToken) });
|
|
596
|
+
}
|
|
597
|
+
/** Emails a password reset link (`{ status: 'accepted' }` whether or not the account exists). */
|
|
598
|
+
static requestPasswordReset(email, opts = {}) {
|
|
599
|
+
return ShardfluxAccount.#post(opts, '/v1/auth/password/reset/request', { email });
|
|
600
|
+
}
|
|
601
|
+
/** Sets a new password with the emailed reset link (or its token); every session is revoked. */
|
|
602
|
+
static confirmPasswordReset(params, opts = {}) {
|
|
603
|
+
return ShardfluxAccount.#post(opts, '/v1/auth/password/reset/confirm', { token: parseEmailToken(params.token), new_password: params.newPassword });
|
|
604
|
+
}
|
|
605
|
+
/** Confirms an email change with the link sent to the new address (or its token). */
|
|
606
|
+
static confirmEmailChange(linkOrToken, opts = {}) {
|
|
607
|
+
return ShardfluxAccount.#post(opts, '/v1/auth/email/change/confirm', { token: parseEmailToken(linkOrToken) });
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Signs in: `account` holds the new session (onSessionToken is called with it). `result.status` `mfa_required`: the
|
|
611
|
+
* session is pending until `account.auth.completeMfa({ code })` (which rotates the token).
|
|
612
|
+
*/
|
|
613
|
+
static async login(params) {
|
|
614
|
+
const { email, password, ...opts } = params;
|
|
615
|
+
const account = ShardfluxAccount.#client(opts);
|
|
616
|
+
const result = await account.#ctx.http.json('POST', '/v1/auth/login', { json: { email, password } });
|
|
617
|
+
await account.#adopt(result);
|
|
618
|
+
return { account, result };
|
|
619
|
+
}
|
|
620
|
+
}
|