@simular-ai/sai-sdk 0.0.0-stage → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Sessions: a conversation with Sai on one computer. Sai decides every click.
3
+ *
4
+ * Maps to `/v1/agents/*`: `new-session`, `message` (wait=false), `events`,
5
+ * `approve`, `abort`. Every message gets a turn id (`turn.id`, read with
6
+ * `sai.turns`); a message with `inputs`, `outputSchema` or `successCriteria`
7
+ * is structured and its turn also returns a typed result.
8
+ */
9
+ import { ConflictError, LinkOnlyApprovalError, NotFoundError, RateLimitError } from './errors.js';
10
+ import { idOf } from './machines.js';
11
+ import { parseApproval, parseEvent, parseTurnRecord, parseUsage, } from './types.js';
12
+ const POLL_WAIT = 30;
13
+ /** Between turn reads; `GET /v1/agents/turns/{id}` never holds. */
14
+ const RECORD_POLL_INTERVAL_MS = 2_000;
15
+ export class Sessions {
16
+ http;
17
+ machines;
18
+ constructor(http, machines) {
19
+ this.http = http;
20
+ this.machines = machines;
21
+ }
22
+ /**
23
+ * Start a fresh conversation on a computer: the next message has no memory
24
+ * of earlier ones. `machine` may be omitted with one computer.
25
+ */
26
+ async create(opts = {}) {
27
+ const machineId = await this.machines.resolveId(opts.machine);
28
+ let sessionId = null;
29
+ try {
30
+ const d = await this.http.json('POST', '/v1/agents/new-session', {
31
+ json: { machineId, channel: 'api' },
32
+ });
33
+ sessionId = d.sessionId ?? null;
34
+ }
35
+ catch (err) {
36
+ // A free account's computer between claims has no conversation to
37
+ // rotate; the first message starts one.
38
+ if (!(err instanceof NotFoundError))
39
+ throw err;
40
+ }
41
+ return new Session(this.http, machineId, sessionId);
42
+ }
43
+ /** Reattach to a session started elsewhere, e.g. to poll or abort it. */
44
+ resume(sessionId, opts = {}) {
45
+ return new Session(this.http, opts.machine ? idOf(opts.machine) : null, sessionId);
46
+ }
47
+ }
48
+ /** A conversation with Sai. Each `send()` is a turn; turns share context. */
49
+ export class Session {
50
+ http;
51
+ machineId;
52
+ id;
53
+ constructor(http, machineId, id) {
54
+ this.http = http;
55
+ this.machineId = machineId;
56
+ this.id = id;
57
+ }
58
+ /**
59
+ * Give Sai a task, or steer the running one. Returns at once. Sent while a
60
+ * turn is running, the message steers that turn (`turn.queued` is true).
61
+ *
62
+ * Any of `inputs`, `outputSchema` or `successCriteria` makes the message
63
+ * structured: the message is a template whose `{{name}}` placeholders are
64
+ * filled from `inputs`, the output must match `outputSchema`, and the turn
65
+ * is checked against `successCriteria`. `wait()` then also returns the
66
+ * turn's record in `result.record`. A structured message is refused with
67
+ * `ConflictError` (`session_busy`) while a turn is running. These go to the
68
+ * API as `options`.
69
+ */
70
+ async send(message, opts = {}) {
71
+ const body = { message, wait: false };
72
+ if (this.machineId)
73
+ body.machineId = this.machineId;
74
+ if (this.id)
75
+ body.sessionId = this.id;
76
+ if (opts.model)
77
+ body.model = opts.model;
78
+ if (opts.attachments?.length)
79
+ body.attachments = opts.attachments.map((a) => a.raw);
80
+ if (opts.autonomous)
81
+ body.autonomous = true;
82
+ const options = {};
83
+ if (opts.inputs !== undefined)
84
+ options.inputs = opts.inputs;
85
+ if (opts.outputSchema !== undefined)
86
+ options.output_schema = opts.outputSchema;
87
+ if (opts.successCriteria !== undefined)
88
+ options.success_criteria = opts.successCriteria;
89
+ if (opts.idempotencyKey !== undefined)
90
+ options.idempotency_key = opts.idempotencyKey;
91
+ if (opts.recordTrajectory)
92
+ options.recordTrajectory = true;
93
+ if (Object.keys(options).length)
94
+ body.options = options;
95
+ const structured = [opts.inputs, opts.outputSchema, opts.successCriteria].some((v) => v !== undefined);
96
+ const d = await this.http.json('POST', '/v1/agents/message', {
97
+ json: body,
98
+ });
99
+ if (d.idempotent_replay) {
100
+ // The key was used before: this is that turn, and nothing was sent.
101
+ const record = parseTurnRecord(d);
102
+ this.id = record.sessionId ?? this.id;
103
+ this.machineId = record.machineId || this.machineId;
104
+ return new Turn(this, this.http, false, null, { id: record.id, structured, replay: record });
105
+ }
106
+ if (typeof d.sessionId !== 'string' || !d.sessionId) {
107
+ // Handled without starting a task (e.g. a plain acknowledgement): the reply is in `reply`,
108
+ // or in `error` on servers before `reply` existed.
109
+ const text = typeof d.reply === 'string' ? d.reply : typeof d.error === 'string' ? d.error : '';
110
+ return new Turn(this, this.http, false, localResult(this.id ?? '', text));
111
+ }
112
+ this.id = d.sessionId;
113
+ if (typeof d.machineId === 'string')
114
+ this.machineId = d.machineId;
115
+ const id = typeof d.turnId === 'string' ? d.turnId : null;
116
+ return new Turn(this, this.http, Boolean(d.queued), null, { id, structured });
117
+ }
118
+ /** `send()` then `wait()`: the answer, or where it stopped. */
119
+ async run(message, opts = {}) {
120
+ const turn = await this.send(message, opts);
121
+ return turn.wait(opts);
122
+ }
123
+ /** Stop the running turn. Pending approvals are denied. False if nothing was running. */
124
+ async abort() {
125
+ if (!this.id)
126
+ return false;
127
+ const d = await this.http.json('POST', '/v1/agents/abort', {
128
+ json: { sessionId: this.id },
129
+ });
130
+ return Boolean(d.aborted);
131
+ }
132
+ /**
133
+ * Answer an approval: `yes`, `no`, or `task` (allow for the rest of the task).
134
+ * A `choice` approval needs `selections`, one list of option values per
135
+ * question. Throws `LinkOnlyApprovalError` (with `approvalUrl`) when only a
136
+ * browser can answer it. Resolves to `approved`, `approved_task` or `denied`.
137
+ */
138
+ async respond(approval, response = 'yes', opts = {}) {
139
+ if (typeof approval !== 'string' && approval.isLinkOnly) {
140
+ throw new LinkOnlyApprovalError('This approval must be completed in a browser. Open approval.url.', { status: 409, code: 'link_only', body: { approvalUrl: approval.url } });
141
+ }
142
+ const body = {
143
+ approvalId: typeof approval === 'string' ? approval : approval.id,
144
+ response,
145
+ };
146
+ if (opts.selections)
147
+ body.selections = opts.selections;
148
+ const d = await this.http.json('POST', '/v1/agents/approve', {
149
+ json: body,
150
+ });
151
+ return d.status ?? '';
152
+ }
153
+ }
154
+ /**
155
+ * One message's work. Iterate `events()` to watch it, or `wait()` for the
156
+ * result. `id` is the turn the message started, or the running turn it joined
157
+ * (`sai.turns.get(turn)`); null when the message was answered without one.
158
+ */
159
+ export class Turn {
160
+ session;
161
+ http;
162
+ queued;
163
+ id;
164
+ cursor = null;
165
+ last;
166
+ structured;
167
+ replay;
168
+ // answered without starting a task, or an idempotent replay: no events to poll
169
+ local;
170
+ constructor(session, http, queued, done = null, turn = {
171
+ id: null,
172
+ structured: false,
173
+ }) {
174
+ this.session = session;
175
+ this.http = http;
176
+ this.queued = queued;
177
+ this.last = done;
178
+ this.id = turn.id;
179
+ this.structured = turn.structured;
180
+ this.replay = turn.replay ?? null;
181
+ this.local = done !== null || this.replay !== null;
182
+ }
183
+ get status() {
184
+ return this.last?.status ?? 'running';
185
+ }
186
+ /** The last known state, or null before the first poll. */
187
+ get result() {
188
+ return this.last;
189
+ }
190
+ /**
191
+ * Yield events until the turn is idle or errors. It keeps going through
192
+ * `needs_approval`: answer with `turn.respond(ev.approval, ...)` inside the
193
+ * loop, or let a human answer a link-only approval in a browser. Without
194
+ * `raw`, framing, reasoning and tool-call frames are skipped.
195
+ */
196
+ async *events(opts = {}) {
197
+ if (this.local)
198
+ return;
199
+ for (;;) {
200
+ const page = await this.#poll(opts.pollWait ?? POLL_WAIT);
201
+ for (const e of page.events ?? []) {
202
+ const ev = parseEvent(e);
203
+ if (opts.raw || ev.kind !== 'other')
204
+ yield ev;
205
+ }
206
+ if (page.status === 'idle' || page.status === 'error')
207
+ return;
208
+ }
209
+ }
210
+ /**
211
+ * Wait until the turn is done, or stops on an approval nobody answered.
212
+ * Without `onApproval`, resolves with `status === 'needs_approval'` and the
213
+ * `approval` so the caller can decide. With it, the handler's answer is sent
214
+ * and waiting continues; a handler returning null leaves the approval to
215
+ * someone else (a link-only one to a human) and keeps waiting. For a
216
+ * structured message, it then waits for the turn's record (`result.record`:
217
+ * output, evidence), which is worked out after the turn.
218
+ */
219
+ async wait(opts = {}) {
220
+ const deadline = opts.timeoutMs === undefined ? null : Date.now() + opts.timeoutMs;
221
+ if (this.replay) {
222
+ const record = this.replay.done
223
+ ? this.replay
224
+ : await waitTurnRecord(this.http, this.replay.id, deadline);
225
+ this.last = replayResult(record, record.sessionId ?? this.session.id ?? '');
226
+ return this.last;
227
+ }
228
+ if (this.local)
229
+ return this.last;
230
+ const answered = new Set();
231
+ for (;;) {
232
+ let pollWait = opts.pollWait ?? POLL_WAIT;
233
+ if (deadline !== null) {
234
+ const left = deadline - Date.now();
235
+ if (left <= 0) {
236
+ throw new DOMException(`Turn still ${this.status} after ${opts.timeoutMs}ms (session ${this.session.id}).`, 'TimeoutError');
237
+ }
238
+ pollWait = Math.max(1, Math.min(pollWait, Math.floor(left / 1000)));
239
+ }
240
+ const page = await this.#poll(pollWait);
241
+ if (page.status === 'idle' || page.status === 'error') {
242
+ if (this.structured && this.id) {
243
+ this.last = withRecord(this.last, await waitTurnRecord(this.http, this.id, deadline));
244
+ }
245
+ return this.last;
246
+ }
247
+ const approval = this.last?.approval;
248
+ if (page.status !== 'needs_approval' || !approval)
249
+ continue;
250
+ if (!opts.onApproval)
251
+ return this.last;
252
+ if (answered.has(approval.id))
253
+ continue;
254
+ const answer = await opts.onApproval(approval);
255
+ answered.add(approval.id);
256
+ if (answer === null)
257
+ continue;
258
+ const [response, selections] = typeof answer === 'string' ? [answer, undefined] : answer;
259
+ try {
260
+ await this.session.respond(approval, response, { selections });
261
+ }
262
+ catch (err) {
263
+ // answered elsewhere or expired; the next poll shows the new state
264
+ if (!(err instanceof ConflictError))
265
+ throw err;
266
+ }
267
+ }
268
+ }
269
+ respond(approval, response = 'yes', opts = {}) {
270
+ return this.session.respond(approval, response, opts);
271
+ }
272
+ abort() {
273
+ return this.session.abort();
274
+ }
275
+ async #poll(wait) {
276
+ let page;
277
+ for (;;) {
278
+ try {
279
+ page = await this.http.json('GET', '/v1/agents/events', {
280
+ params: { sessionId: this.session.id ?? '', wait, since: this.cursor ?? undefined },
281
+ timeoutMs: (wait + 30) * 1000,
282
+ });
283
+ break;
284
+ }
285
+ catch (err) {
286
+ if (!(err instanceof RateLimitError))
287
+ throw err;
288
+ await new Promise((r) => setTimeout(r, (err.retryAfter ?? 30) * 1000));
289
+ }
290
+ }
291
+ this.cursor = page.cursor ?? this.cursor;
292
+ let error = this.last?.error ?? null;
293
+ let errorCode = this.last?.errorCode ?? null;
294
+ for (const e of page.events ?? []) {
295
+ if (e.type === 'error') {
296
+ error = typeof e.errorText === 'string' ? e.errorText : error;
297
+ errorCode = typeof e.code === 'string' ? e.code : errorCode;
298
+ }
299
+ }
300
+ const status = page.status ?? 'running';
301
+ this.last = {
302
+ status,
303
+ text: page.text ?? '',
304
+ sessionId: page.sessionId ?? this.session.id ?? '',
305
+ approval: page.approval ? parseApproval(page.approval) : null,
306
+ error,
307
+ errorCode,
308
+ usage: parseUsage(page.usage),
309
+ sessionUsage: parseUsage(page.sessionUsage),
310
+ record: null,
311
+ ok: status === 'idle',
312
+ };
313
+ return page;
314
+ }
315
+ }
316
+ /**
317
+ * Read the turn's record until its status is in `stop`. Throws a `TimeoutError`
318
+ * (DOMException) past `deadline` (epoch ms).
319
+ */
320
+ export async function waitTurnRecord(http, turnId, deadline, stop = ['succeeded', 'failed']) {
321
+ for (;;) {
322
+ let record;
323
+ try {
324
+ record = parseTurnRecord(await http.json('GET', `/v1/agents/turns/${encodeURIComponent(turnId)}`));
325
+ }
326
+ catch (err) {
327
+ if (!(err instanceof RateLimitError))
328
+ throw err;
329
+ await sleep((err.retryAfter ?? 30) * 1000);
330
+ continue;
331
+ }
332
+ if (stop.includes(record.status))
333
+ return record;
334
+ if (deadline !== null && Date.now() + RECORD_POLL_INTERVAL_MS > deadline) {
335
+ throw new DOMException(`Turn ${turnId} still ${record.status}.`, 'TimeoutError');
336
+ }
337
+ await sleep(RECORD_POLL_INTERVAL_MS);
338
+ }
339
+ }
340
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
341
+ function withRecord(result, record) {
342
+ return { ...result, record, ok: result.status === 'idle' && record.ok };
343
+ }
344
+ // A turn that failed for these reasons still has the agent's answer; the turn itself did not break.
345
+ const ANSWERED_REASONS = ['output_invalid', 'criteria_not_met', 'criteria_unchecked'];
346
+ /** The result the live path would have returned for this finished turn, from its record. */
347
+ function replayResult(record, sessionId) {
348
+ const result = localResult(sessionId, record.rawAnswer || outputText(record.output));
349
+ if (record.status === 'failed' && !ANSWERED_REASONS.includes(record.reasonCode ?? '')) {
350
+ result.status = 'error';
351
+ result.text = '';
352
+ result.error = record.rawAnswer || `The turn failed (${record.reasonCode ?? 'unknown'}).`;
353
+ result.errorCode = record.errorCode;
354
+ }
355
+ result.usage = record.usage;
356
+ return withRecord(result, record);
357
+ }
358
+ /** `output` as text: a plain message's answer as is, an `outputSchema` object as JSON. */
359
+ function outputText(output) {
360
+ if (output === null || output === undefined)
361
+ return '';
362
+ return typeof output === 'string' ? output : JSON.stringify(output);
363
+ }
364
+ function localResult(sessionId, text) {
365
+ return {
366
+ status: 'idle',
367
+ text,
368
+ sessionId,
369
+ approval: null,
370
+ error: null,
371
+ errorCode: null,
372
+ usage: null,
373
+ sessionUsage: null,
374
+ record: null,
375
+ ok: true,
376
+ };
377
+ }
378
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import type { Sai } from './client.js';
2
+ import type { Credential, Http } from './http.js';
3
+ import { type Enrollment, type Tenant, type TenantMachine, type TenantPage, type TenantToken } from './types.js';
4
+ export type TenantRef = Tenant | string;
5
+ export type TenantMachineRef = TenantMachine | string;
6
+ /** `tenants.client()` exchanges again when its token has less than this left. */
7
+ export declare const TOKEN_REFRESH_MARGIN_MS: number;
8
+ /** Tenants: your customers, under an Organisation. Every call takes an Organisation Key. */
9
+ export declare class Tenants {
10
+ private readonly http;
11
+ private readonly orgId;
12
+ private readonly withCredential;
13
+ constructor(http: Http, orgId: string | undefined, withCredential: (credential: Credential) => Sai);
14
+ /** `externalId` is your id for the customer, unique in the Organisation, and cannot change later. */
15
+ create(name: string, opts?: {
16
+ externalId?: string;
17
+ metadata?: Record<string, string>;
18
+ }): Promise<Tenant>;
19
+ /**
20
+ * One page, newest first. `externalId` finds the Tenant with that id (zero or one) and
21
+ * cannot be combined with `status`.
22
+ */
23
+ list(opts?: {
24
+ status?: string;
25
+ externalId?: string;
26
+ limit?: number;
27
+ cursor?: string;
28
+ }): Promise<TenantPage>;
29
+ get(tenant: TenantRef): Promise<Tenant>;
30
+ /**
31
+ * `metadata` replaces the whole map. `status: 'disabled'` stops the Tenant's tokens and
32
+ * machines; `status: 'active'` brings it back.
33
+ *
34
+ * `cloudMachineLimit` (0 to 1000) caps the Tenant's cloud machines; `null` removes the cap.
35
+ * Lowering it deletes nothing: creates are refused until the count fits. `residentialProxy`
36
+ * is the Tenant's residential proxy switch; it has no effect yet, and traffic goes direct.
37
+ * Only the Organisation Key or an owner may set these two.
38
+ */
39
+ update(tenant: TenantRef, opts: {
40
+ name?: string;
41
+ metadata?: Record<string, string>;
42
+ status?: string;
43
+ cloudMachineLimit?: number | null;
44
+ residentialProxy?: boolean;
45
+ }): Promise<Tenant>;
46
+ /**
47
+ * Disables the Tenant and revokes every enrolled machine. Cloud machines stay and hibernate:
48
+ * delete them with `tenantMachines.delete`. Its data is kept: erase it from the console.
49
+ */
50
+ delete(tenant: TenantRef): Promise<void>;
51
+ /** A single-use code that enrolls one computer into the Tenant. It expires in 15 minutes. */
52
+ createEnrollment(tenant: TenantRef): Promise<Enrollment>;
53
+ /**
54
+ * Exchange the Organisation Key for a one-hour token that acts as the Tenant. `orgId`
55
+ * defaults to the client's `orgId` (`SAI_ORG_ID`).
56
+ */
57
+ token(tenant: TenantRef, opts?: {
58
+ orgId?: string;
59
+ }): Promise<TenantToken>;
60
+ /**
61
+ * A client that acts as the Tenant. It exchanges a token now, and again before a request
62
+ * when the token has less than five minutes left. Uses this client's Organisation Key.
63
+ */
64
+ client(tenant: TenantRef, opts?: {
65
+ orgId?: string;
66
+ }): Promise<Sai>;
67
+ }
68
+ /** A Tenant's computers, enrolled and cloud. Every call takes an Organisation Key. */
69
+ export declare class TenantMachines {
70
+ private readonly http;
71
+ constructor(http: Http);
72
+ /**
73
+ * Create a cloud machine for the Tenant. `os` is `windows` or `linux`; the size follows it.
74
+ * `name` (up to 64 characters) is a label you show your users.
75
+ *
76
+ * `requestId` (letters, digits, `_` and `-`, up to 128) makes a retry safe: the same id and
77
+ * body return the first machine, and the same id with another body throws `ConflictError`
78
+ * (`request_id_reused`). Without one, every call creates a machine.
79
+ *
80
+ * Returns at once with `state: 'provisioning'`. `wait: true` reads the machine until it is
81
+ * `ready`; it throws `SaiError` if the machine fails or is deleted, and a `TimeoutError`
82
+ * (DOMException) after `timeoutMs` (default 10 minutes).
83
+ */
84
+ create(tenant: TenantRef, opts: {
85
+ os: 'windows' | 'linux';
86
+ name?: string;
87
+ requestId?: string;
88
+ wait?: boolean;
89
+ timeoutMs?: number;
90
+ }): Promise<TenantMachine>;
91
+ /** Both kinds, enrolled and cloud. A cloud machine is listed from the moment it is created. */
92
+ list(tenant: TenantRef): Promise<TenantMachine[]>;
93
+ get(tenant: TenantRef, machineId: string): Promise<TenantMachine>;
94
+ /**
95
+ * An enrolled machine is signed out at once (`'revoked'`); enroll it again with a new code to
96
+ * bring it back. A cloud machine is deleted with its disk (`'deleting'`), which cannot be
97
+ * undone; it leaves the list when the deletion finishes.
98
+ */
99
+ delete(tenant: TenantRef, machine: TenantMachineRef): Promise<'revoked' | 'deleting'>;
100
+ /** `delete` without its answer. On a cloud machine it deletes the machine: use `delete`. */
101
+ revoke(tenant: TenantRef, machine: TenantMachineRef): Promise<void>;
102
+ }
@@ -0,0 +1,157 @@
1
+ import { RateLimitError, SaiError } from './errors.js';
2
+ import { parseEnrollment, parseTenant, parseTenantMachine, parseTenantToken, } from './types.js';
3
+ /** `tenants.client()` exchanges again when its token has less than this left. */
4
+ export const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
5
+ /** Between machine reads while `tenantMachines.create({ wait: true })` waits. */
6
+ const MACHINE_POLL_INTERVAL_MS = 5_000;
7
+ const q = encodeURIComponent;
8
+ const tenantPath = (tenant) => `/v1/tenants/${q(typeof tenant === 'string' ? tenant : tenant.id)}`;
9
+ /** Tenants: your customers, under an Organisation. Every call takes an Organisation Key. */
10
+ export class Tenants {
11
+ http;
12
+ orgId;
13
+ withCredential;
14
+ constructor(http, orgId, withCredential) {
15
+ this.http = http;
16
+ this.orgId = orgId;
17
+ this.withCredential = withCredential;
18
+ }
19
+ /** `externalId` is your id for the customer, unique in the Organisation, and cannot change later. */
20
+ async create(name, opts = {}) {
21
+ return parseTenant(await this.http.json('POST', '/v1/tenants', {
22
+ json: { name, externalId: opts.externalId, metadata: opts.metadata },
23
+ }));
24
+ }
25
+ /**
26
+ * One page, newest first. `externalId` finds the Tenant with that id (zero or one) and
27
+ * cannot be combined with `status`.
28
+ */
29
+ async list(opts = {}) {
30
+ const d = await this.http.json('GET', '/v1/tenants', { params: opts });
31
+ return { tenants: (d.tenants ?? []).map(parseTenant), nextCursor: d.nextCursor ?? null };
32
+ }
33
+ async get(tenant) {
34
+ return parseTenant(await this.http.json('GET', tenantPath(tenant)));
35
+ }
36
+ /**
37
+ * `metadata` replaces the whole map. `status: 'disabled'` stops the Tenant's tokens and
38
+ * machines; `status: 'active'` brings it back.
39
+ *
40
+ * `cloudMachineLimit` (0 to 1000) caps the Tenant's cloud machines; `null` removes the cap.
41
+ * Lowering it deletes nothing: creates are refused until the count fits. `residentialProxy`
42
+ * is the Tenant's residential proxy switch; it has no effect yet, and traffic goes direct.
43
+ * Only the Organisation Key or an owner may set these two.
44
+ */
45
+ async update(tenant, opts) {
46
+ return parseTenant(await this.http.json('PATCH', tenantPath(tenant), { json: opts }));
47
+ }
48
+ /**
49
+ * Disables the Tenant and revokes every enrolled machine. Cloud machines stay and hibernate:
50
+ * delete them with `tenantMachines.delete`. Its data is kept: erase it from the console.
51
+ */
52
+ async delete(tenant) {
53
+ await this.http.request('DELETE', tenantPath(tenant));
54
+ }
55
+ /** A single-use code that enrolls one computer into the Tenant. It expires in 15 minutes. */
56
+ async createEnrollment(tenant) {
57
+ return parseEnrollment(await this.http.json('POST', `${tenantPath(tenant)}/enrollments`));
58
+ }
59
+ /**
60
+ * Exchange the Organisation Key for a one-hour token that acts as the Tenant. `orgId`
61
+ * defaults to the client's `orgId` (`SAI_ORG_ID`).
62
+ */
63
+ async token(tenant, opts = {}) {
64
+ const orgId = opts.orgId ?? this.orgId;
65
+ if (!orgId) {
66
+ throw new Error('No Organisation id: pass orgId here or to new Sai(), or set SAI_ORG_ID.');
67
+ }
68
+ const id = typeof tenant === 'string' ? tenant : tenant.id;
69
+ return parseTenantToken(await this.http.json('POST', `/v1/orgs/${q(orgId)}/tenants/${q(id)}/token`));
70
+ }
71
+ /**
72
+ * A client that acts as the Tenant. It exchanges a token now, and again before a request
73
+ * when the token has less than five minutes left. Uses this client's Organisation Key.
74
+ */
75
+ async client(tenant, opts = {}) {
76
+ let current = await this.token(tenant, opts);
77
+ let pending;
78
+ return this.withCredential(async () => {
79
+ if (current.expiresAt - Date.now() >= TOKEN_REFRESH_MARGIN_MS)
80
+ return current.token;
81
+ pending ??= this.token(tenant, opts).finally(() => {
82
+ pending = undefined;
83
+ });
84
+ current = await pending;
85
+ return current.token;
86
+ });
87
+ }
88
+ }
89
+ /** A Tenant's computers, enrolled and cloud. Every call takes an Organisation Key. */
90
+ export class TenantMachines {
91
+ http;
92
+ constructor(http) {
93
+ this.http = http;
94
+ }
95
+ /**
96
+ * Create a cloud machine for the Tenant. `os` is `windows` or `linux`; the size follows it.
97
+ * `name` (up to 64 characters) is a label you show your users.
98
+ *
99
+ * `requestId` (letters, digits, `_` and `-`, up to 128) makes a retry safe: the same id and
100
+ * body return the first machine, and the same id with another body throws `ConflictError`
101
+ * (`request_id_reused`). Without one, every call creates a machine.
102
+ *
103
+ * Returns at once with `state: 'provisioning'`. `wait: true` reads the machine until it is
104
+ * `ready`; it throws `SaiError` if the machine fails or is deleted, and a `TimeoutError`
105
+ * (DOMException) after `timeoutMs` (default 10 minutes).
106
+ */
107
+ async create(tenant, opts) {
108
+ let machine = parseTenantMachine(await this.http.json('POST', `${tenantPath(tenant)}/machines`, {
109
+ json: { os: opts.os, name: opts.name, requestId: opts.requestId },
110
+ }));
111
+ if (!opts.wait)
112
+ return machine;
113
+ const deadline = Date.now() + (opts.timeoutMs ?? 600_000);
114
+ while (machine.state !== 'ready') {
115
+ if (machine.state === 'failed' || machine.state === 'deleting') {
116
+ throw new SaiError(`Cloud machine ${machine.id} is ${machine.state} and will not become ready.`, { body: machine.raw });
117
+ }
118
+ if (Date.now() + MACHINE_POLL_INTERVAL_MS > deadline) {
119
+ throw new DOMException(`Cloud machine ${machine.id} still ${machine.state}.`, 'TimeoutError');
120
+ }
121
+ await sleep(MACHINE_POLL_INTERVAL_MS);
122
+ try {
123
+ machine = await this.get(tenant, machine.id);
124
+ }
125
+ catch (err) {
126
+ if (!(err instanceof RateLimitError))
127
+ throw err;
128
+ await sleep((err.retryAfter ?? 30) * 1000);
129
+ }
130
+ }
131
+ return machine;
132
+ }
133
+ /** Both kinds, enrolled and cloud. A cloud machine is listed from the moment it is created. */
134
+ async list(tenant) {
135
+ const d = await this.http.json('GET', `${tenantPath(tenant)}/machines`);
136
+ return (d.machines ?? []).map(parseTenantMachine);
137
+ }
138
+ async get(tenant, machineId) {
139
+ return parseTenantMachine(await this.http.json('GET', `${tenantPath(tenant)}/machines/${q(machineId)}`));
140
+ }
141
+ /**
142
+ * An enrolled machine is signed out at once (`'revoked'`); enroll it again with a new code to
143
+ * bring it back. A cloud machine is deleted with its disk (`'deleting'`), which cannot be
144
+ * undone; it leaves the list when the deletion finishes.
145
+ */
146
+ async delete(tenant, machine) {
147
+ const id = typeof machine === 'string' ? machine : machine.id;
148
+ const res = await this.http.request('DELETE', `${tenantPath(tenant)}/machines/${q(id)}`);
149
+ return res.status === 202 ? 'deleting' : 'revoked';
150
+ }
151
+ /** `delete` without its answer. On a cloud machine it deletes the machine: use `delete`. */
152
+ async revoke(tenant, machine) {
153
+ await this.delete(tenant, machine);
154
+ }
155
+ }
156
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
157
+ //# sourceMappingURL=tenants.js.map