@prized/sdk 0.1.1 → 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 +74 -29
- package/dist/cjs/commands.d.ts +3 -1
- package/dist/cjs/commands.js +45 -7
- package/dist/cjs/files.d.ts +6 -5
- package/dist/cjs/files.js +8 -5
- package/dist/cjs/http.d.ts +49 -6
- package/dist/cjs/http.js +149 -19
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/outcome.d.ts +9 -0
- package/dist/cjs/outcome.js +21 -1
- package/dist/cjs/output.js +1 -0
- package/dist/cjs/sandbox.d.ts +68 -2
- package/dist/cjs/sandbox.js +112 -7
- package/dist/cjs/shell.d.ts +2 -1
- package/dist/cjs/shell.js +2 -1
- package/dist/cjs/toolbox.d.ts +2 -0
- package/dist/cjs/toolbox.js +1 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/commands.d.ts +3 -1
- package/dist/esm/commands.js +46 -8
- package/dist/esm/files.d.ts +6 -5
- package/dist/esm/files.js +9 -6
- package/dist/esm/http.d.ts +49 -6
- package/dist/esm/http.js +148 -18
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/outcome.d.ts +9 -0
- package/dist/esm/outcome.js +20 -1
- package/dist/esm/output.js +2 -1
- package/dist/esm/sandbox.d.ts +68 -2
- package/dist/esm/sandbox.js +113 -8
- package/dist/esm/shell.d.ts +2 -1
- package/dist/esm/shell.js +2 -1
- package/dist/esm/toolbox.d.ts +2 -0
- package/dist/esm/toolbox.js +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
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. */
|
|
@@ -128,8 +164,9 @@ export declare class Sandbox {
|
|
|
128
164
|
/**
|
|
129
165
|
* Delete every sandbox carrying all of `labels` (at least one) and resolve
|
|
130
166
|
* with their ids (the API takes up to 250 a request; this repeats it until
|
|
131
|
-
* none are left).
|
|
132
|
-
*
|
|
167
|
+
* none are left). Each request selects the current matches, so a repeat
|
|
168
|
+
* can delete sandboxes created since the first request. An uncertain
|
|
169
|
+
* answer is not retried; list the matches before deleting again. Each
|
|
133
170
|
* counts toward the workspace's quotas until it is gone;
|
|
134
171
|
* `Sandbox.list({ labels })` coming back empty means they all are.
|
|
135
172
|
*/
|
|
@@ -193,6 +230,35 @@ export declare class Sandbox {
|
|
|
193
230
|
* `redirect: 'manual'`: fetch resends custom headers to whatever host a
|
|
194
231
|
* redirect names, so only ever send them to the sandbox's preview URL. */
|
|
195
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;
|
|
196
262
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
197
263
|
* held by the API until the sandbox is that (each attempt asks only for
|
|
198
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
|
}
|
|
@@ -437,8 +444,9 @@ export class Sandbox {
|
|
|
437
444
|
/**
|
|
438
445
|
* Delete every sandbox carrying all of `labels` (at least one) and resolve
|
|
439
446
|
* with their ids (the API takes up to 250 a request; this repeats it until
|
|
440
|
-
* none are left).
|
|
441
|
-
*
|
|
447
|
+
* none are left). Each request selects the current matches, so a repeat
|
|
448
|
+
* can delete sandboxes created since the first request. An uncertain
|
|
449
|
+
* answer is not retried; list the matches before deleting again. Each
|
|
442
450
|
* counts toward the workspace's quotas until it is gone;
|
|
443
451
|
* `Sandbox.list({ labels })` coming back empty means they all are.
|
|
444
452
|
*/
|
|
@@ -452,7 +460,15 @@ export class Sandbox {
|
|
|
452
460
|
const deleted = new Set();
|
|
453
461
|
const stuck = (calls) => new PrizedError('delete_incomplete', `Deleted ${deleted.size} sandboxes in ${calls} requests and some are left; call again to delete the rest.`);
|
|
454
462
|
for (let call = 1; call <= MAX_BULK_DELETE_CALLS; call += 1) {
|
|
455
|
-
|
|
463
|
+
// A label is a live selector, not a stable resource or idempotency key.
|
|
464
|
+
const result = await request(config, {
|
|
465
|
+
method: 'DELETE', url: apiUrl(config, `/sandboxes${query}`), retry: 'exec', deadline,
|
|
466
|
+
effect: {
|
|
467
|
+
maybe: 'some matching sandboxes may already be deleting',
|
|
468
|
+
did: 'The matching sandboxes were marked for deletion',
|
|
469
|
+
check: 'It was not sent again: list the matching sandboxes before deleting again, because the matches may have changed.',
|
|
470
|
+
},
|
|
471
|
+
});
|
|
456
472
|
const answer = result.json;
|
|
457
473
|
const ids = answer?.ids;
|
|
458
474
|
if (!Array.isArray(ids) || !ids.every((id) => typeof id === 'string')) {
|
|
@@ -549,9 +565,13 @@ export class Sandbox {
|
|
|
549
565
|
body.maxRuntimeMin = maxRuntimeMin;
|
|
550
566
|
const idempotencyKey = options.idempotencyKey ?? randomKey();
|
|
551
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;
|
|
552
572
|
const send = (deadline, attempts) => request(this.config, {
|
|
553
573
|
method: 'POST',
|
|
554
|
-
url: apiUrl(this.config, `${
|
|
574
|
+
url: (leftMs) => apiUrl(this.config, held && attempts !== undefined ? `${path}${holdQuery(this.config, 'running', leftMs)}` : path),
|
|
555
575
|
json: wire,
|
|
556
576
|
headers: { 'idempotency-key': idempotencyKey },
|
|
557
577
|
retry: 'safe',
|
|
@@ -674,9 +694,7 @@ export class Sandbox {
|
|
|
674
694
|
* `preview_unavailable` when the deployment has sandbox ingress off.
|
|
675
695
|
*/
|
|
676
696
|
previewUrl(port) {
|
|
677
|
-
|
|
678
|
-
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 });
|
|
679
|
-
}
|
|
697
|
+
this.previewPort(port);
|
|
680
698
|
const template = this.current.raw.previewUrlTemplate;
|
|
681
699
|
if (typeof template !== 'string' || !template.includes('{port}')) {
|
|
682
700
|
throw new PrizedError('preview_unavailable', 'sandbox ports at a URL are not turned on for this deployment', { sandboxId: this.id });
|
|
@@ -690,6 +708,93 @@ export class Sandbox {
|
|
|
690
708
|
previewHeaders() {
|
|
691
709
|
return { [PREVIEW_KEY_HEADER]: this.config.apiKey };
|
|
692
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
|
+
}
|
|
693
798
|
// --- internals ------------------------------------------------------------------
|
|
694
799
|
/** POST the verb, the call and its retries within `deadline`; with `hold`,
|
|
695
800
|
* held by the API until the sandbox is that (each attempt asks only for
|
package/dist/esm/shell.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare const RUN_ID_RE: RegExp;
|
|
|
11
11
|
/**
|
|
12
12
|
* Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
|
|
13
13
|
* stays: what keeps a resend of its start from running the command again.
|
|
14
|
-
* The SDK stops resending a start
|
|
14
|
+
* The SDK stops resending a start once pollOutageMs (2 minutes by default)
|
|
15
|
+
* has run out after its first copy's retries, so an hour is ample; each
|
|
15
16
|
* cleanup then removes the ones older than that, so they never pile up.
|
|
16
17
|
*/
|
|
17
18
|
export declare const CLEANED_KEEP_MIN = 60;
|
package/dist/esm/shell.js
CHANGED
|
@@ -50,7 +50,8 @@ export const RUN_ID_RE = /^run_[0-9a-f]{16}$/;
|
|
|
50
50
|
/**
|
|
51
51
|
* Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
|
|
52
52
|
* stays: what keeps a resend of its start from running the command again.
|
|
53
|
-
* The SDK stops resending a start
|
|
53
|
+
* The SDK stops resending a start once pollOutageMs (2 minutes by default)
|
|
54
|
+
* has run out after its first copy's retries, so an hour is ample; each
|
|
54
55
|
* cleanup then removes the ones older than that, so they never pile up.
|
|
55
56
|
*/
|
|
56
57
|
export const CLEANED_KEEP_MIN = 60;
|
package/dist/esm/toolbox.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function rawExec(toolbox: Toolbox, req: RawExecRequest, options:
|
|
|
30
30
|
deadline?: number | undefined;
|
|
31
31
|
/** What the command does, for an error that says whether it happened (`exec`: a command unless given). */
|
|
32
32
|
effect?: Effect | undefined;
|
|
33
|
+
/** An earlier copy of a keyed start may have reached the sandbox. */
|
|
34
|
+
acted?: boolean | undefined;
|
|
33
35
|
}): Promise<ExecResult>;
|
|
34
36
|
/** A helper command the SDK sends itself; a non-zero exit is the caller's to judge. */
|
|
35
37
|
export declare function helperExec(toolbox: Toolbox, command: string, options?: {
|
package/dist/esm/toolbox.js
CHANGED
|
@@ -46,6 +46,7 @@ export async function rawExec(toolbox, req, options) {
|
|
|
46
46
|
deadline: options.deadline,
|
|
47
47
|
sandboxId: toolbox.sandboxId,
|
|
48
48
|
...(effect === undefined ? {} : { effect }),
|
|
49
|
+
...(options.acted ? { acted: true } : {}),
|
|
49
50
|
});
|
|
50
51
|
const doc = result.json;
|
|
51
52
|
if (doc === null || typeof doc !== 'object' || typeof doc.exitCode !== 'number') {
|
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",
|