@vercel/sandbox 1.1.9 → 1.2.1
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/dist/api-client/api-client.d.ts +18 -13
- package/dist/api-client/api-client.js +46 -0
- package/dist/api-client/api-client.js.map +1 -1
- package/dist/api-client/validators.d.ts +1 -0
- package/dist/api-client/validators.js.map +1 -1
- package/dist/sandbox.d.ts +39 -5
- package/dist/sandbox.js +111 -23
- package/dist/sandbox.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -24
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -267
- package/__mocks__/picocolors.ts +0 -13
- package/scripts/inject-version.ts +0 -11
- package/src/api-client/api-client.test.ts +0 -176
- package/src/api-client/api-client.ts +0 -520
- package/src/api-client/api-error.ts +0 -46
- package/src/api-client/base-client.ts +0 -171
- package/src/api-client/file-writer.ts +0 -90
- package/src/api-client/index.ts +0 -2
- package/src/api-client/validators.ts +0 -144
- package/src/api-client/with-retry.ts +0 -131
- package/src/auth/api.ts +0 -31
- package/src/auth/error.ts +0 -8
- package/src/auth/file.ts +0 -69
- package/src/auth/index.ts +0 -9
- package/src/auth/infer-scope.test.ts +0 -178
- package/src/auth/linked-project.test.ts +0 -86
- package/src/auth/linked-project.ts +0 -40
- package/src/auth/oauth.ts +0 -333
- package/src/auth/poll-for-token.ts +0 -89
- package/src/auth/project.ts +0 -92
- package/src/auth/zod.ts +0 -16
- package/src/command.test.ts +0 -103
- package/src/command.ts +0 -287
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -4
- package/src/sandbox.test.ts +0 -93
- package/src/sandbox.ts +0 -572
- package/src/snapshot.ts +0 -110
- package/src/utils/array.ts +0 -15
- package/src/utils/consume-readable.ts +0 -12
- package/src/utils/decode-base64-url.ts +0 -14
- package/src/utils/dev-credentials.test.ts +0 -217
- package/src/utils/dev-credentials.ts +0 -196
- package/src/utils/get-credentials.test.ts +0 -20
- package/src/utils/get-credentials.ts +0 -183
- package/src/utils/jwt-expiry.test.ts +0 -125
- package/src/utils/jwt-expiry.ts +0 -105
- package/src/utils/log.ts +0 -20
- package/src/utils/normalizePath.test.ts +0 -114
- package/src/utils/normalizePath.ts +0 -33
- package/src/utils/resolveSignal.ts +0 -24
- package/src/utils/types.test.js +0 -7
- package/src/utils/types.ts +0 -23
- package/src/version.ts +0 -2
- package/test-utils/mock-response.ts +0 -12
- package/tsconfig.json +0 -16
- package/turbo.json +0 -9
- package/typedoc.json +0 -13
- package/vercel.json +0 -9
- package/vitest.config.ts +0 -9
- package/vitest.setup.ts +0 -4
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import type { Options as RetryOptions } from "async-retry";
|
|
2
|
-
import { APIError } from "./api-error";
|
|
3
|
-
import { ZodType } from "zod";
|
|
4
|
-
import { array } from "../utils/array";
|
|
5
|
-
import { withRetry, type RequestOptions } from "./with-retry";
|
|
6
|
-
import { Agent } from "undici";
|
|
7
|
-
|
|
8
|
-
export interface RequestParams extends RequestInit {
|
|
9
|
-
headers?: Record<string, string>;
|
|
10
|
-
method?: string;
|
|
11
|
-
onRetry?(error: any, options: RequestOptions): void;
|
|
12
|
-
query?: Record<string, number | string | null | undefined | string[]>;
|
|
13
|
-
retry?: Partial<RetryOptions>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A base API client that provides a convenience wrapper for fetching where
|
|
18
|
-
* we can pass query parameters as an object, support retries, debugging
|
|
19
|
-
* and automatic authorization.
|
|
20
|
-
*/
|
|
21
|
-
export class BaseClient {
|
|
22
|
-
protected token?: string;
|
|
23
|
-
private fetch: ReturnType<typeof withRetry<RequestInit>>;
|
|
24
|
-
private debug: boolean;
|
|
25
|
-
private baseUrl: string;
|
|
26
|
-
private agent: Agent;
|
|
27
|
-
|
|
28
|
-
constructor(params: {
|
|
29
|
-
debug?: boolean;
|
|
30
|
-
baseUrl: string;
|
|
31
|
-
token?: string;
|
|
32
|
-
fetch?: typeof globalThis.fetch;
|
|
33
|
-
}) {
|
|
34
|
-
this.fetch = withRetry(params.fetch ?? globalThis.fetch);
|
|
35
|
-
this.baseUrl = params.baseUrl;
|
|
36
|
-
this.debug = params.debug ?? process.env.DEBUG_FETCH === "true";
|
|
37
|
-
this.token = params.token;
|
|
38
|
-
this.agent = new Agent({
|
|
39
|
-
bodyTimeout: 0, // disable body timeout to allow long logs streaming
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
protected async request(path: string, opts?: RequestParams) {
|
|
44
|
-
const url = new URL(`${this.baseUrl}${path}`);
|
|
45
|
-
if (opts?.query) {
|
|
46
|
-
for (const [key, value] of Object.entries(opts.query)) {
|
|
47
|
-
array(value).forEach((value) => {
|
|
48
|
-
url.searchParams.append(key, value.toString());
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
const response = await this.fetch(url.toString(), {
|
|
55
|
-
...opts,
|
|
56
|
-
body: opts?.body,
|
|
57
|
-
method: opts?.method || "GET",
|
|
58
|
-
headers: this.token
|
|
59
|
-
? { Authorization: `Bearer ${this.token}`, ...opts?.headers }
|
|
60
|
-
: opts?.headers,
|
|
61
|
-
// @ts-expect-error Node.js' and undici's Agent have different types
|
|
62
|
-
dispatcher: this.agent,
|
|
63
|
-
signal: opts?.signal,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (this.debug) {
|
|
67
|
-
const duration = Date.now() - start;
|
|
68
|
-
console.log(`[API] ${url} (${response.status}) ${duration}ms`);
|
|
69
|
-
if (response.status === 429) {
|
|
70
|
-
const retry = parseInt(response.headers.get("Retry-After") ?? "", 10);
|
|
71
|
-
const hours = Math.floor(retry / 60 / 60);
|
|
72
|
-
const minutes = Math.floor(retry / 60) % 60;
|
|
73
|
-
const seconds = retry % 60;
|
|
74
|
-
console.warn(
|
|
75
|
-
`[API] ${url} Rate Limited, Retry After ${hours}h ${minutes}m ${seconds}s`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return response;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface Parsed<Data> {
|
|
85
|
-
response: Response;
|
|
86
|
-
text: string;
|
|
87
|
-
json: Data;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Extract sandboxId from a sandbox API URL.
|
|
92
|
-
* URLs follow the pattern: /v1/sandboxes/{sandboxId}/...
|
|
93
|
-
*/
|
|
94
|
-
function extractSandboxId(url: string): string | undefined {
|
|
95
|
-
const match = url.match(/\/v1\/sandboxes\/([^/?]+)/);
|
|
96
|
-
return match?.[1];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Allows to read the response text and parse it as JSON casting to the given
|
|
101
|
-
* type. If the response is not ok or cannot be parsed it will return error.
|
|
102
|
-
*
|
|
103
|
-
* @param response Response to parse.
|
|
104
|
-
* @returns Parsed response or error.
|
|
105
|
-
*/
|
|
106
|
-
export async function parse<Data, ErrorData>(
|
|
107
|
-
validator: ZodType<Data>,
|
|
108
|
-
response: Response,
|
|
109
|
-
): Promise<Parsed<Data> | APIError<ErrorData>> {
|
|
110
|
-
const sandboxId = extractSandboxId(response.url);
|
|
111
|
-
|
|
112
|
-
const text = await response.text().catch((err) => {
|
|
113
|
-
return new APIError<ErrorData>(response, {
|
|
114
|
-
message: `Can't read response text: ${String(err)}`,
|
|
115
|
-
sandboxId,
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (typeof text !== "string") {
|
|
120
|
-
return text;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let json: Data | ErrorData;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
json = JSON.parse(text || "{}");
|
|
127
|
-
} catch (error) {
|
|
128
|
-
return new APIError<ErrorData>(response, {
|
|
129
|
-
message: `Can't parse JSON: ${String(error)}`,
|
|
130
|
-
text,
|
|
131
|
-
sandboxId,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
return new APIError<ErrorData>(response, {
|
|
137
|
-
message: `Status code ${response.status} is not ok`,
|
|
138
|
-
json: json as ErrorData,
|
|
139
|
-
text,
|
|
140
|
-
sandboxId,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const validated = validator.safeParse(json);
|
|
145
|
-
if (!validated.success) {
|
|
146
|
-
return new APIError<ErrorData>(response, {
|
|
147
|
-
message: `Response JSON is not valid: ${validated.error}`,
|
|
148
|
-
json: json as ErrorData,
|
|
149
|
-
text,
|
|
150
|
-
sandboxId,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
json: validated.data,
|
|
156
|
-
response,
|
|
157
|
-
text,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function parseOrThrow<Data, ErrorData>(
|
|
162
|
-
validator: ZodType<Data>,
|
|
163
|
-
response: Response,
|
|
164
|
-
): Promise<Parsed<Data>> {
|
|
165
|
-
const result = await parse<Data, ErrorData>(validator, response);
|
|
166
|
-
if (result instanceof APIError) {
|
|
167
|
-
throw result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import zlib from "zlib";
|
|
2
|
-
import tar, { type Pack } from "tar-stream";
|
|
3
|
-
import { Readable } from "stream";
|
|
4
|
-
|
|
5
|
-
interface FileBuffer {
|
|
6
|
-
/**
|
|
7
|
-
* The name (path) of the file to write.
|
|
8
|
-
*/
|
|
9
|
-
name: string;
|
|
10
|
-
/**
|
|
11
|
-
* The content of the file as a Buffer.
|
|
12
|
-
*/
|
|
13
|
-
content: Buffer;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface FileStream {
|
|
17
|
-
/**
|
|
18
|
-
* The name (path) of the file to write.
|
|
19
|
-
*/
|
|
20
|
-
name: string;
|
|
21
|
-
/**
|
|
22
|
-
* A Readable stream to consume the content of the file.
|
|
23
|
-
*/
|
|
24
|
-
content: Readable;
|
|
25
|
-
/**
|
|
26
|
-
* The expected size of the file. This is required to write
|
|
27
|
-
* the header of the compressed file.
|
|
28
|
-
*/
|
|
29
|
-
size: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Allows to create a Readable stream with methods to write files
|
|
34
|
-
* to it and to finish it. Files written are compressed together
|
|
35
|
-
* and gzipped in the stream.
|
|
36
|
-
*/
|
|
37
|
-
export class FileWriter {
|
|
38
|
-
public readable: Readable;
|
|
39
|
-
private pack: Pack;
|
|
40
|
-
|
|
41
|
-
constructor() {
|
|
42
|
-
const gzip = zlib.createGzip();
|
|
43
|
-
this.pack = tar.pack();
|
|
44
|
-
this.readable = this.pack.pipe(gzip);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Allows to add a file to the stream. Size is required to write
|
|
49
|
-
* the tarball header so when content is a stream it must be
|
|
50
|
-
* provided.
|
|
51
|
-
*
|
|
52
|
-
* Returns a Promise resolved once the file is written in the
|
|
53
|
-
* stream.
|
|
54
|
-
*/
|
|
55
|
-
async addFile(file: FileBuffer | FileStream) {
|
|
56
|
-
return new Promise<void>((resolve, reject) => {
|
|
57
|
-
const entry = this.pack.entry(
|
|
58
|
-
"size" in file
|
|
59
|
-
? { name: file.name, size: file.size }
|
|
60
|
-
: { name: file.name, size: file.content.length },
|
|
61
|
-
(error) => {
|
|
62
|
-
if (error) {
|
|
63
|
-
return reject(error);
|
|
64
|
-
} else {
|
|
65
|
-
resolve();
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (file.content instanceof Readable) {
|
|
71
|
-
file.content.pipe(entry);
|
|
72
|
-
} else {
|
|
73
|
-
entry.end(file.content);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Allows to finish the stream returning a Promise that will
|
|
80
|
-
* resolve once the readable is effectively closed or
|
|
81
|
-
* errored.
|
|
82
|
-
*/
|
|
83
|
-
async end() {
|
|
84
|
-
return new Promise<void>((resolve, reject) => {
|
|
85
|
-
this.readable.on("error", reject);
|
|
86
|
-
this.readable.on("end", resolve);
|
|
87
|
-
this.pack.finalize();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
package/src/api-client/index.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export type SandboxMetaData = z.infer<typeof Sandbox>;
|
|
4
|
-
|
|
5
|
-
export const Sandbox = z.object({
|
|
6
|
-
id: z.string(),
|
|
7
|
-
memory: z.number(),
|
|
8
|
-
vcpus: z.number(),
|
|
9
|
-
region: z.string(),
|
|
10
|
-
runtime: z.string(),
|
|
11
|
-
timeout: z.number(),
|
|
12
|
-
status: z.enum([
|
|
13
|
-
"pending",
|
|
14
|
-
"running",
|
|
15
|
-
"stopping",
|
|
16
|
-
"stopped",
|
|
17
|
-
"failed",
|
|
18
|
-
"snapshotting",
|
|
19
|
-
]),
|
|
20
|
-
requestedAt: z.number(),
|
|
21
|
-
startedAt: z.number().optional(),
|
|
22
|
-
requestedStopAt: z.number().optional(),
|
|
23
|
-
stoppedAt: z.number().optional(),
|
|
24
|
-
duration: z.number().optional(),
|
|
25
|
-
sourceSnapshotId: z.string().optional(),
|
|
26
|
-
snapshottedAt: z.number().optional(),
|
|
27
|
-
createdAt: z.number(),
|
|
28
|
-
cwd: z.string(),
|
|
29
|
-
updatedAt: z.number(),
|
|
30
|
-
interactivePort: z.number().optional(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export type SandboxRouteData = z.infer<typeof SandboxRoute>;
|
|
34
|
-
|
|
35
|
-
export const SandboxRoute = z.object({
|
|
36
|
-
url: z.string(),
|
|
37
|
-
subdomain: z.string(),
|
|
38
|
-
port: z.number(),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
export type SnapshotMetadata = z.infer<typeof Snapshot>;
|
|
42
|
-
|
|
43
|
-
export const Snapshot = z.object({
|
|
44
|
-
id: z.string(),
|
|
45
|
-
sourceSandboxId: z.string(),
|
|
46
|
-
region: z.string(),
|
|
47
|
-
status: z.enum(["created", "deleted", "failed"]),
|
|
48
|
-
sizeBytes: z.number(),
|
|
49
|
-
expiresAt: z.number(),
|
|
50
|
-
createdAt: z.number(),
|
|
51
|
-
updatedAt: z.number(),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export const Pagination = z.object({
|
|
55
|
-
/**
|
|
56
|
-
* Amount of items in the current page.
|
|
57
|
-
* @example 20
|
|
58
|
-
*/
|
|
59
|
-
count: z.number(),
|
|
60
|
-
/**
|
|
61
|
-
* Timestamp that must be used to request the next page.
|
|
62
|
-
* @example 1540095775951
|
|
63
|
-
*/
|
|
64
|
-
next: z.number().nullable(),
|
|
65
|
-
/**
|
|
66
|
-
* Timestamp that must be used to request the previous page.
|
|
67
|
-
* @example 1540095775951
|
|
68
|
-
*/
|
|
69
|
-
prev: z.number().nullable(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
export type CommandData = z.infer<typeof Command>;
|
|
73
|
-
|
|
74
|
-
export const Command = z.object({
|
|
75
|
-
id: z.string(),
|
|
76
|
-
name: z.string(),
|
|
77
|
-
args: z.array(z.string()),
|
|
78
|
-
cwd: z.string(),
|
|
79
|
-
sandboxId: z.string(),
|
|
80
|
-
exitCode: z.number().nullable(),
|
|
81
|
-
startedAt: z.number(),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const CommandFinished = Command.extend({
|
|
85
|
-
exitCode: z.number(),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
export const SandboxResponse = z.object({
|
|
89
|
-
sandbox: Sandbox,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
export const SandboxAndRoutesResponse = SandboxResponse.extend({
|
|
93
|
-
routes: z.array(SandboxRoute),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
export const CommandResponse = z.object({
|
|
97
|
-
command: Command,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export const CommandFinishedResponse = z.object({
|
|
101
|
-
command: CommandFinished,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
export const EmptyResponse = z.object({});
|
|
105
|
-
|
|
106
|
-
const LogLineBase = z.object({ data: z.string() });
|
|
107
|
-
export const LogLineStdout = LogLineBase.extend({
|
|
108
|
-
stream: z.literal("stdout"),
|
|
109
|
-
});
|
|
110
|
-
export const LogLineStderr = LogLineBase.extend({
|
|
111
|
-
stream: z.literal("stderr"),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export const LogError = z.object({
|
|
115
|
-
stream: z.literal("error"),
|
|
116
|
-
data: z.object({
|
|
117
|
-
code: z.string(),
|
|
118
|
-
message: z.string(),
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
export const LogLine = z.discriminatedUnion("stream", [
|
|
123
|
-
LogLineStdout,
|
|
124
|
-
LogLineStderr,
|
|
125
|
-
LogError,
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
export const SandboxesResponse = z.object({
|
|
129
|
-
sandboxes: z.array(Sandbox),
|
|
130
|
-
pagination: Pagination,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
export const ExtendTimeoutResponse = z.object({
|
|
134
|
-
sandbox: Sandbox,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
export const CreateSnapshotResponse = z.object({
|
|
138
|
-
snapshot: Snapshot,
|
|
139
|
-
sandbox: Sandbox,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
export const SnapshotResponse = z.object({
|
|
143
|
-
snapshot: Snapshot,
|
|
144
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { Options as RetryOptions } from "async-retry";
|
|
2
|
-
import { APIError } from "./api-error";
|
|
3
|
-
import { setTimeout } from "node:timers/promises";
|
|
4
|
-
import retry from "async-retry";
|
|
5
|
-
|
|
6
|
-
export interface RequestOptions {
|
|
7
|
-
onRetry?(error: any, options: RequestOptions): void;
|
|
8
|
-
retry?: Partial<RetryOptions>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Wraps a fetch function with retry logic. The retry logic will retry
|
|
13
|
-
* on network errors, 429 responses and 5xx responses. The retry logic
|
|
14
|
-
* will not retry on 4xx responses.
|
|
15
|
-
*
|
|
16
|
-
* @param rawFetch The fetch function to wrap.
|
|
17
|
-
* @returns The wrapped fetch function.
|
|
18
|
-
*/
|
|
19
|
-
export function withRetry<T extends RequestInit>(
|
|
20
|
-
rawFetch: (url: URL | string, init?: T) => Promise<Response>,
|
|
21
|
-
) {
|
|
22
|
-
return async (
|
|
23
|
-
url: URL | string,
|
|
24
|
-
opts: T & RequestOptions = <T & RequestOptions>{},
|
|
25
|
-
) => {
|
|
26
|
-
/**
|
|
27
|
-
* Timeouts by default will be [10, 60, 360, 2160, 12960]
|
|
28
|
-
* before randomization is added.
|
|
29
|
-
*/
|
|
30
|
-
const retryOpts = Object.assign(
|
|
31
|
-
{
|
|
32
|
-
minTimeout: 10,
|
|
33
|
-
retries: 5,
|
|
34
|
-
factor: 6,
|
|
35
|
-
maxRetryAfter: 20,
|
|
36
|
-
},
|
|
37
|
-
opts.retry,
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
if (opts.onRetry) {
|
|
41
|
-
retryOpts.onRetry = (error, attempts) => {
|
|
42
|
-
opts.onRetry!(error, opts);
|
|
43
|
-
if (opts.retry && opts.retry.onRetry) {
|
|
44
|
-
opts.retry.onRetry(error, attempts);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
return (await retry(async (bail) => {
|
|
51
|
-
try {
|
|
52
|
-
if (opts.signal?.aborted) {
|
|
53
|
-
return bail(opts.signal.reason || new Error("Request aborted"));
|
|
54
|
-
}
|
|
55
|
-
const response = await rawFetch(url, opts);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* When the response is 429 we will try to parse the Retry-After
|
|
59
|
-
* header. If the header exists we will try to parse it and, if
|
|
60
|
-
* the wait time is higher than the maximum defined, we respond.
|
|
61
|
-
* Otherwise we wait for the time given in the header and throw
|
|
62
|
-
* to retry.
|
|
63
|
-
*/
|
|
64
|
-
if (response.status === 429) {
|
|
65
|
-
const retryAfter = parseInt(
|
|
66
|
-
response.headers.get("retry-after") || "",
|
|
67
|
-
10,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (retryAfter && !isNaN(retryAfter)) {
|
|
71
|
-
if (retryAfter > retryOpts.maxRetryAfter) {
|
|
72
|
-
return response;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await setTimeout(retryAfter * 1e3);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
throw new APIError(response);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* If the response is a a retryable error, we throw in
|
|
83
|
-
* order to retry.
|
|
84
|
-
*/
|
|
85
|
-
if (response.status >= 500 && response.status < 600) {
|
|
86
|
-
throw new APIError(response);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return response;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
/**
|
|
92
|
-
* If the request was aborted using the AbortController
|
|
93
|
-
* we bail from retrying throwing the original error.
|
|
94
|
-
*/
|
|
95
|
-
if (isAbortError(error)) {
|
|
96
|
-
return bail(error);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* If the signal was aborted meanwhile we were
|
|
101
|
-
* waiting, we bail from retrying.
|
|
102
|
-
*/
|
|
103
|
-
if (opts.signal?.aborted) {
|
|
104
|
-
return bail(opts.signal.reason || new Error("Request aborted"));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}, retryOpts)) as Response;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
/**
|
|
112
|
-
* The ResponseError is only intended for retries so in case we
|
|
113
|
-
* ran out of attempts we will respond with the last response
|
|
114
|
-
* we obtained.
|
|
115
|
-
*/
|
|
116
|
-
if (error instanceof APIError) {
|
|
117
|
-
return error.response;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isAbortError(error: unknown): error is Error {
|
|
126
|
-
return (
|
|
127
|
-
error !== undefined &&
|
|
128
|
-
error !== null &&
|
|
129
|
-
(error as Error).name === "AbortError"
|
|
130
|
-
);
|
|
131
|
-
}
|
package/src/auth/api.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { NotOk } from "./error";
|
|
2
|
-
|
|
3
|
-
export async function fetchApi(opts: {
|
|
4
|
-
token: string;
|
|
5
|
-
endpoint: string;
|
|
6
|
-
method?: string;
|
|
7
|
-
body?: string;
|
|
8
|
-
}): Promise<unknown> {
|
|
9
|
-
const x = await fetch(`https://api.vercel.com${opts.endpoint}`, {
|
|
10
|
-
method: opts.method,
|
|
11
|
-
body: opts.body,
|
|
12
|
-
headers: {
|
|
13
|
-
Authorization: `Bearer ${opts.token}`,
|
|
14
|
-
"Content-Type": "application/json",
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
if (!x.ok) {
|
|
18
|
-
let message = await x.text();
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const { error } = JSON.parse(message);
|
|
22
|
-
message = `${error.code.toUpperCase()}: ${error.message}`;
|
|
23
|
-
} catch {}
|
|
24
|
-
|
|
25
|
-
throw new NotOk({
|
|
26
|
-
responseText: message,
|
|
27
|
-
statusCode: x.status,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
return (await x.json()) as unknown;
|
|
31
|
-
}
|
package/src/auth/error.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export class NotOk extends Error {
|
|
2
|
-
name = "NotOk";
|
|
3
|
-
response: { statusCode: number; responseText: string };
|
|
4
|
-
constructor(response: { statusCode: number; responseText: string }) {
|
|
5
|
-
super(`HTTP ${response.statusCode}: ${response.responseText}`);
|
|
6
|
-
this.response = response;
|
|
7
|
-
}
|
|
8
|
-
}
|
package/src/auth/file.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import XDGAppPaths from "xdg-app-paths";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { json } from "./zod";
|
|
7
|
-
|
|
8
|
-
const ZodDate = z.number().transform((seconds) => new Date(seconds * 1000));
|
|
9
|
-
|
|
10
|
-
const AuthFile = z.object({
|
|
11
|
-
token: z.string().min(1).optional(),
|
|
12
|
-
refreshToken: z.string().min(1).optional(),
|
|
13
|
-
expiresAt: ZodDate.optional(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const StoredAuthFile = json.pipe(AuthFile);
|
|
17
|
-
|
|
18
|
-
type AuthFile = z.infer<typeof AuthFile>;
|
|
19
|
-
|
|
20
|
-
// Returns whether a directory exists
|
|
21
|
-
const isDirectory = (path: string): boolean => {
|
|
22
|
-
try {
|
|
23
|
-
return fs.lstatSync(path).isDirectory();
|
|
24
|
-
} catch (_) {
|
|
25
|
-
// We don't care which kind of error occured, it isn't a directory anyway.
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Returns in which directory the config should be present
|
|
31
|
-
const getGlobalPathConfig = (): string => {
|
|
32
|
-
const vercelDirectories = XDGAppPaths("com.vercel.cli").dataDirs();
|
|
33
|
-
|
|
34
|
-
const possibleConfigPaths = [
|
|
35
|
-
...vercelDirectories, // latest vercel directory
|
|
36
|
-
path.join(homedir(), ".now"), // legacy config in user's home directory
|
|
37
|
-
...XDGAppPaths("now").dataDirs(), // legacy XDG directory
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
// The customPath flag is the preferred location,
|
|
41
|
-
// followed by the vercel directory,
|
|
42
|
-
// followed by the now directory.
|
|
43
|
-
// If none of those exist, use the vercel directory.
|
|
44
|
-
return (
|
|
45
|
-
possibleConfigPaths.find((configPath) => isDirectory(configPath)) ||
|
|
46
|
-
vercelDirectories[0]
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const getAuth = () => {
|
|
51
|
-
try {
|
|
52
|
-
const pathname = path.join(getGlobalPathConfig(), "auth.json");
|
|
53
|
-
return StoredAuthFile.parse(fs.readFileSync(pathname, "utf8"));
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export function updateAuthConfig(config: AuthFile): void {
|
|
60
|
-
const pathname = path.join(getGlobalPathConfig(), "auth.json");
|
|
61
|
-
fs.mkdirSync(path.dirname(pathname), { recursive: true });
|
|
62
|
-
const content = {
|
|
63
|
-
token: config.token,
|
|
64
|
-
expiresAt:
|
|
65
|
-
config.expiresAt && Math.round(config.expiresAt.getTime() / 1000),
|
|
66
|
-
refreshToken: config.refreshToken,
|
|
67
|
-
} satisfies z.input<typeof AuthFile>;
|
|
68
|
-
fs.writeFileSync(pathname, JSON.stringify(content) + "\n");
|
|
69
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// This file can also be imported as `@vercel/sandbox/dist/auth`, which is completely fine.
|
|
2
|
-
// The only valid importer of this would be the CLI as we share the same codebase.
|
|
3
|
-
|
|
4
|
-
export * from "./file";
|
|
5
|
-
export type * from "./file";
|
|
6
|
-
export * from "./oauth";
|
|
7
|
-
export type * from "./oauth";
|
|
8
|
-
export { pollForToken } from "./poll-for-token";
|
|
9
|
-
export { inferScope, selectTeam } from "./project";
|