@vercel/sandbox 0.0.17 → 0.0.19
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/api-client/api-client.d.ts +54 -3
- package/dist/api-client/api-client.js +106 -14
- package/dist/api-client/validators.d.ts +147 -0
- package/dist/api-client/validators.js +22 -1
- package/dist/command.d.ts +8 -2
- package/dist/command.js +4 -1
- package/dist/sandbox.d.ts +31 -1
- package/dist/sandbox.js +18 -2
- package/dist/utils/get-credentials.d.ts +22 -1
- package/dist/utils/get-credentials.js +11 -4
- package/dist/utils/jwt-expiry.d.ts +42 -0
- package/dist/utils/jwt-expiry.js +105 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/types.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/api-client/api-client.ts +168 -34
- package/src/api-client/validators.ts +23 -0
- package/src/command.ts +4 -1
- package/src/sandbox.ts +24 -3
- package/src/utils/get-credentials.ts +15 -10
- package/src/utils/jwt-expiry.test.ts +125 -0
- package/src/utils/jwt-expiry.ts +105 -0
- package/src/utils/types.test.js +7 -0
- package/src/utils/types.ts +23 -0
- package/src/version.ts +1 -1
- package/turbo.json +9 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.schema = void 0;
|
|
3
4
|
exports.getCredentials = getCredentials;
|
|
4
5
|
const oidc_1 = require("@vercel/oidc");
|
|
5
6
|
const decode_base64_url_1 = require("./decode-base64-url");
|
|
@@ -14,12 +15,12 @@ const zod_1 = require("zod");
|
|
|
14
15
|
* If both methods are used, the object properties take precedence over the
|
|
15
16
|
* environment variable. If neither method is used, an error is thrown.
|
|
16
17
|
*/
|
|
17
|
-
function getCredentials(params) {
|
|
18
|
+
async function getCredentials(params) {
|
|
18
19
|
const credentials = getCredentialsFromParams(params ?? {});
|
|
19
20
|
if (credentials) {
|
|
20
21
|
return credentials;
|
|
21
22
|
}
|
|
22
|
-
const oidcToken = (0, oidc_1.
|
|
23
|
+
const oidcToken = await (0, oidc_1.getVercelOidcToken)();
|
|
23
24
|
if (oidcToken) {
|
|
24
25
|
return getCredentialsFromOIDCToken(oidcToken);
|
|
25
26
|
}
|
|
@@ -32,6 +33,10 @@ function getCredentials(params) {
|
|
|
32
33
|
* or none of them, otherwise an error is thrown.
|
|
33
34
|
*/
|
|
34
35
|
function getCredentialsFromParams(params) {
|
|
36
|
+
// Type guard: params must be an object
|
|
37
|
+
if (!params || typeof params !== "object") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
35
40
|
const missing = [
|
|
36
41
|
"token" in params && typeof params.token === "string" ? null : "token",
|
|
37
42
|
"teamId" in params && typeof params.teamId === "string" ? null : "teamId",
|
|
@@ -57,7 +62,9 @@ function getCredentialsFromParams(params) {
|
|
|
57
62
|
* Schema to validate the payload of the Vercel OIDC token where we expect
|
|
58
63
|
* to find the `teamId` and `projectId`.
|
|
59
64
|
*/
|
|
60
|
-
|
|
65
|
+
exports.schema = zod_1.z.object({
|
|
66
|
+
exp: zod_1.z.number().optional().describe("Expiry timestamp (seconds since epoch)"),
|
|
67
|
+
iat: zod_1.z.number().optional().describe("Issued at timestamp"),
|
|
61
68
|
owner_id: zod_1.z.string(),
|
|
62
69
|
project_id: zod_1.z.string(),
|
|
63
70
|
});
|
|
@@ -71,7 +78,7 @@ const schema = zod_1.z.object({
|
|
|
71
78
|
*/
|
|
72
79
|
function getCredentialsFromOIDCToken(token) {
|
|
73
80
|
try {
|
|
74
|
-
const payload = schema.parse((0, decode_base64_url_1.decodeBase64Url)(token.split(".")[1]));
|
|
81
|
+
const payload = exports.schema.parse((0, decode_base64_url_1.decodeBase64Url)(token.split(".")[1]));
|
|
75
82
|
return {
|
|
76
83
|
token,
|
|
77
84
|
projectId: payload.project_id,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { schema } from "./get-credentials";
|
|
3
|
+
export declare class OidcRefreshError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Expiry implementation for JWT tokens (OIDC tokens).
|
|
8
|
+
* Parses the JWT once and provides fast expiry validation.
|
|
9
|
+
*/
|
|
10
|
+
export declare class JwtExpiry {
|
|
11
|
+
readonly token: string;
|
|
12
|
+
private expiryTime;
|
|
13
|
+
readonly payload?: Readonly<z.infer<typeof schema>>;
|
|
14
|
+
static fromToken(token: string): JwtExpiry | null;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new JWT expiry checker.
|
|
17
|
+
*
|
|
18
|
+
* @param token - The JWT token to parse
|
|
19
|
+
*/
|
|
20
|
+
constructor(token: string);
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the JWT token is valid (not expired).
|
|
23
|
+
* @returns true if token is valid, false if expired or expiring soon
|
|
24
|
+
*/
|
|
25
|
+
isValid(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the expiry date of the JWT token.
|
|
28
|
+
*
|
|
29
|
+
* @returns Date object representing when the token expires, or null if no expiry
|
|
30
|
+
*/
|
|
31
|
+
getExpiryDate(): Date | null;
|
|
32
|
+
/**
|
|
33
|
+
* Refreshes the JWT token by fetching a new OIDC token.
|
|
34
|
+
*
|
|
35
|
+
* @returns Promise resolving to a new JwtExpiry instance with fresh token
|
|
36
|
+
*/
|
|
37
|
+
refresh(): Promise<JwtExpiry>;
|
|
38
|
+
/**
|
|
39
|
+
* Refreshes the JWT token if it's expired or expiring soon.
|
|
40
|
+
*/
|
|
41
|
+
tryRefresh(): Promise<JwtExpiry | null>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JwtExpiry = exports.OidcRefreshError = void 0;
|
|
7
|
+
const decode_base64_url_1 = require("./decode-base64-url");
|
|
8
|
+
const get_credentials_1 = require("./get-credentials");
|
|
9
|
+
const oidc_1 = require("@vercel/oidc");
|
|
10
|
+
const ms_1 = __importDefault(require("ms"));
|
|
11
|
+
/** Time buffer before token expiry to consider it invalid (in milliseconds) */
|
|
12
|
+
const BUFFER_MS = (0, ms_1.default)("5m");
|
|
13
|
+
class OidcRefreshError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.name = "OidcRefreshError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.OidcRefreshError = OidcRefreshError;
|
|
20
|
+
/**
|
|
21
|
+
* Expiry implementation for JWT tokens (OIDC tokens).
|
|
22
|
+
* Parses the JWT once and provides fast expiry validation.
|
|
23
|
+
*/
|
|
24
|
+
class JwtExpiry {
|
|
25
|
+
static fromToken(token) {
|
|
26
|
+
if (!isJwtFormat(token)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return new JwtExpiry(token);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new JWT expiry checker.
|
|
35
|
+
*
|
|
36
|
+
* @param token - The JWT token to parse
|
|
37
|
+
*/
|
|
38
|
+
constructor(token) {
|
|
39
|
+
this.token = token;
|
|
40
|
+
try {
|
|
41
|
+
const tokenContents = token.split(".")[1];
|
|
42
|
+
this.payload = get_credentials_1.schema.parse((0, decode_base64_url_1.decodeBase64Url)(tokenContents));
|
|
43
|
+
this.expiryTime = this.payload.exp || null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Malformed token - treat as expired to trigger refresh
|
|
47
|
+
this.expiryTime = 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if the JWT token is valid (not expired).
|
|
52
|
+
* @returns true if token is valid, false if expired or expiring soon
|
|
53
|
+
*/
|
|
54
|
+
isValid() {
|
|
55
|
+
if (this.expiryTime === null) {
|
|
56
|
+
return false; // No expiry means malformed JWT
|
|
57
|
+
}
|
|
58
|
+
const now = Math.floor(Date.now() / 1000);
|
|
59
|
+
const buffer = BUFFER_MS / 1000;
|
|
60
|
+
return now + buffer < this.expiryTime;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Gets the expiry date of the JWT token.
|
|
64
|
+
*
|
|
65
|
+
* @returns Date object representing when the token expires, or null if no expiry
|
|
66
|
+
*/
|
|
67
|
+
getExpiryDate() {
|
|
68
|
+
return this.expiryTime ? new Date(this.expiryTime * 1000) : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Refreshes the JWT token by fetching a new OIDC token.
|
|
72
|
+
*
|
|
73
|
+
* @returns Promise resolving to a new JwtExpiry instance with fresh token
|
|
74
|
+
*/
|
|
75
|
+
async refresh() {
|
|
76
|
+
try {
|
|
77
|
+
const freshToken = await (0, oidc_1.getVercelOidcToken)();
|
|
78
|
+
return new JwtExpiry(freshToken);
|
|
79
|
+
}
|
|
80
|
+
catch (cause) {
|
|
81
|
+
throw new OidcRefreshError("Failed to refresh OIDC token", {
|
|
82
|
+
cause,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refreshes the JWT token if it's expired or expiring soon.
|
|
88
|
+
*/
|
|
89
|
+
async tryRefresh() {
|
|
90
|
+
if (this.isValid()) {
|
|
91
|
+
return null; // Still valid, no need to refresh
|
|
92
|
+
}
|
|
93
|
+
return this.refresh();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.JwtExpiry = JwtExpiry;
|
|
97
|
+
/**
|
|
98
|
+
* Checks if a token follows JWT format (has 3 parts separated by dots).
|
|
99
|
+
*
|
|
100
|
+
* @param token - The token to check
|
|
101
|
+
* @returns true if token appears to be a JWT, false otherwise
|
|
102
|
+
*/
|
|
103
|
+
function isJwtFormat(token) {
|
|
104
|
+
return token.split(".").length === 3;
|
|
105
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility type that extends a type to accept private parameters.
|
|
3
|
+
*
|
|
4
|
+
* The private parameters can then be extracted out of the object using
|
|
5
|
+
* `getPrivateParams`.
|
|
6
|
+
*/
|
|
7
|
+
export type WithPrivate<T> = T & {
|
|
8
|
+
[K in `__${string}`]?: unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Extract private parameters out of an object.
|
|
12
|
+
*/
|
|
13
|
+
export declare const getPrivateParams: (params?: object) => { [K in keyof typeof params as K extends `__${string}` ? K : never]: (typeof params)[K]; };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPrivateParams = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Extract private parameters out of an object.
|
|
6
|
+
*/
|
|
7
|
+
const getPrivateParams = (params) => {
|
|
8
|
+
const privateEntries = Object.entries(params ?? {}).filter(([k]) => k.startsWith("__"));
|
|
9
|
+
return Object.fromEntries(privateEntries);
|
|
10
|
+
};
|
|
11
|
+
exports.getPrivateParams = getPrivateParams;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.19";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/sandbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"author": "",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@vercel/oidc": "^2.0.
|
|
12
|
+
"@vercel/oidc": "^2.0.2",
|
|
13
13
|
"async-retry": "1.3.3",
|
|
14
14
|
"jsonlines": "0.1.1",
|
|
15
15
|
"ms": "2.1.3",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"test": "vitest run",
|
|
34
34
|
"typedoc": "typedoc",
|
|
35
35
|
"typecheck": "tsc --noEmit",
|
|
36
|
-
"
|
|
36
|
+
"version:bump": "ts-node scripts/inject-version.ts"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
CommandFinishedResponse,
|
|
12
12
|
EmptyResponse,
|
|
13
13
|
LogLine,
|
|
14
|
+
SandboxesResponse,
|
|
14
15
|
} from "./validators";
|
|
15
16
|
import { APIError } from "./api-error";
|
|
16
17
|
import { FileWriter } from "./file-writer";
|
|
@@ -21,9 +22,12 @@ import jsonlines from "jsonlines";
|
|
|
21
22
|
import os from "os";
|
|
22
23
|
import { Readable } from "stream";
|
|
23
24
|
import { normalizePath } from "../utils/normalizePath";
|
|
25
|
+
import { JwtExpiry } from "../utils/jwt-expiry";
|
|
26
|
+
import { getPrivateParams, WithPrivate } from "../utils/types";
|
|
24
27
|
|
|
25
28
|
export class APIClient extends BaseClient {
|
|
26
29
|
private teamId: string;
|
|
30
|
+
private tokenExpiry: JwtExpiry | null;
|
|
27
31
|
|
|
28
32
|
constructor(params: { host?: string; teamId: string; token: string }) {
|
|
29
33
|
super({
|
|
@@ -33,9 +37,29 @@ export class APIClient extends BaseClient {
|
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
this.teamId = params.teamId;
|
|
40
|
+
this.tokenExpiry = JwtExpiry.fromToken(params.token);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async ensureValidToken(): Promise<void> {
|
|
44
|
+
if (!this.tokenExpiry) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const newExpiry = await this.tokenExpiry.tryRefresh();
|
|
49
|
+
if (!newExpiry) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.tokenExpiry = newExpiry;
|
|
54
|
+
this.token = this.tokenExpiry.token;
|
|
55
|
+
if (this.tokenExpiry.payload) {
|
|
56
|
+
this.teamId = this.tokenExpiry.payload?.owner_id;
|
|
57
|
+
}
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
protected async request(path: string, params?: RequestParams) {
|
|
61
|
+
await this.ensureValidToken();
|
|
62
|
+
|
|
39
63
|
return super.request(path, {
|
|
40
64
|
...params,
|
|
41
65
|
query: { teamId: this.teamId, ...params?.query },
|
|
@@ -54,23 +78,26 @@ export class APIClient extends BaseClient {
|
|
|
54
78
|
);
|
|
55
79
|
}
|
|
56
80
|
|
|
57
|
-
async createSandbox(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
async createSandbox(
|
|
82
|
+
params: WithPrivate<{
|
|
83
|
+
ports?: number[];
|
|
84
|
+
projectId: string;
|
|
85
|
+
source?:
|
|
86
|
+
| {
|
|
87
|
+
type: "git";
|
|
88
|
+
url: string;
|
|
89
|
+
depth?: number;
|
|
90
|
+
revision?: string;
|
|
91
|
+
username?: string;
|
|
92
|
+
password?: string;
|
|
93
|
+
}
|
|
94
|
+
| { type: "tarball"; url: string };
|
|
95
|
+
timeout?: number;
|
|
96
|
+
resources?: { vcpus: number };
|
|
97
|
+
runtime?: "node22" | "python3.13" | (string & {});
|
|
98
|
+
}>,
|
|
99
|
+
) {
|
|
100
|
+
const privateParams = getPrivateParams(params);
|
|
74
101
|
return parseOrThrow(
|
|
75
102
|
SandboxAndRoutesResponse,
|
|
76
103
|
await this.request("/v1/sandboxes", {
|
|
@@ -82,6 +109,7 @@ export class APIClient extends BaseClient {
|
|
|
82
109
|
timeout: params.timeout,
|
|
83
110
|
resources: params.resources,
|
|
84
111
|
runtime: params.runtime,
|
|
112
|
+
...privateParams,
|
|
85
113
|
}),
|
|
86
114
|
}),
|
|
87
115
|
);
|
|
@@ -168,6 +196,48 @@ export class APIClient extends BaseClient {
|
|
|
168
196
|
};
|
|
169
197
|
}
|
|
170
198
|
|
|
199
|
+
async listSandboxes(params: {
|
|
200
|
+
/**
|
|
201
|
+
* The ID or name of the project to which the sandboxes belong.
|
|
202
|
+
* @example "my-project"
|
|
203
|
+
*/
|
|
204
|
+
projectId: string;
|
|
205
|
+
/**
|
|
206
|
+
* Maximum number of sandboxes to list from a request.
|
|
207
|
+
* @example 10
|
|
208
|
+
*/
|
|
209
|
+
limit?: number;
|
|
210
|
+
/**
|
|
211
|
+
* Get sandboxes created after this JavaScript timestamp.
|
|
212
|
+
* @example 1540095775941
|
|
213
|
+
*/
|
|
214
|
+
since?: number | Date;
|
|
215
|
+
/**
|
|
216
|
+
* Get sandboxes created before this JavaScript timestamp.
|
|
217
|
+
* @example 1540095775951
|
|
218
|
+
*/
|
|
219
|
+
until?: number | Date;
|
|
220
|
+
}) {
|
|
221
|
+
return parseOrThrow(
|
|
222
|
+
SandboxesResponse,
|
|
223
|
+
await this.request(`/v1/sandboxes`, {
|
|
224
|
+
query: {
|
|
225
|
+
project: params.projectId,
|
|
226
|
+
limit: params.limit,
|
|
227
|
+
since:
|
|
228
|
+
typeof params.since === "number"
|
|
229
|
+
? params.since
|
|
230
|
+
: params.since?.getTime(),
|
|
231
|
+
until:
|
|
232
|
+
typeof params.until === "number"
|
|
233
|
+
? params.until
|
|
234
|
+
: params.until?.getTime(),
|
|
235
|
+
},
|
|
236
|
+
method: "GET",
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
171
241
|
async writeFiles(params: {
|
|
172
242
|
sandboxId: string;
|
|
173
243
|
cwd: string;
|
|
@@ -235,29 +305,53 @@ export class APIClient extends BaseClient {
|
|
|
235
305
|
);
|
|
236
306
|
}
|
|
237
307
|
|
|
238
|
-
|
|
308
|
+
getLogs(params: {
|
|
239
309
|
sandboxId: string;
|
|
240
310
|
cmdId: string;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
311
|
+
signal?: AbortSignal;
|
|
312
|
+
}): AsyncGenerator<z.infer<typeof LogLine>, void, void> &
|
|
313
|
+
Disposable & { close(): void } {
|
|
314
|
+
const self = this;
|
|
315
|
+
const disposer = new AbortController();
|
|
316
|
+
|
|
317
|
+
const signal = !params.signal
|
|
318
|
+
? disposer.signal
|
|
319
|
+
: mergeSignals(params.signal, disposer.signal);
|
|
320
|
+
|
|
321
|
+
const generator = (async function* () {
|
|
322
|
+
const url = `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}/logs`;
|
|
323
|
+
const response = await self.request(url, {
|
|
324
|
+
method: "GET",
|
|
325
|
+
signal,
|
|
247
326
|
});
|
|
248
|
-
|
|
327
|
+
if (response.headers.get("content-type") !== "application/x-ndjson") {
|
|
328
|
+
throw new APIError(response, {
|
|
329
|
+
message: "Expected a stream of logs",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
249
332
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
333
|
+
if (response.body === null) {
|
|
334
|
+
throw new APIError(response, {
|
|
335
|
+
message: "No response body",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const jsonlinesStream = jsonlines.parse();
|
|
340
|
+
pipe(response.body, jsonlinesStream).catch((err) => {
|
|
341
|
+
console.error("Error piping logs:", err);
|
|
253
342
|
});
|
|
254
|
-
}
|
|
255
343
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
344
|
+
for await (const chunk of jsonlinesStream) {
|
|
345
|
+
yield LogLine.parse(chunk);
|
|
346
|
+
}
|
|
347
|
+
})();
|
|
348
|
+
|
|
349
|
+
return Object.assign(generator, {
|
|
350
|
+
[Symbol.dispose]() {
|
|
351
|
+
disposer.abort("Disposed");
|
|
352
|
+
},
|
|
353
|
+
close: () => disposer.abort("Disposed"),
|
|
354
|
+
});
|
|
261
355
|
}
|
|
262
356
|
|
|
263
357
|
async stopSandbox(params: {
|
|
@@ -270,3 +364,43 @@ export class APIClient extends BaseClient {
|
|
|
270
364
|
);
|
|
271
365
|
}
|
|
272
366
|
}
|
|
367
|
+
|
|
368
|
+
async function pipe(
|
|
369
|
+
readable: ReadableStream<Uint8Array>,
|
|
370
|
+
output: NodeJS.WritableStream,
|
|
371
|
+
) {
|
|
372
|
+
const reader = readable.getReader();
|
|
373
|
+
try {
|
|
374
|
+
while (true) {
|
|
375
|
+
const read = await reader.read();
|
|
376
|
+
if (read.value) {
|
|
377
|
+
output.write(Buffer.from(read.value));
|
|
378
|
+
}
|
|
379
|
+
if (read.done) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
output.emit("error", err);
|
|
385
|
+
} finally {
|
|
386
|
+
output.end();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function mergeSignals(...signals: [AbortSignal, ...AbortSignal[]]) {
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
const onAbort = () => {
|
|
393
|
+
controller.abort();
|
|
394
|
+
for (const signal of signals) {
|
|
395
|
+
signal.removeEventListener("abort", onAbort);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
for (const signal of signals) {
|
|
399
|
+
if (signal.aborted) {
|
|
400
|
+
controller.abort();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
signal.addEventListener("abort", onAbort);
|
|
404
|
+
}
|
|
405
|
+
return controller.signal;
|
|
406
|
+
}
|
|
@@ -28,6 +28,24 @@ export const SandboxRoute = z.object({
|
|
|
28
28
|
port: z.number(),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
export const Pagination = z.object({
|
|
32
|
+
/**
|
|
33
|
+
* Amount of items in the current page.
|
|
34
|
+
* @example 20
|
|
35
|
+
*/
|
|
36
|
+
count: z.number(),
|
|
37
|
+
/**
|
|
38
|
+
* Timestamp that must be used to request the next page.
|
|
39
|
+
* @example 1540095775951
|
|
40
|
+
*/
|
|
41
|
+
next: z.number().nullable(),
|
|
42
|
+
/**
|
|
43
|
+
* Timestamp that must be used to request the previous page.
|
|
44
|
+
* @example 1540095775951
|
|
45
|
+
*/
|
|
46
|
+
prev: z.number().nullable(),
|
|
47
|
+
});
|
|
48
|
+
|
|
31
49
|
export type CommandData = z.infer<typeof Command>;
|
|
32
50
|
|
|
33
51
|
export const Command = z.object({
|
|
@@ -66,3 +84,8 @@ export const LogLine = z.object({
|
|
|
66
84
|
stream: z.enum(["stdout", "stderr"]),
|
|
67
85
|
data: z.string(),
|
|
68
86
|
});
|
|
87
|
+
|
|
88
|
+
export const SandboxesResponse = z.object({
|
|
89
|
+
sandboxes: z.array(Sandbox),
|
|
90
|
+
pagination: Pagination,
|
|
91
|
+
});
|
package/src/command.ts
CHANGED
|
@@ -82,15 +82,18 @@ export class Command {
|
|
|
82
82
|
* }
|
|
83
83
|
* ```
|
|
84
84
|
*
|
|
85
|
+
* @param opts - Optional parameters.
|
|
86
|
+
* @param opts.signal - An AbortSignal to cancel log streaming.
|
|
85
87
|
* @returns An async iterable of log entries from the command output.
|
|
86
88
|
*
|
|
87
89
|
* @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
|
|
88
90
|
* to access output as a string.
|
|
89
91
|
*/
|
|
90
|
-
logs() {
|
|
92
|
+
logs(opts?: { signal?: AbortSignal }) {
|
|
91
93
|
return this.client.getLogs({
|
|
92
94
|
sandboxId: this.sandboxId,
|
|
93
95
|
cmdId: this.cmd.id,
|
|
96
|
+
signal: opts?.signal,
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
99
|
|
package/src/sandbox.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Writable } from "stream";
|
|
|
3
3
|
import { APIClient } from "./api-client";
|
|
4
4
|
import { Command, type CommandFinished } from "./command";
|
|
5
5
|
import { type Credentials, getCredentials } from "./utils/get-credentials";
|
|
6
|
+
import { getPrivateParams, WithPrivate } from "./utils/types";
|
|
6
7
|
|
|
7
8
|
/** @inline */
|
|
8
9
|
export interface CreateSandboxParams {
|
|
@@ -133,6 +134,22 @@ export class Sandbox {
|
|
|
133
134
|
*/
|
|
134
135
|
private readonly sandbox: SandboxMetaData;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Allow to get a list of sandboxes for a team narrowed to the given params.
|
|
139
|
+
* It returns both the sandboxes and the pagination metadata to allow getting
|
|
140
|
+
* the next page of results.
|
|
141
|
+
*/
|
|
142
|
+
static async list(
|
|
143
|
+
params: Parameters<APIClient["listSandboxes"]>[0] & Partial<Credentials>,
|
|
144
|
+
) {
|
|
145
|
+
const credentials = await getCredentials(params);
|
|
146
|
+
const client = new APIClient({
|
|
147
|
+
teamId: credentials.teamId,
|
|
148
|
+
token: credentials.token,
|
|
149
|
+
});
|
|
150
|
+
return client.listSandboxes(params);
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
/**
|
|
137
154
|
* Create a new sandbox.
|
|
138
155
|
*
|
|
@@ -140,14 +157,17 @@ export class Sandbox {
|
|
|
140
157
|
* @returns A promise resolving to the created {@link Sandbox}.
|
|
141
158
|
*/
|
|
142
159
|
static async create(
|
|
143
|
-
params?:
|
|
160
|
+
params?: WithPrivate<
|
|
161
|
+
CreateSandboxParams | (CreateSandboxParams & Credentials)
|
|
162
|
+
>,
|
|
144
163
|
): Promise<Sandbox> {
|
|
145
|
-
const credentials = getCredentials(params);
|
|
164
|
+
const credentials = await getCredentials(params);
|
|
146
165
|
const client = new APIClient({
|
|
147
166
|
teamId: credentials.teamId,
|
|
148
167
|
token: credentials.token,
|
|
149
168
|
});
|
|
150
169
|
|
|
170
|
+
const privateParams = getPrivateParams(params);
|
|
151
171
|
const sandbox = await client.createSandbox({
|
|
152
172
|
source: params?.source,
|
|
153
173
|
projectId: credentials.projectId,
|
|
@@ -155,6 +175,7 @@ export class Sandbox {
|
|
|
155
175
|
timeout: params?.timeout,
|
|
156
176
|
resources: params?.resources,
|
|
157
177
|
runtime: params?.runtime,
|
|
178
|
+
...privateParams,
|
|
158
179
|
});
|
|
159
180
|
|
|
160
181
|
return new Sandbox({
|
|
@@ -173,7 +194,7 @@ export class Sandbox {
|
|
|
173
194
|
static async get(
|
|
174
195
|
params: GetSandboxParams | (GetSandboxParams & Credentials),
|
|
175
196
|
): Promise<Sandbox> {
|
|
176
|
-
const credentials = getCredentials(params);
|
|
197
|
+
const credentials = await getCredentials(params);
|
|
177
198
|
const client = new APIClient({
|
|
178
199
|
teamId: credentials.teamId,
|
|
179
200
|
token: credentials.token,
|