@prized/sdk 0.1.2 → 0.1.3
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 +49 -18
- package/dist/cjs/http.d.ts +1 -1
- package/dist/cjs/http.js +2 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/sandbox.d.ts +65 -0
- package/dist/cjs/sandbox.js +100 -4
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/http.d.ts +1 -1
- package/dist/esm/http.js +2 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/sandbox.d.ts +65 -0
- package/dist/esm/sandbox.js +101 -5
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,10 @@ console.log(stdout); // 42
|
|
|
13
13
|
await sandbox.delete();
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Save it as `hello.mjs` and run `node hello.mjs` (Node 18 or later) with
|
|
17
|
+
`PRIZED_API_KEY` set ([Authentication](#authentication)). Top-level `await`
|
|
18
|
+
needs an ES module, so a `.js` file needs `"type": "module"` in package.json.
|
|
19
|
+
|
|
16
20
|
Sandboxes are in beta. A workspace on free credit alone runs them on the
|
|
17
21
|
free tier: up to 10 at once, each up to 2 vCPU and 4 GB of memory and 60
|
|
18
22
|
minutes of running. A top-up or a paid plan lifts the limits: a sandbox then
|
|
@@ -301,9 +305,9 @@ Python's buffering), so a Python program's output arrives as it prints.
|
|
|
301
305
|
Commands run as the user `prized` (home `/home/prized`, no sudo). The
|
|
302
306
|
templates come with git, curl, Python 3 with pip and venv, Node 22 with npm
|
|
303
307
|
and pnpm; `prized-agent-v1` adds Claude Code and Codex. Install what else
|
|
304
|
-
you need into your home:
|
|
305
|
-
|
|
306
|
-
`
|
|
308
|
+
you need into your home: `pip install numpy` goes to `~/.local` (or use a
|
|
309
|
+
venv: `python3 -m venv ~/.venv && ~/.venv/bin/pip install numpy`), and
|
|
310
|
+
`npm install -g` to `~/.local` too.
|
|
307
311
|
|
|
308
312
|
## Background commands
|
|
309
313
|
|
|
@@ -423,11 +427,6 @@ prized sandbox timeout sbx_0123456789abcdef --in 30m # deleted 30 minutes from
|
|
|
423
427
|
On a paid plan an extend stops at 7 days from now; on the free tier neither
|
|
424
428
|
moves the end later.
|
|
425
429
|
|
|
426
|
-
<!-- Held while sandbox ports at a URL are switched off: this section stays
|
|
427
|
-
out of the rendered README until they are on. In the change that turns them
|
|
428
|
-
on, delete these five lines and the closing line after the section, take
|
|
429
|
-
"a sandbox's ports at a URL" out of "Not yet" under Limits, and delete the
|
|
430
|
-
test "the README holds A port at a URL" (test/preview.test.ts).
|
|
431
430
|
## A port at a URL
|
|
432
431
|
|
|
433
432
|
`sandbox.previewUrl(port)` is the https URL a port of the sandbox answers at:
|
|
@@ -454,15 +453,47 @@ the preview URL, and one that points anywhere else comes back as
|
|
|
454
453
|
`Prized-Location` instead, which no client follows. To follow a redirect,
|
|
455
454
|
resolve its `Location` against the preview URL and send `previewHeaders()`
|
|
456
455
|
again only when the result has the preview URL's origin, scheme included.
|
|
457
|
-
Call preview URLs from server-side code: a browser hides
|
|
458
|
-
`'manual'`.
|
|
456
|
+
Call preview URLs with the key from server-side code: a browser hides
|
|
457
|
+
`Location` from `'manual'`.
|
|
459
458
|
|
|
460
459
|
Nothing is created: the URL is the sandbox's own, and every port has one
|
|
461
|
-
(but 22 and 81). A request to a paused sandbox resumes it and
|
|
462
|
-
seconds for it, as `exec` does; send
|
|
463
|
-
`503` instead. `previewUrl` throws
|
|
464
|
-
turned on.
|
|
465
|
-
|
|
460
|
+
(but 22 and 81). A request with the key to a paused sandbox resumes it and
|
|
461
|
+
waits up to 10 seconds for it, as `exec` does; send
|
|
462
|
+
`Prized-Auto-Resume: false` to get `503` instead. `previewUrl` throws
|
|
463
|
+
`preview_unavailable` where this is not turned on.
|
|
464
|
+
|
|
465
|
+
A person in a browser, or a client that cannot send the key in a header (a
|
|
466
|
+
WebSocket from a browser page, a program you hand one port to and not your
|
|
467
|
+
key), takes a short-lived token in the URL instead:
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
const { browserUrl, url, expiresAt } = await sandbox.previewToken(3000, { expiresIn: 600 });
|
|
471
|
+
// browserUrl: https://3000-sbx-….prized.link/?prized_token=… opens in a tab or an iframe
|
|
472
|
+
const socket = new WebSocket(url.replace('https:', 'wss:'));
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`browserUrl` is the port on `prized.link`, a domain of its own for pages your
|
|
476
|
+
sandboxes serve: open it in a tab or embed it in an iframe. The browser swaps
|
|
477
|
+
the token for a cookie for that one host, so the page's own assets, fetches
|
|
478
|
+
and WebSockets work, and a tab's first visit shows a short notice that the
|
|
479
|
+
page is hosted by a Prized user. `url` is the same token on the preview URL,
|
|
480
|
+
for programs. The token opens that one port until `expiresAt` (an hour by
|
|
481
|
+
default, a day at most), or until your key is revoked; the app never sees
|
|
482
|
+
it. To let anyone reach a port with no credential at all, make it public:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const { browserUrl } = await sandbox.setPortPublic(3000, true, { expiresIn: 7 * 86400 });
|
|
486
|
+
await sandbox.setPortPublic(3000, false); // private again
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Tokens and public ports need a paid workspace (`share_links_need_payment`
|
|
490
|
+
otherwise), and making a port public takes the workspace owner's key. On the
|
|
491
|
+
preview URL they answer programs, `fetch` and WebSockets but not a page
|
|
492
|
+
opened in a browser tab (`403 ingress_browser_page`, which names the
|
|
493
|
+
`browserUrl`); on `browserUrl` they open in a browser too. Neither wakes a
|
|
494
|
+
paused sandbox: a request gets `503` with `error: 'sandbox_not_running'`
|
|
495
|
+
until something with the key resumes it. Making a port public or private
|
|
496
|
+
again ends the tokens made for it.
|
|
466
497
|
|
|
467
498
|
## Fork
|
|
468
499
|
|
|
@@ -538,7 +569,7 @@ try {
|
|
|
538
569
|
| `sandbox_not_running` | 409 | The sandbox is not running and the call could not wake it: it is resizing (`refresh()` until it leaves `resizing`, then retry), failed, or being deleted. A paused sandbox is resumed by any command or file call. The SDK never turns waking off; a request that carries `Prized-Auto-Resume: false` (added by a custom `fetch`, say) gets this for a paused or starting sandbox too: drop the header to let the call wake it and wait. |
|
|
539
570
|
| `sandbox_not_ready` | 503 | A paused or starting sandbox did not reach running within the edge's 10-second wait. The wait starts when the edge, having checked the key, finds the sandbox not running, and calls with the same key arriving during it share it, so a later call may wait less. Nothing ran; retry. |
|
|
540
571
|
| `parent_not_running` | 409 | Only a running or paused sandbox can be forked. |
|
|
541
|
-
| `parent_not_forkable` | 409 | This sandbox cannot be forked. `reason` `resized_while_paused`: it has no memory to copy; resume it, then fork it. `reason` `parent_too_big`: its machine cannot hold even one copy beside it; fork a smaller sandbox. |
|
|
572
|
+
| `parent_not_forkable` | 409 | This sandbox cannot be forked. `reason` `resized_while_paused` or `memory_discarded`: it has no memory to copy; resume it, then fork it. `reason` `parent_too_big`: its machine cannot hold even one copy beside it; fork a smaller sandbox. |
|
|
542
573
|
| `fork_capacity` | 409 | The parent's machine has room for only `fits` more copies (`err.body?.fits`). If `fits` is above 0, fork that many or fewer; or try again later, or start a new sandbox and fork that one, keeping the parent until it runs. The new sandbox and each copy count toward your quota: at the limit, delete sandboxes you no longer need with `delete({ wait: true })` (a deleting sandbox still counts), or top up on the free tier. Copy out what you need first, and never delete a parent while a copy forked from it is still starting. |
|
|
543
574
|
| `fork_unsupported_host` | 409 | The parent's machine cannot start copies of it. With `reason` `host_retiring` (`err.reason`), the machine is being replaced: it keeps the parent but starts no new sandboxes, so no retry or smaller `count` helps. Create a new sandbox, set it up again, and fork that one. |
|
|
544
575
|
| `sandbox_capacity` | 503 | No sandbox capacity is free right now. Nothing was created or billed; the SDK retries it after `Retry-After`. |
|
|
@@ -666,8 +697,8 @@ runtime) are on the pricing and docs pages at prized.dev; a create or resume
|
|
|
666
697
|
past one fails with `sandbox_quota_exceeded`.
|
|
667
698
|
|
|
668
699
|
Not yet: custom templates (from a Dockerfile), egress allow and deny lists,
|
|
669
|
-
a
|
|
670
|
-
other than US West.
|
|
700
|
+
a port URL that opens without an API key (for a browser or a webhook), more
|
|
701
|
+
than 32 vCPU or 128 GB, GPUs, and regions other than US West.
|
|
671
702
|
|
|
672
703
|
## Runtimes
|
|
673
704
|
|
package/dist/cjs/http.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export type RetryMode = 'safe' | 'exec' | 'staged';
|
|
|
130
130
|
export declare function neverSent(error: unknown, depth?: number): boolean;
|
|
131
131
|
export declare function retryable(error: PrizedError, mode: RetryMode, method: RequestSpec['method']): boolean;
|
|
132
132
|
export interface RequestSpec {
|
|
133
|
-
method: 'GET' | 'POST' | 'DELETE';
|
|
133
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
134
134
|
/** The URL, or one built per attempt from the ms left before `deadline`
|
|
135
135
|
* (Infinity without one): a held request asks only for what is left. */
|
|
136
136
|
url: string | ((leftMs: number) => string);
|
package/dist/cjs/http.js
CHANGED
|
@@ -248,6 +248,8 @@ const NEVER_RETRY = new Set([
|
|
|
248
248
|
'idempotency_conflict',
|
|
249
249
|
'paid_balance_required',
|
|
250
250
|
'insufficient_balance',
|
|
251
|
+
// A 503 that says preview links are off on this deployment: no wait turns them on.
|
|
252
|
+
'preview_unavailable',
|
|
251
253
|
]);
|
|
252
254
|
const EXEC_UNDELIVERED = new Set([
|
|
253
255
|
'rate_limited',
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Sandbox, DEFAULT_TEMPLATE, PREVIEW_KEY_HEADER } from './sandbox.js';
|
|
2
|
-
export type { CreateOptions, ForkOptions, ListOptions, WaitableOptions } from './sandbox.js';
|
|
2
|
+
export type { CreateOptions, ForkOptions, ListOptions, PortPreview, PreviewToken, PreviewTokenOptions, SetPortPublicOptions, WaitableOptions, } from './sandbox.js';
|
|
3
3
|
export { CommandHandle, DEFAULT_EXEC_TIMEOUT_MS, MAX_BLOCKING_EXEC_MS } from './commands.js';
|
|
4
4
|
export type { CommandStatus, ExecOptions, WaitOptions } from './commands.js';
|
|
5
5
|
export { Files } from './files.js';
|
package/dist/cjs/sandbox.d.ts
CHANGED
|
@@ -4,6 +4,42 @@ import { type ClientOptions } from './http.js';
|
|
|
4
4
|
import { type ExecResult, type Resources, type ResourcesInput, type SandboxInfo, type SandboxState, type Template } from './types.js';
|
|
5
5
|
/** The header a request to a sandbox port's URL carries the API key in. */
|
|
6
6
|
export declare const PREVIEW_KEY_HEADER = "Prized-Key";
|
|
7
|
+
/** A sandbox port's preview link (`setPortPublic`): its URL, and whether
|
|
8
|
+
* it is public, until when. */
|
|
9
|
+
export interface PortPreview {
|
|
10
|
+
sandboxId: string;
|
|
11
|
+
port: number;
|
|
12
|
+
/** The port's URL for a program with the API key (`previewUrl`). */
|
|
13
|
+
url: string;
|
|
14
|
+
/** The same port on prized.link, which opens in a browser tab once the
|
|
15
|
+
* port is public; null where the deployment has no browser domain. */
|
|
16
|
+
browserUrl: string | null;
|
|
17
|
+
/** Anyone with the URL reaches the port, no credential needed. */
|
|
18
|
+
public: boolean;
|
|
19
|
+
/** When the port goes private again on its own; null while private. */
|
|
20
|
+
publicExpiresAt: Date | null;
|
|
21
|
+
}
|
|
22
|
+
/** A short-lived URL token for a sandbox port (`previewToken`). */
|
|
23
|
+
export interface PreviewToken {
|
|
24
|
+
/** The preview URL with the token in its `prized_token` query parameter. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** The same on prized.link, to open in a browser tab or a frame; null
|
|
27
|
+
* where the deployment has no browser domain. */
|
|
28
|
+
browserUrl: string | null;
|
|
29
|
+
token: string;
|
|
30
|
+
expiresAt: Date;
|
|
31
|
+
}
|
|
32
|
+
export interface PreviewTokenOptions {
|
|
33
|
+
/** How long the token opens the port, in seconds: 60 to 86 400. Default 3600 (an hour). */
|
|
34
|
+
expiresIn?: number;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
export interface SetPortPublicOptions {
|
|
38
|
+
/** With `true`: how long the port stays public, in seconds: 300 to
|
|
39
|
+
* 2 592 000 (30 days). Default 7 days. */
|
|
40
|
+
expiresIn?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
7
43
|
/** The template a create uses when it names none. */
|
|
8
44
|
export declare const DEFAULT_TEMPLATE = "prized-base-v1";
|
|
9
45
|
/** How long create, resume and fork wait for `running` by default. */
|
|
@@ -194,6 +230,35 @@ export declare class Sandbox {
|
|
|
194
230
|
* `redirect: 'manual'`: fetch resends custom headers to whatever host a
|
|
195
231
|
* redirect names, so only ever send them to the sandbox's preview URL. */
|
|
196
232
|
previewHeaders(): Record<string, string>;
|
|
233
|
+
/**
|
|
234
|
+
* A short-lived token for `port`, for a client that cannot send
|
|
235
|
+
* `Prized-Key` in a header: a person in a browser tab, a WebSocket opened
|
|
236
|
+
* from a browser page, or a program you hand this one port to and not
|
|
237
|
+
* your API key. `browserUrl` is the port on prized.link with the token in
|
|
238
|
+
* its `prized_token` query parameter: it opens in a browser tab or a
|
|
239
|
+
* frame (the token moves into a cookie for that host; a tab's first visit
|
|
240
|
+
* shows a notice). `url` is the same on the preview URL, for programs. The
|
|
241
|
+
* app never sees the token. It opens the port until `expiresAt`
|
|
242
|
+
* (`expiresIn` seconds, 60 to 86 400, an hour by default), or until this
|
|
243
|
+
* client's key is revoked; making the port public or private again ends
|
|
244
|
+
* it too. It never wakes a paused sandbox (`503 sandbox_not_running`).
|
|
245
|
+
* Needs a paid workspace (`share_links_need_payment` otherwise).
|
|
246
|
+
*/
|
|
247
|
+
previewToken(port: number, options?: PreviewTokenOptions): Promise<PreviewToken>;
|
|
248
|
+
/**
|
|
249
|
+
* Make `port` public (anyone with its URL reaches it, with no
|
|
250
|
+
* credential) for `expiresIn` seconds (300 to 2 592 000, 7 days by
|
|
251
|
+
* default), or private again with `false`. Public needs the workspace
|
|
252
|
+
* owner's key and a paid workspace, and counts toward the workspace's 10
|
|
253
|
+
* public ports and links. The answer's `browserUrl` (prized.link) opens in
|
|
254
|
+
* a browser tab (a tab's first visit shows a notice); `url` answers
|
|
255
|
+
* programs, `fetch` and WebSockets. A public port never wakes a paused
|
|
256
|
+
* sandbox. A port already public keeps its expiry: make it private, then
|
|
257
|
+
* public again, to change it.
|
|
258
|
+
*/
|
|
259
|
+
setPortPublic(port: number, isPublic: boolean, options?: SetPortPublicOptions): Promise<PortPreview>;
|
|
260
|
+
/** A port a preview link may name: 1 to 65 535, never 22 or 81 (the sandbox's own). */
|
|
261
|
+
private previewPort;
|
|
197
262
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
198
263
|
* held by the API until the sandbox is that (each attempt asks only for
|
|
199
264
|
* what is left). */
|
package/dist/cjs/sandbox.js
CHANGED
|
@@ -76,6 +76,13 @@ function wholeMinutes(name, value, allowZero) {
|
|
|
76
76
|
}
|
|
77
77
|
return value;
|
|
78
78
|
}
|
|
79
|
+
function wholeSeconds(name, value) {
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
83
|
+
throw new errors_js_1.PrizedError('invalid_argument', `${name} must be a whole number of seconds > 0`);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
79
86
|
function apiUrl(config, path) {
|
|
80
87
|
return `${config.baseUrl}/api/v1${path}`;
|
|
81
88
|
}
|
|
@@ -561,9 +568,13 @@ class Sandbox {
|
|
|
561
568
|
body.maxRuntimeMin = maxRuntimeMin;
|
|
562
569
|
const idempotencyKey = options.idempotencyKey ?? randomKey();
|
|
563
570
|
const wire = snapshot(body); // as in create
|
|
571
|
+
const path = `${sandboxPathFor(this.id)}/fork`;
|
|
572
|
+
// Held by the API until every copy runs (or its bound), as create is,
|
|
573
|
+
// instead of polled; the cleanup's replay only wants the ids.
|
|
574
|
+
const held = options.wait !== false;
|
|
564
575
|
const send = (deadline, attempts) => (0, http_js_1.request)(this.config, {
|
|
565
576
|
method: 'POST',
|
|
566
|
-
url: apiUrl(this.config, `${
|
|
577
|
+
url: (leftMs) => apiUrl(this.config, held && attempts !== undefined ? `${path}${holdQuery(this.config, 'running', leftMs)}` : path),
|
|
567
578
|
json: wire,
|
|
568
579
|
headers: { 'idempotency-key': idempotencyKey },
|
|
569
580
|
retry: 'safe',
|
|
@@ -686,9 +697,7 @@ class Sandbox {
|
|
|
686
697
|
* `preview_unavailable` when the deployment has sandbox ingress off.
|
|
687
698
|
*/
|
|
688
699
|
previewUrl(port) {
|
|
689
|
-
|
|
690
|
-
throw new errors_js_1.PrizedError('bad_request', `port must be a whole number from 1 to 65535 other than 22 and 81, not ${String(port)}`, { sandboxId: this.id });
|
|
691
|
-
}
|
|
700
|
+
this.previewPort(port);
|
|
692
701
|
const template = this.current.raw.previewUrlTemplate;
|
|
693
702
|
if (typeof template !== 'string' || !template.includes('{port}')) {
|
|
694
703
|
throw new errors_js_1.PrizedError('preview_unavailable', 'sandbox ports at a URL are not turned on for this deployment', { sandboxId: this.id });
|
|
@@ -702,6 +711,93 @@ class Sandbox {
|
|
|
702
711
|
previewHeaders() {
|
|
703
712
|
return { [exports.PREVIEW_KEY_HEADER]: this.config.apiKey };
|
|
704
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* A short-lived token for `port`, for a client that cannot send
|
|
716
|
+
* `Prized-Key` in a header: a person in a browser tab, a WebSocket opened
|
|
717
|
+
* from a browser page, or a program you hand this one port to and not
|
|
718
|
+
* your API key. `browserUrl` is the port on prized.link with the token in
|
|
719
|
+
* its `prized_token` query parameter: it opens in a browser tab or a
|
|
720
|
+
* frame (the token moves into a cookie for that host; a tab's first visit
|
|
721
|
+
* shows a notice). `url` is the same on the preview URL, for programs. The
|
|
722
|
+
* app never sees the token. It opens the port until `expiresAt`
|
|
723
|
+
* (`expiresIn` seconds, 60 to 86 400, an hour by default), or until this
|
|
724
|
+
* client's key is revoked; making the port public or private again ends
|
|
725
|
+
* it too. It never wakes a paused sandbox (`503 sandbox_not_running`).
|
|
726
|
+
* Needs a paid workspace (`share_links_need_payment` otherwise).
|
|
727
|
+
*/
|
|
728
|
+
async previewToken(port, options = {}) {
|
|
729
|
+
this.previewPort(port);
|
|
730
|
+
const expiresIn = wholeSeconds('expiresIn', options.expiresIn);
|
|
731
|
+
const result = await (0, http_js_1.request)(this.config, {
|
|
732
|
+
method: 'POST',
|
|
733
|
+
url: apiUrl(this.config, `${sandboxPathFor(this.id)}/ports/${port}/token`),
|
|
734
|
+
json: expiresIn === undefined ? {} : { expiresIn },
|
|
735
|
+
// A second copy mints a second token nobody holds, which expires on its own.
|
|
736
|
+
retry: 'safe',
|
|
737
|
+
signal: options.signal,
|
|
738
|
+
deadline: (0, http_js_1.callDeadline)(this.config),
|
|
739
|
+
sandboxId: this.id,
|
|
740
|
+
});
|
|
741
|
+
const body = result.json;
|
|
742
|
+
const expiresAt = (0, types_js_1.parseTime)(body?.expiresAt);
|
|
743
|
+
if (typeof body?.url !== 'string' || typeof body.token !== 'string' || expiresAt === null) {
|
|
744
|
+
throw new errors_js_1.PrizedError('bad_response', 'the API answered a preview token without its url, token or expiry', { sandboxId: this.id });
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
url: body.url,
|
|
748
|
+
browserUrl: typeof body.browserUrl === 'string' ? body.browserUrl : null,
|
|
749
|
+
token: body.token,
|
|
750
|
+
expiresAt,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Make `port` public (anyone with its URL reaches it, with no
|
|
755
|
+
* credential) for `expiresIn` seconds (300 to 2 592 000, 7 days by
|
|
756
|
+
* default), or private again with `false`. Public needs the workspace
|
|
757
|
+
* owner's key and a paid workspace, and counts toward the workspace's 10
|
|
758
|
+
* public ports and links. The answer's `browserUrl` (prized.link) opens in
|
|
759
|
+
* a browser tab (a tab's first visit shows a notice); `url` answers
|
|
760
|
+
* programs, `fetch` and WebSockets. A public port never wakes a paused
|
|
761
|
+
* sandbox. A port already public keeps its expiry: make it private, then
|
|
762
|
+
* public again, to change it.
|
|
763
|
+
*/
|
|
764
|
+
async setPortPublic(port, isPublic, options = {}) {
|
|
765
|
+
this.previewPort(port);
|
|
766
|
+
if (typeof isPublic !== 'boolean')
|
|
767
|
+
throw new errors_js_1.PrizedError('invalid_argument', 'isPublic must be true or false', { sandboxId: this.id });
|
|
768
|
+
const expiresIn = wholeSeconds('expiresIn', options.expiresIn);
|
|
769
|
+
if (expiresIn !== undefined && !isPublic) {
|
|
770
|
+
throw new errors_js_1.PrizedError('invalid_argument', 'expiresIn applies only when making a port public', { sandboxId: this.id });
|
|
771
|
+
}
|
|
772
|
+
const result = await (0, http_js_1.request)(this.config, {
|
|
773
|
+
method: 'PUT',
|
|
774
|
+
url: apiUrl(this.config, `${sandboxPathFor(this.id)}/ports/${port}`),
|
|
775
|
+
json: isPublic ? { public: true, ...(expiresIn === undefined ? {} : { expiresIn }) } : { public: false },
|
|
776
|
+
// Setting the same state again changes nothing.
|
|
777
|
+
retry: 'safe',
|
|
778
|
+
signal: options.signal,
|
|
779
|
+
deadline: (0, http_js_1.callDeadline)(this.config),
|
|
780
|
+
sandboxId: this.id,
|
|
781
|
+
});
|
|
782
|
+
const doc = result.json?.preview;
|
|
783
|
+
if (doc === undefined || typeof doc.url !== 'string' || typeof doc.public !== 'boolean') {
|
|
784
|
+
throw new errors_js_1.PrizedError('bad_response', 'the API answered a port without its url or public setting', { sandboxId: this.id });
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
sandboxId: typeof doc.sandboxId === 'string' ? doc.sandboxId : this.id,
|
|
788
|
+
port: typeof doc.port === 'number' ? doc.port : port,
|
|
789
|
+
url: doc.url,
|
|
790
|
+
browserUrl: typeof doc.browserUrl === 'string' ? doc.browserUrl : null,
|
|
791
|
+
public: doc.public,
|
|
792
|
+
publicExpiresAt: (0, types_js_1.parseTime)(doc.publicExpiresAt),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
/** A port a preview link may name: 1 to 65 535, never 22 or 81 (the sandbox's own). */
|
|
796
|
+
previewPort(port) {
|
|
797
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535 || port === 22 || port === 81) {
|
|
798
|
+
throw new errors_js_1.PrizedError('bad_request', `port must be a whole number from 1 to 65535 other than 22 and 81, not ${String(port)}`, { sandboxId: this.id });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
705
801
|
// --- internals ------------------------------------------------------------------
|
|
706
802
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
707
803
|
* held by the API until the sandbox is that (each attempt asks only for
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version; test/version.test.ts holds it equal to package.json. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.3";
|
package/dist/cjs/version.js
CHANGED
package/dist/esm/http.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export type RetryMode = 'safe' | 'exec' | 'staged';
|
|
|
130
130
|
export declare function neverSent(error: unknown, depth?: number): boolean;
|
|
131
131
|
export declare function retryable(error: PrizedError, mode: RetryMode, method: RequestSpec['method']): boolean;
|
|
132
132
|
export interface RequestSpec {
|
|
133
|
-
method: 'GET' | 'POST' | 'DELETE';
|
|
133
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
134
134
|
/** The URL, or one built per attempt from the ms left before `deadline`
|
|
135
135
|
* (Infinity without one): a held request asks only for what is left. */
|
|
136
136
|
url: string | ((leftMs: number) => string);
|
package/dist/esm/http.js
CHANGED
|
@@ -232,6 +232,8 @@ const NEVER_RETRY = new Set([
|
|
|
232
232
|
'idempotency_conflict',
|
|
233
233
|
'paid_balance_required',
|
|
234
234
|
'insufficient_balance',
|
|
235
|
+
// A 503 that says preview links are off on this deployment: no wait turns them on.
|
|
236
|
+
'preview_unavailable',
|
|
235
237
|
]);
|
|
236
238
|
const EXEC_UNDELIVERED = new Set([
|
|
237
239
|
'rate_limited',
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Sandbox, DEFAULT_TEMPLATE, PREVIEW_KEY_HEADER } from './sandbox.js';
|
|
2
|
-
export type { CreateOptions, ForkOptions, ListOptions, WaitableOptions } from './sandbox.js';
|
|
2
|
+
export type { CreateOptions, ForkOptions, ListOptions, PortPreview, PreviewToken, PreviewTokenOptions, SetPortPublicOptions, WaitableOptions, } from './sandbox.js';
|
|
3
3
|
export { CommandHandle, DEFAULT_EXEC_TIMEOUT_MS, MAX_BLOCKING_EXEC_MS } from './commands.js';
|
|
4
4
|
export type { CommandStatus, ExecOptions, WaitOptions } from './commands.js';
|
|
5
5
|
export { Files } from './files.js';
|
package/dist/esm/sandbox.d.ts
CHANGED
|
@@ -4,6 +4,42 @@ import { type ClientOptions } from './http.js';
|
|
|
4
4
|
import { type ExecResult, type Resources, type ResourcesInput, type SandboxInfo, type SandboxState, type Template } from './types.js';
|
|
5
5
|
/** The header a request to a sandbox port's URL carries the API key in. */
|
|
6
6
|
export declare const PREVIEW_KEY_HEADER = "Prized-Key";
|
|
7
|
+
/** A sandbox port's preview link (`setPortPublic`): its URL, and whether
|
|
8
|
+
* it is public, until when. */
|
|
9
|
+
export interface PortPreview {
|
|
10
|
+
sandboxId: string;
|
|
11
|
+
port: number;
|
|
12
|
+
/** The port's URL for a program with the API key (`previewUrl`). */
|
|
13
|
+
url: string;
|
|
14
|
+
/** The same port on prized.link, which opens in a browser tab once the
|
|
15
|
+
* port is public; null where the deployment has no browser domain. */
|
|
16
|
+
browserUrl: string | null;
|
|
17
|
+
/** Anyone with the URL reaches the port, no credential needed. */
|
|
18
|
+
public: boolean;
|
|
19
|
+
/** When the port goes private again on its own; null while private. */
|
|
20
|
+
publicExpiresAt: Date | null;
|
|
21
|
+
}
|
|
22
|
+
/** A short-lived URL token for a sandbox port (`previewToken`). */
|
|
23
|
+
export interface PreviewToken {
|
|
24
|
+
/** The preview URL with the token in its `prized_token` query parameter. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** The same on prized.link, to open in a browser tab or a frame; null
|
|
27
|
+
* where the deployment has no browser domain. */
|
|
28
|
+
browserUrl: string | null;
|
|
29
|
+
token: string;
|
|
30
|
+
expiresAt: Date;
|
|
31
|
+
}
|
|
32
|
+
export interface PreviewTokenOptions {
|
|
33
|
+
/** How long the token opens the port, in seconds: 60 to 86 400. Default 3600 (an hour). */
|
|
34
|
+
expiresIn?: number;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
export interface SetPortPublicOptions {
|
|
38
|
+
/** With `true`: how long the port stays public, in seconds: 300 to
|
|
39
|
+
* 2 592 000 (30 days). Default 7 days. */
|
|
40
|
+
expiresIn?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
7
43
|
/** The template a create uses when it names none. */
|
|
8
44
|
export declare const DEFAULT_TEMPLATE = "prized-base-v1";
|
|
9
45
|
/** How long create, resume and fork wait for `running` by default. */
|
|
@@ -194,6 +230,35 @@ export declare class Sandbox {
|
|
|
194
230
|
* `redirect: 'manual'`: fetch resends custom headers to whatever host a
|
|
195
231
|
* redirect names, so only ever send them to the sandbox's preview URL. */
|
|
196
232
|
previewHeaders(): Record<string, string>;
|
|
233
|
+
/**
|
|
234
|
+
* A short-lived token for `port`, for a client that cannot send
|
|
235
|
+
* `Prized-Key` in a header: a person in a browser tab, a WebSocket opened
|
|
236
|
+
* from a browser page, or a program you hand this one port to and not
|
|
237
|
+
* your API key. `browserUrl` is the port on prized.link with the token in
|
|
238
|
+
* its `prized_token` query parameter: it opens in a browser tab or a
|
|
239
|
+
* frame (the token moves into a cookie for that host; a tab's first visit
|
|
240
|
+
* shows a notice). `url` is the same on the preview URL, for programs. The
|
|
241
|
+
* app never sees the token. It opens the port until `expiresAt`
|
|
242
|
+
* (`expiresIn` seconds, 60 to 86 400, an hour by default), or until this
|
|
243
|
+
* client's key is revoked; making the port public or private again ends
|
|
244
|
+
* it too. It never wakes a paused sandbox (`503 sandbox_not_running`).
|
|
245
|
+
* Needs a paid workspace (`share_links_need_payment` otherwise).
|
|
246
|
+
*/
|
|
247
|
+
previewToken(port: number, options?: PreviewTokenOptions): Promise<PreviewToken>;
|
|
248
|
+
/**
|
|
249
|
+
* Make `port` public (anyone with its URL reaches it, with no
|
|
250
|
+
* credential) for `expiresIn` seconds (300 to 2 592 000, 7 days by
|
|
251
|
+
* default), or private again with `false`. Public needs the workspace
|
|
252
|
+
* owner's key and a paid workspace, and counts toward the workspace's 10
|
|
253
|
+
* public ports and links. The answer's `browserUrl` (prized.link) opens in
|
|
254
|
+
* a browser tab (a tab's first visit shows a notice); `url` answers
|
|
255
|
+
* programs, `fetch` and WebSockets. A public port never wakes a paused
|
|
256
|
+
* sandbox. A port already public keeps its expiry: make it private, then
|
|
257
|
+
* public again, to change it.
|
|
258
|
+
*/
|
|
259
|
+
setPortPublic(port: number, isPublic: boolean, options?: SetPortPublicOptions): Promise<PortPreview>;
|
|
260
|
+
/** A port a preview link may name: 1 to 65 535, never 22 or 81 (the sandbox's own). */
|
|
261
|
+
private previewPort;
|
|
197
262
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
198
263
|
* held by the API until the sandbox is that (each attempt asks only for
|
|
199
264
|
* what is left). */
|
package/dist/esm/sandbox.js
CHANGED
|
@@ -4,7 +4,7 @@ import { execCommand } from './commands.js';
|
|
|
4
4
|
import { PrizedError } from './errors.js';
|
|
5
5
|
import { Files } from './files.js';
|
|
6
6
|
import { callDeadline, cutByDeadline, MAX_RETRY_AFTER_MS, Polls, request, resolveConfig, toolboxBaseFor, warmEdge, } from './http.js';
|
|
7
|
-
import { parseSandbox, parseTemplate, resourcesToWire, } from './types.js';
|
|
7
|
+
import { parseSandbox, parseTemplate, parseTime, resourcesToWire, } from './types.js';
|
|
8
8
|
/** The header a request to a sandbox port's URL carries the API key in. */
|
|
9
9
|
export const PREVIEW_KEY_HEADER = 'Prized-Key';
|
|
10
10
|
/** The template a create uses when it names none. */
|
|
@@ -73,6 +73,13 @@ function wholeMinutes(name, value, allowZero) {
|
|
|
73
73
|
}
|
|
74
74
|
return value;
|
|
75
75
|
}
|
|
76
|
+
function wholeSeconds(name, value) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
80
|
+
throw new PrizedError('invalid_argument', `${name} must be a whole number of seconds > 0`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
76
83
|
function apiUrl(config, path) {
|
|
77
84
|
return `${config.baseUrl}/api/v1${path}`;
|
|
78
85
|
}
|
|
@@ -558,9 +565,13 @@ export class Sandbox {
|
|
|
558
565
|
body.maxRuntimeMin = maxRuntimeMin;
|
|
559
566
|
const idempotencyKey = options.idempotencyKey ?? randomKey();
|
|
560
567
|
const wire = snapshot(body); // as in create
|
|
568
|
+
const path = `${sandboxPathFor(this.id)}/fork`;
|
|
569
|
+
// Held by the API until every copy runs (or its bound), as create is,
|
|
570
|
+
// instead of polled; the cleanup's replay only wants the ids.
|
|
571
|
+
const held = options.wait !== false;
|
|
561
572
|
const send = (deadline, attempts) => request(this.config, {
|
|
562
573
|
method: 'POST',
|
|
563
|
-
url: apiUrl(this.config, `${
|
|
574
|
+
url: (leftMs) => apiUrl(this.config, held && attempts !== undefined ? `${path}${holdQuery(this.config, 'running', leftMs)}` : path),
|
|
564
575
|
json: wire,
|
|
565
576
|
headers: { 'idempotency-key': idempotencyKey },
|
|
566
577
|
retry: 'safe',
|
|
@@ -683,9 +694,7 @@ export class Sandbox {
|
|
|
683
694
|
* `preview_unavailable` when the deployment has sandbox ingress off.
|
|
684
695
|
*/
|
|
685
696
|
previewUrl(port) {
|
|
686
|
-
|
|
687
|
-
throw new PrizedError('bad_request', `port must be a whole number from 1 to 65535 other than 22 and 81, not ${String(port)}`, { sandboxId: this.id });
|
|
688
|
-
}
|
|
697
|
+
this.previewPort(port);
|
|
689
698
|
const template = this.current.raw.previewUrlTemplate;
|
|
690
699
|
if (typeof template !== 'string' || !template.includes('{port}')) {
|
|
691
700
|
throw new PrizedError('preview_unavailable', 'sandbox ports at a URL are not turned on for this deployment', { sandboxId: this.id });
|
|
@@ -699,6 +708,93 @@ export class Sandbox {
|
|
|
699
708
|
previewHeaders() {
|
|
700
709
|
return { [PREVIEW_KEY_HEADER]: this.config.apiKey };
|
|
701
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* A short-lived token for `port`, for a client that cannot send
|
|
713
|
+
* `Prized-Key` in a header: a person in a browser tab, a WebSocket opened
|
|
714
|
+
* from a browser page, or a program you hand this one port to and not
|
|
715
|
+
* your API key. `browserUrl` is the port on prized.link with the token in
|
|
716
|
+
* its `prized_token` query parameter: it opens in a browser tab or a
|
|
717
|
+
* frame (the token moves into a cookie for that host; a tab's first visit
|
|
718
|
+
* shows a notice). `url` is the same on the preview URL, for programs. The
|
|
719
|
+
* app never sees the token. It opens the port until `expiresAt`
|
|
720
|
+
* (`expiresIn` seconds, 60 to 86 400, an hour by default), or until this
|
|
721
|
+
* client's key is revoked; making the port public or private again ends
|
|
722
|
+
* it too. It never wakes a paused sandbox (`503 sandbox_not_running`).
|
|
723
|
+
* Needs a paid workspace (`share_links_need_payment` otherwise).
|
|
724
|
+
*/
|
|
725
|
+
async previewToken(port, options = {}) {
|
|
726
|
+
this.previewPort(port);
|
|
727
|
+
const expiresIn = wholeSeconds('expiresIn', options.expiresIn);
|
|
728
|
+
const result = await request(this.config, {
|
|
729
|
+
method: 'POST',
|
|
730
|
+
url: apiUrl(this.config, `${sandboxPathFor(this.id)}/ports/${port}/token`),
|
|
731
|
+
json: expiresIn === undefined ? {} : { expiresIn },
|
|
732
|
+
// A second copy mints a second token nobody holds, which expires on its own.
|
|
733
|
+
retry: 'safe',
|
|
734
|
+
signal: options.signal,
|
|
735
|
+
deadline: callDeadline(this.config),
|
|
736
|
+
sandboxId: this.id,
|
|
737
|
+
});
|
|
738
|
+
const body = result.json;
|
|
739
|
+
const expiresAt = parseTime(body?.expiresAt);
|
|
740
|
+
if (typeof body?.url !== 'string' || typeof body.token !== 'string' || expiresAt === null) {
|
|
741
|
+
throw new PrizedError('bad_response', 'the API answered a preview token without its url, token or expiry', { sandboxId: this.id });
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
url: body.url,
|
|
745
|
+
browserUrl: typeof body.browserUrl === 'string' ? body.browserUrl : null,
|
|
746
|
+
token: body.token,
|
|
747
|
+
expiresAt,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Make `port` public (anyone with its URL reaches it, with no
|
|
752
|
+
* credential) for `expiresIn` seconds (300 to 2 592 000, 7 days by
|
|
753
|
+
* default), or private again with `false`. Public needs the workspace
|
|
754
|
+
* owner's key and a paid workspace, and counts toward the workspace's 10
|
|
755
|
+
* public ports and links. The answer's `browserUrl` (prized.link) opens in
|
|
756
|
+
* a browser tab (a tab's first visit shows a notice); `url` answers
|
|
757
|
+
* programs, `fetch` and WebSockets. A public port never wakes a paused
|
|
758
|
+
* sandbox. A port already public keeps its expiry: make it private, then
|
|
759
|
+
* public again, to change it.
|
|
760
|
+
*/
|
|
761
|
+
async setPortPublic(port, isPublic, options = {}) {
|
|
762
|
+
this.previewPort(port);
|
|
763
|
+
if (typeof isPublic !== 'boolean')
|
|
764
|
+
throw new PrizedError('invalid_argument', 'isPublic must be true or false', { sandboxId: this.id });
|
|
765
|
+
const expiresIn = wholeSeconds('expiresIn', options.expiresIn);
|
|
766
|
+
if (expiresIn !== undefined && !isPublic) {
|
|
767
|
+
throw new PrizedError('invalid_argument', 'expiresIn applies only when making a port public', { sandboxId: this.id });
|
|
768
|
+
}
|
|
769
|
+
const result = await request(this.config, {
|
|
770
|
+
method: 'PUT',
|
|
771
|
+
url: apiUrl(this.config, `${sandboxPathFor(this.id)}/ports/${port}`),
|
|
772
|
+
json: isPublic ? { public: true, ...(expiresIn === undefined ? {} : { expiresIn }) } : { public: false },
|
|
773
|
+
// Setting the same state again changes nothing.
|
|
774
|
+
retry: 'safe',
|
|
775
|
+
signal: options.signal,
|
|
776
|
+
deadline: callDeadline(this.config),
|
|
777
|
+
sandboxId: this.id,
|
|
778
|
+
});
|
|
779
|
+
const doc = result.json?.preview;
|
|
780
|
+
if (doc === undefined || typeof doc.url !== 'string' || typeof doc.public !== 'boolean') {
|
|
781
|
+
throw new PrizedError('bad_response', 'the API answered a port without its url or public setting', { sandboxId: this.id });
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
sandboxId: typeof doc.sandboxId === 'string' ? doc.sandboxId : this.id,
|
|
785
|
+
port: typeof doc.port === 'number' ? doc.port : port,
|
|
786
|
+
url: doc.url,
|
|
787
|
+
browserUrl: typeof doc.browserUrl === 'string' ? doc.browserUrl : null,
|
|
788
|
+
public: doc.public,
|
|
789
|
+
publicExpiresAt: parseTime(doc.publicExpiresAt),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/** A port a preview link may name: 1 to 65 535, never 22 or 81 (the sandbox's own). */
|
|
793
|
+
previewPort(port) {
|
|
794
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535 || port === 22 || port === 81) {
|
|
795
|
+
throw new PrizedError('bad_request', `port must be a whole number from 1 to 65535 other than 22 and 81, not ${String(port)}`, { sandboxId: this.id });
|
|
796
|
+
}
|
|
797
|
+
}
|
|
702
798
|
// --- internals ------------------------------------------------------------------
|
|
703
799
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
704
800
|
* held by the API until the sandbox is that (each attempt asks only for
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version; test/version.test.ts holds it equal to package.json. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.3";
|
package/dist/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version; test/version.test.ts holds it equal to package.json. */
|
|
2
|
-
export const VERSION = '0.1.
|
|
2
|
+
export const VERSION = '0.1.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prized/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Prized Sandboxes (beta): create microVM sandboxes, run commands, move files, pause, resume and fork them from TypeScript.",
|
|
5
5
|
"homepage": "https://prized.dev/docs/sandboxes",
|
|
6
6
|
"license": "Apache-2.0",
|