promigence 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +88 -0
- package/dist/account.js +850 -0
- package/dist/agent.js +374 -0
- package/dist/argv.js +159 -0
- package/dist/bin.js +332 -0
- package/dist/bundle.js +133 -0
- package/dist/capacity.js +69 -0
- package/dist/commands.d.ts +110 -0
- package/dist/commands.js +2675 -0
- package/dist/connectreq.js +125 -0
- package/dist/credfile.js +188 -0
- package/dist/credits.js +197 -0
- package/dist/devcontainer.d.ts +99 -0
- package/dist/devcontainer.js +744 -0
- package/dist/device.js +129 -0
- package/dist/dirsnap.js +378 -0
- package/dist/errfix.js +262 -0
- package/dist/generated.d.ts +35 -0
- package/dist/generated.js +307 -0
- package/dist/github.js +307 -0
- package/dist/gitlab.js +210 -0
- package/dist/guide.d.ts +19 -0
- package/dist/guide.js +366 -0
- package/dist/handlers.js +511 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/init.d.ts +40 -0
- package/dist/init.js +352 -0
- package/dist/install.d.ts +23 -0
- package/dist/install.js +127 -0
- package/dist/io.d.ts +37 -0
- package/dist/io.js +318 -0
- package/dist/live.js +469 -0
- package/dist/login.js +1063 -0
- package/dist/mcp.js +889 -0
- package/dist/money.js +5 -0
- package/dist/orgs.js +475 -0
- package/dist/pricing.d.ts +52 -0
- package/dist/pricing.js +181 -0
- package/dist/report.js +132 -0
- package/dist/routes.js +135 -0
- package/dist/runners-github.js +257 -0
- package/dist/runners-gitlab.js +125 -0
- package/dist/runners-spec.js +137 -0
- package/dist/runners.js +1458 -0
- package/dist/sdk/blake3.d.ts +10 -0
- package/dist/sdk/blake3.js +111 -0
- package/dist/sdk/bundle.d.ts +96 -0
- package/dist/sdk/bundle.js +227 -0
- package/dist/sdk/ca-guard.js +166 -0
- package/dist/sdk/caller.d.ts +73 -0
- package/dist/sdk/caller.js +47 -0
- package/dist/sdk/client.d.ts +3015 -0
- package/dist/sdk/client.js +2583 -0
- package/dist/sdk/command-errors.d.ts +41 -0
- package/dist/sdk/command-errors.js +36 -0
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +285 -0
- package/dist/sdk/core.js +266 -0
- package/dist/sdk/credstore.d.ts +90 -0
- package/dist/sdk/credstore.js +206 -0
- package/dist/sdk/dispatcher.js +86 -0
- package/dist/sdk/github.d.ts +158 -0
- package/dist/sdk/github.js +78 -0
- package/dist/sdk/gitlab.d.ts +104 -0
- package/dist/sdk/gitlab.js +20 -0
- package/dist/sdk/handles.d.ts +82 -0
- package/dist/sdk/handles.js +135 -0
- package/dist/sdk/hint.js +7 -0
- package/dist/sdk/http2.js +1921 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +15 -0
- package/dist/sdk/keyed.gen.js +7 -0
- package/dist/sdk/models.gen.d.ts +6162 -0
- package/dist/sdk/models.gen.js +1 -0
- package/dist/sdk/proxy.js +249 -0
- package/dist/sdk/pty.d.ts +171 -0
- package/dist/sdk/pty.js +496 -0
- package/dist/sdk/sandbox.d.ts +537 -0
- package/dist/sdk/sandbox.js +725 -0
- package/dist/sdk/ssh.d.ts +25 -0
- package/dist/sdk/ssh.js +114 -0
- package/dist/sdk/trace.js +80 -0
- package/dist/sdk/transport-worker.js +466 -0
- package/dist/sdk/warm.js +40 -0
- package/dist/ssh.js +267 -0
- package/dist/surface.d.ts +95 -0
- package/dist/surface.js +1452 -0
- package/dist/templates.js +53 -0
- package/dist/term.js +50 -0
- package/dist/timing.js +71 -0
- package/dist/untar.js +324 -0
- package/dist/uploadguard.js +75 -0
- package/llms.txt +209 -0
- package/package.json +52 -0
|
@@ -0,0 +1,2583 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { inspect } from "node:util";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, ERRORS_DOCS_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, exitForCode, previewsOff, withPreviewsFix, } from "./core.js";
|
|
6
|
+
import { buildBundle } from "./bundle.js";
|
|
7
|
+
import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
|
|
8
|
+
import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
|
|
9
|
+
import { connectPty } from "./pty.js";
|
|
10
|
+
import { connectSshTunnel } from "./ssh.js";
|
|
11
|
+
import { H2Response, Http2Transport, http2Allowed, warmTls } from "./http2.js";
|
|
12
|
+
import { CONTEXT_TAR_PREFIX } from "./context.js";
|
|
13
|
+
import { proxyFetch, proxyFor, transportEnv } from "./proxy.js";
|
|
14
|
+
import { mark, traceBegin, traceEvent } from "./trace.js";
|
|
15
|
+
import { KEYED_CREATES } from "./keyed.gen.js";
|
|
16
|
+
import { customDispatcher, longRequestDispatcher } from "./dispatcher.js";
|
|
17
|
+
import { compromisedKeyCause, guardedFetchDispatcher } from "./ca-guard.js";
|
|
18
|
+
import { hintArg } from "./hint.js";
|
|
19
|
+
import { GITHUB_REPO_PREFIX, githubCreateBody, } from "./github.js";
|
|
20
|
+
import { GITLAB_REPO_PREFIX, } from "./gitlab.js";
|
|
21
|
+
export const FILE_TIMEOUT_MS = 15 * 60_000;
|
|
22
|
+
export const isWaiting = (s) => s === "queued" || s === "awaiting_capacity";
|
|
23
|
+
function b64ToBytes(s) {
|
|
24
|
+
if (!s)
|
|
25
|
+
return new Uint8Array(0);
|
|
26
|
+
const bin = typeof atob === "function"
|
|
27
|
+
? atob(s)
|
|
28
|
+
: Buffer.from(s, "base64").toString("binary");
|
|
29
|
+
const out = new Uint8Array(bin.length);
|
|
30
|
+
for (let i = 0; i < bin.length; i++)
|
|
31
|
+
out[i] = bin.charCodeAt(i);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function bytesToB64(b) {
|
|
35
|
+
if (typeof btoa !== "function")
|
|
36
|
+
return Buffer.from(b).toString("base64");
|
|
37
|
+
let bin = "";
|
|
38
|
+
for (let i = 0; i < b.length; i += 8192)
|
|
39
|
+
bin += String.fromCharCode(...b.subarray(i, i + 8192));
|
|
40
|
+
return btoa(bin);
|
|
41
|
+
}
|
|
42
|
+
function modeParam(mode) {
|
|
43
|
+
return typeof mode === "number" ? mode.toString(8) : mode;
|
|
44
|
+
}
|
|
45
|
+
export function execOutputBytes(r, stream = "stdout") {
|
|
46
|
+
const b64 = r[`${stream}_b64`];
|
|
47
|
+
if (typeof b64 === "string")
|
|
48
|
+
return b64ToBytes(b64);
|
|
49
|
+
return new TextEncoder().encode(r[stream] ?? "");
|
|
50
|
+
}
|
|
51
|
+
function pageOf(r, key, asked) {
|
|
52
|
+
if (Array.isArray(r))
|
|
53
|
+
return { items: r, nextCursor: null, limit: asked };
|
|
54
|
+
const o = (r ?? {});
|
|
55
|
+
const items = Array.isArray(o[key]) ? o[key] : [];
|
|
56
|
+
const c = o["next_cursor"];
|
|
57
|
+
return {
|
|
58
|
+
items,
|
|
59
|
+
nextCursor: typeof c === "string" && c !== "" ? c : null,
|
|
60
|
+
limit: typeof o["limit"] === "number" ? o["limit"] : asked,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function orgSeg(org) {
|
|
64
|
+
if (!org?.trim())
|
|
65
|
+
throw new InvalidArgumentError("an organization is required", "Pass its id (org_…) or name; me().org_id is this key's.");
|
|
66
|
+
return encodeURIComponent(org.trim());
|
|
67
|
+
}
|
|
68
|
+
function pageQuery(limit, max, cursor) {
|
|
69
|
+
const q = new URLSearchParams({ limit: String(Math.max(1, Math.min(max, Math.floor(limit)))) });
|
|
70
|
+
if (cursor)
|
|
71
|
+
q.set("cursor", cursor);
|
|
72
|
+
return q.toString();
|
|
73
|
+
}
|
|
74
|
+
function expiryFields(o) {
|
|
75
|
+
const body = {};
|
|
76
|
+
if (o.expiresInDays !== undefined)
|
|
77
|
+
body["expires_in_days"] = o.expiresInDays;
|
|
78
|
+
if (o.expiresAt !== undefined)
|
|
79
|
+
body["expires_at"] = o.expiresAt;
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
export const NOTICE_HEADER = "promigence-notice";
|
|
83
|
+
export const MAX_SOURCES = 8;
|
|
84
|
+
export function configDir(env = process.env) {
|
|
85
|
+
return join(env["XDG_CONFIG_HOME"] || join(homedir(), ".config"), "promigence");
|
|
86
|
+
}
|
|
87
|
+
export function credentialsPath(env = process.env) {
|
|
88
|
+
return join(configDir(env), "credentials");
|
|
89
|
+
}
|
|
90
|
+
export function credentialsTrustProblem(path) {
|
|
91
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
92
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : undefined;
|
|
93
|
+
if (uid === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
for (const p of [dirname(path), path]) {
|
|
96
|
+
let st;
|
|
97
|
+
try {
|
|
98
|
+
st = statSync(p);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (st.uid !== uid)
|
|
104
|
+
return `${p} belongs to another user (uid ${st.uid})`;
|
|
105
|
+
if ((st.mode & 0o002) !== 0)
|
|
106
|
+
return `${p} is writable by every user`;
|
|
107
|
+
if ((st.mode & 0o020) !== 0 && st.gid !== gid)
|
|
108
|
+
return `${p} is writable by group ${st.gid}`;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const credentialsWarned = new Set();
|
|
113
|
+
export function credentialsExposure(path) {
|
|
114
|
+
if (typeof process.getuid !== "function")
|
|
115
|
+
return undefined;
|
|
116
|
+
let st;
|
|
117
|
+
try {
|
|
118
|
+
st = statSync(path);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if ((st.mode & 0o044) === 0)
|
|
124
|
+
return undefined;
|
|
125
|
+
return `${path} is readable by ${(st.mode & 0o004) !== 0 ? "every user" : "its group"} (mode ${(st.mode & 0o777).toString(8)})`;
|
|
126
|
+
}
|
|
127
|
+
function decodeCredentials(buf) {
|
|
128
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe)
|
|
129
|
+
return buf.subarray(2).toString("utf16le");
|
|
130
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
131
|
+
const le = Buffer.from(buf.subarray(2));
|
|
132
|
+
le.swap16();
|
|
133
|
+
return le.toString("utf16le");
|
|
134
|
+
}
|
|
135
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf)
|
|
136
|
+
return buf.subarray(3).toString("utf8");
|
|
137
|
+
return buf.toString("utf8");
|
|
138
|
+
}
|
|
139
|
+
export function storedCredentials(env = process.env, platform = process.platform) {
|
|
140
|
+
const path = credentialsPath(env);
|
|
141
|
+
let present = true;
|
|
142
|
+
try {
|
|
143
|
+
statSync(path);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
present = e.code !== "ENOENT";
|
|
147
|
+
}
|
|
148
|
+
if (!present) {
|
|
149
|
+
const store = keychainStore(env, platform);
|
|
150
|
+
if (store === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
const r = keychainReadCached(store, path, env);
|
|
153
|
+
if (r.status === "found")
|
|
154
|
+
return { text: r.text, store, path };
|
|
155
|
+
if (r.status === "absent")
|
|
156
|
+
return undefined;
|
|
157
|
+
if (r.locked) {
|
|
158
|
+
const why = `${storeLabel(store)} is locked`;
|
|
159
|
+
if (!credentialsWarned.has(why)) {
|
|
160
|
+
credentialsWarned.add(why);
|
|
161
|
+
process.emitWarning(`${why}, so the Promigence key stored in it cannot be read (an SSH session cannot unlock it). `
|
|
162
|
+
+ (store === "macos-keychain" ? "Unlock it with `security unlock-keychain`, or " : "Unlock it, or ")
|
|
163
|
+
+ "set PROMIGENCE_API_KEY for this shell.", { code: "PROMIGENCE_CREDENTIALS" });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { unavailable: { store, reason: r.reason, locked: r.locked }, path };
|
|
167
|
+
}
|
|
168
|
+
const untrusted = credentialsTrustProblem(path);
|
|
169
|
+
if (untrusted !== undefined) {
|
|
170
|
+
if (!credentialsWarned.has(untrusted)) {
|
|
171
|
+
credentialsWarned.add(untrusted);
|
|
172
|
+
process.emitWarning(`ignoring the Promigence credentials file: ${untrusted}. Fix the owner or mode (chmod go-w), `
|
|
173
|
+
+ "or set XDG_CONFIG_HOME to a directory only you can write.", { code: "PROMIGENCE_CREDENTIALS" });
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = decodeCredentials(readFileSync(path));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const exposed = credentialsExposure(path);
|
|
185
|
+
if (exposed !== undefined && !credentialsWarned.has(exposed)) {
|
|
186
|
+
credentialsWarned.add(exposed);
|
|
187
|
+
process.emitWarning(`the Promigence credentials file holds an API key and ${exposed}. `
|
|
188
|
+
+ `Run \`chmod 600 ${path}\` (promigence login writes it that way).`, { code: "PROMIGENCE_CREDENTIALS" });
|
|
189
|
+
}
|
|
190
|
+
return { text: raw, store: "file", path };
|
|
191
|
+
}
|
|
192
|
+
export function storedCredentialsText(env = process.env) {
|
|
193
|
+
const s = storedCredentials(env);
|
|
194
|
+
return s === undefined || "unavailable" in s ? undefined : s;
|
|
195
|
+
}
|
|
196
|
+
function parseCredentials(raw) {
|
|
197
|
+
const text = raw.trim();
|
|
198
|
+
if (!text)
|
|
199
|
+
return {};
|
|
200
|
+
if (text.startsWith("{")) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(text);
|
|
203
|
+
const out = {};
|
|
204
|
+
const key = parsed["api_key"] ?? parsed["apiKey"];
|
|
205
|
+
const url = parsed["api_url"] ?? parsed["apiUrl"];
|
|
206
|
+
if (typeof key === "string" && key.trim())
|
|
207
|
+
out.api_key = key.trim();
|
|
208
|
+
if (typeof url === "string" && url)
|
|
209
|
+
out.api_url = url;
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const first = text.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("#"));
|
|
217
|
+
return first ? { api_key: first } : {};
|
|
218
|
+
}
|
|
219
|
+
export function readCredentialsWithStore(env = process.env) {
|
|
220
|
+
const s = storedCredentials(env);
|
|
221
|
+
if (s === undefined)
|
|
222
|
+
return { creds: {}, store: undefined };
|
|
223
|
+
if ("unavailable" in s)
|
|
224
|
+
return { creds: {}, store: undefined, unavailable: s.unavailable };
|
|
225
|
+
return { creds: parseCredentials(s.text), store: s.store };
|
|
226
|
+
}
|
|
227
|
+
export function readCredentials(env = process.env) {
|
|
228
|
+
return readCredentialsWithStore(env).creds;
|
|
229
|
+
}
|
|
230
|
+
function storedName(store, env) {
|
|
231
|
+
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : storeLabel(store);
|
|
232
|
+
}
|
|
233
|
+
const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
|
|
234
|
+
export function apiUrlFromCompatUrl(url) {
|
|
235
|
+
const u = (url ?? "").trim();
|
|
236
|
+
if (!u)
|
|
237
|
+
return undefined;
|
|
238
|
+
let host = "";
|
|
239
|
+
try {
|
|
240
|
+
host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
|
|
246
|
+
}
|
|
247
|
+
export function apiUrlFromCompatDomain(domain) {
|
|
248
|
+
const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
249
|
+
if (!d || COMPAT_REFUSED_HOSTS.test(d))
|
|
250
|
+
return undefined;
|
|
251
|
+
return `https://api.${d}/v1`;
|
|
252
|
+
}
|
|
253
|
+
export function compatApiKey(key) {
|
|
254
|
+
const k = key?.trim();
|
|
255
|
+
return k && !k.startsWith("e2b_") ? k : undefined;
|
|
256
|
+
}
|
|
257
|
+
export function keyOrigin(env = process.env) {
|
|
258
|
+
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
259
|
+
return "PROMIGENCE_API_KEY";
|
|
260
|
+
const stored = readCredentialsWithStore(env);
|
|
261
|
+
if (stored.creds.api_key)
|
|
262
|
+
return stored.store === "macos-keychain" ? "macOS Keychain" : stored.store === "secret-service" ? "Secret Service keyring" : "credentials file";
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
function resolveCredentials(opts, env = process.env) {
|
|
266
|
+
const set = (v) => (v ? v : undefined);
|
|
267
|
+
const trim = (u) => {
|
|
268
|
+
const t = u.replace(/\/+$/, "");
|
|
269
|
+
return /^https?:\/\/[^/?#]+$/i.test(t) ? `${t}/v1` : t;
|
|
270
|
+
};
|
|
271
|
+
const optKey = opts.apiKey?.trim();
|
|
272
|
+
const envKey = optKey === undefined ? set(env["PROMIGENCE_API_KEY"]?.trim()) : undefined;
|
|
273
|
+
const envUrl = optKey !== undefined && opts.apiUrl !== undefined ? undefined : set(env["PROMIGENCE_API_URL"]);
|
|
274
|
+
const given = optKey !== undefined || envKey !== undefined;
|
|
275
|
+
const { creds, store } = given
|
|
276
|
+
? { creds: {}, store: undefined }
|
|
277
|
+
: readCredentialsWithStore(env);
|
|
278
|
+
const source = optKey !== undefined ? "opts" : envKey !== undefined ? "env" : creds.api_key !== undefined ? "file" : undefined;
|
|
279
|
+
const ours = optKey ?? envKey ?? creds.api_key;
|
|
280
|
+
const fallback = ours === undefined ? opts.fallbackKey : undefined;
|
|
281
|
+
const fallbackKey = set(fallback?.apiKey.trim());
|
|
282
|
+
const fallbackUrl = fallbackKey === undefined ? undefined : set(fallback?.apiUrl);
|
|
283
|
+
const fromFile = () => trim(creds.api_url ?? fallbackUrl ?? DEFAULT_API_URL);
|
|
284
|
+
const fromEnv = () => (envUrl !== undefined ? trim(envUrl) : fromFile());
|
|
285
|
+
const apiUrl = opts.apiUrl !== undefined ? trim(opts.apiUrl) : fromEnv();
|
|
286
|
+
const paired = source === "file" ? fromFile() : source === "env" ? fromEnv() : apiUrl;
|
|
287
|
+
if (ours !== undefined && paired !== apiUrl) {
|
|
288
|
+
const keyFrom = source === "file" ? (store === undefined || store === "file" ? "the credentials file" : storeLabel(store)) : "PROMIGENCE_API_KEY";
|
|
289
|
+
const urlFrom = opts.apiUrl !== undefined ? "apiUrl" : "PROMIGENCE_API_URL";
|
|
290
|
+
const refusal = new InvalidArgumentError(`the API key from ${keyFrom} belongs to ${paired}; it is not sent to ${apiUrl} (from ${urlFrom})`, source === "file"
|
|
291
|
+
? `Pass the key with the URL (apiKey beside apiUrl, or PROMIGENCE_API_KEY beside PROMIGENCE_API_URL), or run \`promigence login\` against ${apiUrl}.`
|
|
292
|
+
: "Pass apiKey together with apiUrl, or set PROMIGENCE_API_URL to that URL instead of passing apiUrl.");
|
|
293
|
+
return { apiKey: undefined, apiUrl, refusal };
|
|
294
|
+
}
|
|
295
|
+
const key = ours ?? fallbackKey;
|
|
296
|
+
if (key) {
|
|
297
|
+
const from = source === "opts" ? "the apiKey option" : source === "env" ? "PROMIGENCE_API_KEY"
|
|
298
|
+
: source === "file" ? storedName(store, env) : fallback.from;
|
|
299
|
+
const refusal = keyRefusal(key, from);
|
|
300
|
+
if (refusal)
|
|
301
|
+
return { apiKey: undefined, apiUrl, refusal };
|
|
302
|
+
}
|
|
303
|
+
return { apiKey: key, apiUrl };
|
|
304
|
+
}
|
|
305
|
+
function keyRefusal(key, from) {
|
|
306
|
+
if (/^[\x21-\x7E]+$/.test(key))
|
|
307
|
+
return undefined;
|
|
308
|
+
return new AuthError(`the API key from ${from} is not a valid HTTP header value (it holds a space, a control character or a non-ASCII character)`, `Check ${from} for stray characters inside the key, or run \`promigence login\` to store a fresh one.`);
|
|
309
|
+
}
|
|
310
|
+
function transportDetail(err) {
|
|
311
|
+
const refused = compromisedKeyCause(err);
|
|
312
|
+
if (refused !== undefined)
|
|
313
|
+
return refused.message;
|
|
314
|
+
const e = err;
|
|
315
|
+
if (e?.name === "TypeError" && /header/i.test(e.message ?? ""))
|
|
316
|
+
return "a request header holds a character HTTP does not allow";
|
|
317
|
+
return e?.cause?.code ?? e?.message ?? "connection failed";
|
|
318
|
+
}
|
|
319
|
+
function bodyExtra(payload) {
|
|
320
|
+
const e = payload.error;
|
|
321
|
+
return {
|
|
322
|
+
...(e?.docs ? { docs: e.docs } : {}),
|
|
323
|
+
...(typeof e?.category === "string" ? { category: e.category } : {}),
|
|
324
|
+
...(typeof e?.retryable === "boolean" ? { retryable: e.retryable } : {}),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function codeForStatus(status) {
|
|
328
|
+
if (status === 400 || status === 422)
|
|
329
|
+
return "usage";
|
|
330
|
+
if (status === 401 || status === 403)
|
|
331
|
+
return "auth";
|
|
332
|
+
if (status === 404)
|
|
333
|
+
return "not_found";
|
|
334
|
+
if (status === 409)
|
|
335
|
+
return "cap_exceeded";
|
|
336
|
+
if (status === 402)
|
|
337
|
+
return "cap_exceeded";
|
|
338
|
+
if (status === 429)
|
|
339
|
+
return "rate_limited";
|
|
340
|
+
if (status === 502 || status === 503 || status === 504)
|
|
341
|
+
return "unreachable";
|
|
342
|
+
return "internal";
|
|
343
|
+
}
|
|
344
|
+
function errorCodeOf(text) {
|
|
345
|
+
try {
|
|
346
|
+
const code = JSON.parse(text)?.error?.code;
|
|
347
|
+
return typeof code === "string" ? code : undefined;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export const IMAGE_DIGEST_RE = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
|
|
354
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
355
|
+
const IMAGE_TAG_RE = /^[A-Za-z0-9_.-]+$/;
|
|
356
|
+
const IMAGE_REPO_RE = /^[a-z0-9._/-]+$/;
|
|
357
|
+
export function imageRefProblem(image) {
|
|
358
|
+
if (typeof image !== "string")
|
|
359
|
+
return "it is not a string";
|
|
360
|
+
const ref = image.trim();
|
|
361
|
+
if (!ref)
|
|
362
|
+
return "it is empty";
|
|
363
|
+
if (ref.length > 1024)
|
|
364
|
+
return "it is longer than 1024 characters";
|
|
365
|
+
if (/[\s\x00-\x1f\x7f]/.test(ref))
|
|
366
|
+
return "it contains whitespace";
|
|
367
|
+
let rest = ref;
|
|
368
|
+
const at = rest.lastIndexOf("@");
|
|
369
|
+
if (at >= 0) {
|
|
370
|
+
const digest = rest.slice(at + 1);
|
|
371
|
+
if (!SHA256_RE.test(digest))
|
|
372
|
+
return `digest ${JSON.stringify(digest)} is not \`sha256:\` followed by 64 lower-case hex characters`;
|
|
373
|
+
rest = rest.slice(0, at);
|
|
374
|
+
}
|
|
375
|
+
const colon = rest.lastIndexOf(":");
|
|
376
|
+
if (colon >= 0 && !rest.slice(colon + 1).includes("/")) {
|
|
377
|
+
const tag = rest.slice(colon + 1);
|
|
378
|
+
if (!tag)
|
|
379
|
+
return "its tag is empty";
|
|
380
|
+
if (!IMAGE_TAG_RE.test(tag))
|
|
381
|
+
return `tag ${JSON.stringify(tag)} is not a valid tag`;
|
|
382
|
+
rest = rest.slice(0, colon);
|
|
383
|
+
}
|
|
384
|
+
const slash = rest.indexOf("/");
|
|
385
|
+
const head = slash >= 0 ? rest.slice(0, slash) : "";
|
|
386
|
+
const repo = slash >= 0 && (head.includes(".") || head.includes(":") || head === "localhost") ? rest.slice(slash + 1) : rest;
|
|
387
|
+
if (!repo)
|
|
388
|
+
return "it names no repository";
|
|
389
|
+
if (!IMAGE_REPO_RE.test(repo)) {
|
|
390
|
+
return `repository ${JSON.stringify(repo)} is not a valid name (lower-case letters, digits, \`.\`, \`_\`, \`-\` and \`/\`)`;
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
export function checkImageRef(image) {
|
|
395
|
+
const problem = imageRefProblem(image);
|
|
396
|
+
if (problem !== undefined) {
|
|
397
|
+
throw new InvalidArgumentError(`image ${JSON.stringify(image ?? null)} is not an image reference: ${problem}`, "Pass a tag such as python:3.12 or ghcr.io/org/app:v1 (the API pins it to a digest when " +
|
|
398
|
+
"the snapshot is created), or a reference already pinned as NAME@sha256:<64 hex>.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export function imageRefNamesVersion(image) {
|
|
402
|
+
const ref = image.trim();
|
|
403
|
+
if (ref.includes("@"))
|
|
404
|
+
return true;
|
|
405
|
+
const colon = ref.lastIndexOf(":");
|
|
406
|
+
return colon >= 0 && !ref.slice(colon + 1).includes("/");
|
|
407
|
+
}
|
|
408
|
+
export function checkImageVersioned(image) {
|
|
409
|
+
if (imageRefNamesVersion(image))
|
|
410
|
+
return;
|
|
411
|
+
const ref = image.trim();
|
|
412
|
+
throw new InvalidArgumentError(`image ${JSON.stringify(ref)} names no tag or digest, so it would be pinned to whatever ${ref}:latest is today`, `Name the version: a tag (${ref}:<version>), ${ref}:latest if you mean the moving tag, or a digest (${ref}@sha256:<64 hex>).`);
|
|
413
|
+
}
|
|
414
|
+
export const CONTEXT_GIT_RE = /^(?:https?:\/\/|ssh:\/\/|git:\/\/|git@)\S+@[0-9a-f]{40}$/i;
|
|
415
|
+
export function repoUrlCredential(url) {
|
|
416
|
+
const u = url.trim();
|
|
417
|
+
const sep = u.indexOf("://");
|
|
418
|
+
let auth;
|
|
419
|
+
if (sep >= 0) {
|
|
420
|
+
const rest = u.slice(sep + 3);
|
|
421
|
+
const end = rest.search(/[/?#]/);
|
|
422
|
+
auth = end < 0 ? rest : rest.slice(0, end);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const colon = u.indexOf(":");
|
|
426
|
+
if (colon >= 0 && !u.slice(0, colon).includes("/"))
|
|
427
|
+
auth = u.slice(0, colon);
|
|
428
|
+
}
|
|
429
|
+
if (auth !== undefined && (auth.includes("@") || /%40/i.test(auth) || (sep >= 0 && u.includes("@")))) {
|
|
430
|
+
return "userinfo";
|
|
431
|
+
}
|
|
432
|
+
if (u.includes("?"))
|
|
433
|
+
return "query";
|
|
434
|
+
if (u.includes("#"))
|
|
435
|
+
return "fragment";
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
export function withoutPin(s) {
|
|
439
|
+
const at = s.lastIndexOf("@");
|
|
440
|
+
return at > 0 && !s.slice(at + 1).includes("/") ? s.slice(0, at) : s;
|
|
441
|
+
}
|
|
442
|
+
export const REPO_CREDENTIALS_FIX = "Nothing was built or billed. A repository URL never carries a credential. For a private repository, connect its " +
|
|
443
|
+
"host once for your organization (`promigence github connect` or `promigence gitlab connect`) and name it as " +
|
|
444
|
+
"`--repo owner/name` (`--repo gitlab:group/project`); a public one is named by its plain https:// URL, with no " +
|
|
445
|
+
"user name, password, token, query string or fragment in it.";
|
|
446
|
+
export function checkRepoUrl(url, field) {
|
|
447
|
+
if (typeof url === "string" && repoUrlCredential(url) !== undefined) {
|
|
448
|
+
throw new InvalidArgumentError(`credentials in repository URLs are not accepted (${field})`, REPO_CREDENTIALS_FIX, "repo_credentials_not_supported");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
export function checkDockerfileContext(context) {
|
|
452
|
+
const c = typeof context === "string" ? context.trim() : "";
|
|
453
|
+
if (!c.startsWith(CONTEXT_TAR_PREFIX))
|
|
454
|
+
checkRepoUrl(withoutPin(c), "dockerfile.context");
|
|
455
|
+
if (!c) {
|
|
456
|
+
throw new InvalidArgumentError("snapshots.create dockerfile needs a context", "Pass { dockerfile: { context: 'https://github.com/org/app@<40-char sha>', path: 'Dockerfile' } }, " +
|
|
457
|
+
"or pack a directory with packContext('./app').");
|
|
458
|
+
}
|
|
459
|
+
if (c.startsWith(CONTEXT_TAR_PREFIX) || CONTEXT_GIT_RE.test(c))
|
|
460
|
+
return;
|
|
461
|
+
throw new InvalidArgumentError(`dockerfile.context must be a git URL pinned to a full 40-character sha, got ${JSON.stringify(context)}`, "A branch or a tag is not a pin: pass URL@<40-char sha> (git rev-parse HEAD), or pack a local " +
|
|
462
|
+
"directory with packContext('./app').");
|
|
463
|
+
}
|
|
464
|
+
function sourcesBody(sources) {
|
|
465
|
+
if (sources.length > MAX_SOURCES) {
|
|
466
|
+
throw new InvalidArgumentError(`snapshots.create takes at most ${MAX_SOURCES} extra sources, got ${sources.length}`, "An environment assembled from more repositories than that is cheaper to build once as " +
|
|
467
|
+
"an image and snapshot from there.");
|
|
468
|
+
}
|
|
469
|
+
return sources.map((s, i) => {
|
|
470
|
+
checkRepoUrl(s?.repo, `sources[${i}].repo`);
|
|
471
|
+
if (!s?.repo) {
|
|
472
|
+
throw new InvalidArgumentError(`snapshots.create sources[${i}] needs a repo`, "Pass { repo: 'https://github.com/org/lib', commit: <40-char sha> }.");
|
|
473
|
+
}
|
|
474
|
+
if (!/^[0-9a-f]{40}$/i.test(s.commit ?? "")) {
|
|
475
|
+
throw new InvalidArgumentError(`snapshots.create sources[${i}] needs a full 40-character commit sha, got ${JSON.stringify(s.commit ?? null)}`, "A branch or a short sha is not a pin: pass the full sha (git rev-parse HEAD).");
|
|
476
|
+
}
|
|
477
|
+
const out = { repo: s.repo, commit: s.commit.toLowerCase() };
|
|
478
|
+
if (s.path)
|
|
479
|
+
out["path"] = s.path;
|
|
480
|
+
return out;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function verifyBody(v) {
|
|
484
|
+
if (v === undefined || v === null)
|
|
485
|
+
return undefined;
|
|
486
|
+
if (typeof v === "string")
|
|
487
|
+
return v ? { cmd: v, allow_fast: false } : undefined;
|
|
488
|
+
if (!v.cmd)
|
|
489
|
+
return undefined;
|
|
490
|
+
const raw = v;
|
|
491
|
+
return { cmd: v.cmd, allow_fast: Boolean(raw.allowFast ?? raw.allow_fast ?? false) };
|
|
492
|
+
}
|
|
493
|
+
export function snapshotCreateBody(opts) {
|
|
494
|
+
const o = (opts ?? {});
|
|
495
|
+
const forms = [
|
|
496
|
+
["image", o.image !== undefined],
|
|
497
|
+
["dockerfile", o.dockerfile !== undefined],
|
|
498
|
+
["repo", o.repo !== undefined || o.commit !== undefined],
|
|
499
|
+
].filter(([, present]) => present).map(([n]) => n);
|
|
500
|
+
if (forms.length === 0) {
|
|
501
|
+
throw new InvalidArgumentError("snapshots.create needs an image, a dockerfile or a repo", "Pass { image: 'registry/name@sha256:…' }, { dockerfile: { context: …, path: 'Dockerfile' } }, " +
|
|
502
|
+
"or { repo: 'https://github.com/org/app', commit: <40-char sha> }.");
|
|
503
|
+
}
|
|
504
|
+
if (forms.length > 1) {
|
|
505
|
+
throw new InvalidArgumentError(`snapshots.create takes one input form, got ${forms.join(" + ")}`, "Pass exactly one of image, dockerfile, or repo/commit.");
|
|
506
|
+
}
|
|
507
|
+
const verify = verifyBody(o.verify);
|
|
508
|
+
let body;
|
|
509
|
+
const github = typeof o.repo === "string"
|
|
510
|
+
&& (o.repo.trim().startsWith(GITHUB_REPO_PREFIX) || o.repo.trim().startsWith(GITLAB_REPO_PREFIX));
|
|
511
|
+
if (o.env !== undefined && Object.keys(o.env).length && forms[0] === "repo") {
|
|
512
|
+
throw new InvalidArgumentError("snapshots.create env is for an image or a Dockerfile", "A repository snapshot sets its environment in its setup steps, or pass env on each command.");
|
|
513
|
+
}
|
|
514
|
+
if ((o.user !== undefined || o.setupUser !== undefined) && forms[0] === "repo") {
|
|
515
|
+
throw new InvalidArgumentError("snapshots.create user and setupUser are for an image or a Dockerfile", "A repository snapshot runs as root; name a user on each command instead ({ user: 'app' }).");
|
|
516
|
+
}
|
|
517
|
+
if (o.setupUser !== undefined && o.user === undefined) {
|
|
518
|
+
throw new InvalidArgumentError("snapshots.create setupUser needs user", "Pass { user: 'app', setupUser: 'app' }: start, setup and verify as app too (without setupUser they run as root), every later command as app.");
|
|
519
|
+
}
|
|
520
|
+
if (!github && (o.ref !== undefined || o.from !== undefined || o.track)) {
|
|
521
|
+
throw new InvalidArgumentError("snapshots.create ref, from and track are for a connected repository (repo: 'github:owner/name' or 'gitlab:group/project')", "Pass { repo: 'github:your-org/app', ref: 'main' }, or drop them for this form.");
|
|
522
|
+
}
|
|
523
|
+
if (github) {
|
|
524
|
+
body = githubCreateBody({
|
|
525
|
+
repo: o.repo, commit: o.commit, ref: o.ref, from: o.from, track: o.track,
|
|
526
|
+
setup: o.setup, verify, tier: o.tier, alias: o.alias,
|
|
527
|
+
others: [
|
|
528
|
+
["sources", Boolean(o.sources?.length)], ["start", o.start !== undefined],
|
|
529
|
+
["registrySecret", Boolean(o.registrySecret)],
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
else if (forms[0] === "repo") {
|
|
534
|
+
if (!o.repo)
|
|
535
|
+
throw new InvalidArgumentError("snapshots.create needs a repo", "Pass { repo: 'https://github.com/org/app' }.");
|
|
536
|
+
checkRepoUrl(o.repo, "repo");
|
|
537
|
+
if (!/^[0-9a-f]{40}$/i.test(o.commit ?? "")) {
|
|
538
|
+
throw new InvalidArgumentError(`snapshots.create needs a full 40-character commit sha, got ${JSON.stringify(o.commit ?? null)}`, "A branch or a short sha is not a pin: pass the full sha (git rev-parse HEAD).");
|
|
539
|
+
}
|
|
540
|
+
if (!verify) {
|
|
541
|
+
throw new InvalidArgumentError("snapshots.create needs a verify command", "Pass { verify: { cmd: 'bun typecheck' } } — a snapshot with nothing to verify is not verified.");
|
|
542
|
+
}
|
|
543
|
+
body = { repo: o.repo, commit: o.commit.toLowerCase(), setup: o.setup ?? [], verify };
|
|
544
|
+
if (o.sources?.length)
|
|
545
|
+
body["sources"] = sourcesBody(o.sources);
|
|
546
|
+
if (o.tier !== undefined)
|
|
547
|
+
body["tier"] = o.tier;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
if (o.sources?.length) {
|
|
551
|
+
throw new InvalidArgumentError("snapshots.create sources is part of the repo form", "An image already carries its tree and may not have git in it: clone the extra " +
|
|
552
|
+
"repository in a setup step, or pass { repo, commit, sources }.");
|
|
553
|
+
}
|
|
554
|
+
body = {};
|
|
555
|
+
if (forms[0] === "image") {
|
|
556
|
+
checkImageRef(o.image);
|
|
557
|
+
body["image"] = o.image;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const ctx = o.dockerfile?.context;
|
|
561
|
+
checkDockerfileContext(ctx);
|
|
562
|
+
body["dockerfile"] = { context: ctx, ...(o.dockerfile.path ? { path: o.dockerfile.path } : {}) };
|
|
563
|
+
}
|
|
564
|
+
if (o.setup?.length)
|
|
565
|
+
body["setup"] = [...o.setup];
|
|
566
|
+
if (verify)
|
|
567
|
+
body["verify"] = verify;
|
|
568
|
+
if (o.start !== undefined && o.start !== "" && !(Array.isArray(o.start) && o.start.length === 0)) {
|
|
569
|
+
body["start"] = typeof o.start === "string" ? o.start : [...o.start];
|
|
570
|
+
}
|
|
571
|
+
if (o.tier !== undefined)
|
|
572
|
+
body["tier"] = o.tier;
|
|
573
|
+
if (o.registrySecret)
|
|
574
|
+
body["registry_secret"] = o.registrySecret;
|
|
575
|
+
if (o.env !== undefined && Object.keys(o.env).length)
|
|
576
|
+
body["env"] = { ...o.env };
|
|
577
|
+
if (o.user !== undefined)
|
|
578
|
+
body["user"] = o.user;
|
|
579
|
+
if (o.setupUser !== undefined)
|
|
580
|
+
body["setup_user"] = o.setupUser;
|
|
581
|
+
}
|
|
582
|
+
if (o.alias)
|
|
583
|
+
body["alias"] = o.alias;
|
|
584
|
+
if (o.grade) {
|
|
585
|
+
if (!o.grade.cmd)
|
|
586
|
+
throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
587
|
+
const g = { cmd: o.grade.cmd };
|
|
588
|
+
if (o.grade.holdoutCmd)
|
|
589
|
+
g["holdout_cmd"] = o.grade.holdoutCmd;
|
|
590
|
+
if (o.grade.timeoutMs !== undefined)
|
|
591
|
+
g["timeout_ms"] = o.grade.timeoutMs;
|
|
592
|
+
if (o.grade.score !== undefined)
|
|
593
|
+
g["score"] = o.grade.score;
|
|
594
|
+
body["grade"] = g;
|
|
595
|
+
}
|
|
596
|
+
if (o.gradeOutput !== undefined) {
|
|
597
|
+
if (o.gradeOutput && !o.grade) {
|
|
598
|
+
throw new InvalidArgumentError("snapshots.create gradeOutput needs a grade", "Pass { grade: { cmd: 'pytest -q tests/' }, gradeOutput: true }.");
|
|
599
|
+
}
|
|
600
|
+
body["grade_output"] = o.gradeOutput;
|
|
601
|
+
}
|
|
602
|
+
return body;
|
|
603
|
+
}
|
|
604
|
+
const ENV_CODES = new Set([
|
|
605
|
+
"fork_failed", "start_timeout", "readiness_failed", "verify_too_slow", "verify_each_failed",
|
|
606
|
+
"toolchain_mismatch", "sandbox_unresponsive", "oom_within_tier", "disk_full_within_tier",
|
|
607
|
+
"sandbox_interrupted", "integrity_mismatch", "egress_unavailable", "service_timeout",
|
|
608
|
+
"resume_unavailable", "sandbox_unreachable", "capacity_busy", "region_at_capacity",
|
|
609
|
+
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "snapshot_unavailable",
|
|
610
|
+
"capacity_limit", "registry_unavailable", "network_policy_unsupported", "secrets_unavailable",
|
|
611
|
+
"secret_binding_unsupported", "idempotency_key_in_use",
|
|
612
|
+
]);
|
|
613
|
+
const REFUSED_503_CODES = new Set([
|
|
614
|
+
"resume_unavailable", "sandbox_unreachable", "capacity_busy", "region_at_capacity",
|
|
615
|
+
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
616
|
+
"secrets_unavailable",
|
|
617
|
+
]);
|
|
618
|
+
function withoutCliHelp(fix) {
|
|
619
|
+
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
620
|
+
if (out === fix.trim())
|
|
621
|
+
return fix;
|
|
622
|
+
return out === "" ? "Change what the message names, then call again." : out;
|
|
623
|
+
}
|
|
624
|
+
export function errorFor(code, message, fix, extra = {}) {
|
|
625
|
+
const attach = (e) => {
|
|
626
|
+
const w = e;
|
|
627
|
+
w.docs = extra.docs ?? `${ERRORS_DOCS_URL}#${code}`;
|
|
628
|
+
if (extra.status !== undefined)
|
|
629
|
+
w.status = extra.status;
|
|
630
|
+
if (extra.requestId !== undefined)
|
|
631
|
+
w.requestId = extra.requestId;
|
|
632
|
+
if (extra.retryAfterMs !== undefined)
|
|
633
|
+
w.retryAfterMs = extra.retryAfterMs;
|
|
634
|
+
if (extra.category !== undefined)
|
|
635
|
+
w.category = extra.category;
|
|
636
|
+
if (extra.retryable !== undefined)
|
|
637
|
+
w.retryable = extra.retryable;
|
|
638
|
+
return e;
|
|
639
|
+
};
|
|
640
|
+
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && previewsOff(message)) {
|
|
641
|
+
return attach(new PreviewsUnavailableError());
|
|
642
|
+
}
|
|
643
|
+
switch (code) {
|
|
644
|
+
case "auth": return attach(new AuthError(message, fix));
|
|
645
|
+
case "not_found": return attach(new NotFoundError(message, fix));
|
|
646
|
+
case "unreachable": return attach(new UnreachableError(message, fix));
|
|
647
|
+
case "rate_limited": return attach(new RateLimitError(message, extra.retryAfterMs));
|
|
648
|
+
case "cap_exceeded":
|
|
649
|
+
case "cap_killed":
|
|
650
|
+
case "cap_paused":
|
|
651
|
+
case "credits_paused":
|
|
652
|
+
return attach(new SpendCapError(message, fix, code));
|
|
653
|
+
case "not_verified": return attach(new NotVerifiedError(message, fix));
|
|
654
|
+
case "timeout": return attach(new TimeoutError(message, fix));
|
|
655
|
+
case "usage":
|
|
656
|
+
case "invalid_argument":
|
|
657
|
+
return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
658
|
+
case "not_released": return attach(new NotReleasedError(message));
|
|
659
|
+
case "service_restart": return attach(new ServiceRestartError(message, fix));
|
|
660
|
+
default:
|
|
661
|
+
if (ENV_CODES.has(code) || extra.category === "env")
|
|
662
|
+
return attach(new EnvFailedError(code, message, fix));
|
|
663
|
+
return attach(new ApiError({
|
|
664
|
+
code,
|
|
665
|
+
message,
|
|
666
|
+
fix,
|
|
667
|
+
exitCode: exitForCode(code),
|
|
668
|
+
...(extra.docs === undefined ? {} : { docs: extra.docs }),
|
|
669
|
+
...(extra.status === undefined ? {} : { status: extra.status }),
|
|
670
|
+
...(extra.requestId === undefined ? {} : { requestId: extra.requestId }),
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const KEYED_PATTERNS = KEYED_CREATES.map(([m, p]) => [m, p.split("/").filter(Boolean)]);
|
|
675
|
+
const KEYED_METHODS = new Set(KEYED_CREATES.map(([m]) => m));
|
|
676
|
+
export function isKeyedCreate(method, path) {
|
|
677
|
+
if (!KEYED_METHODS.has(method))
|
|
678
|
+
return false;
|
|
679
|
+
const segs = (path.split("?")[0] ?? "").split("/").filter(Boolean);
|
|
680
|
+
for (const [m, pat] of KEYED_PATTERNS) {
|
|
681
|
+
if (m !== method || pat.length !== segs.length)
|
|
682
|
+
continue;
|
|
683
|
+
let all = true;
|
|
684
|
+
for (let i = 0; i < pat.length; i++) {
|
|
685
|
+
const s = pat[i];
|
|
686
|
+
if (s.startsWith("{") ? (segs[i] ?? "").length === 0 : s !== segs[i]) {
|
|
687
|
+
all = false;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (all)
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
function commandTimeoutMs(v) {
|
|
697
|
+
if (v === undefined)
|
|
698
|
+
return undefined;
|
|
699
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
700
|
+
throw new InvalidArgumentError(`timeoutMs must be a positive whole number of milliseconds, got ${String(v)}`, "Omit it for the default (15 minutes in the foreground, the sandbox's own timeout in the background), or pass a bound such as 600_000.");
|
|
701
|
+
}
|
|
702
|
+
return v;
|
|
703
|
+
}
|
|
704
|
+
export function checkSandboxId(sandboxId, call) {
|
|
705
|
+
if (typeof sandboxId === "string" && sandboxId.trim() !== "")
|
|
706
|
+
return;
|
|
707
|
+
throw new InvalidArgumentError(`${call} needs a sandbox id, got ${JSON.stringify(sandboxId ?? null)}`, "Pass the id a create returned (sandbox.sandboxId, `sbx_…`). An unset environment variable is the usual way to pass an empty one.");
|
|
708
|
+
}
|
|
709
|
+
export function checkSpendCap(spendCap) {
|
|
710
|
+
if (typeof spendCap === "number" && Number.isFinite(spendCap) && spendCap >= 0)
|
|
711
|
+
return spendCap;
|
|
712
|
+
throw new InvalidArgumentError(`spend cap must be a number of US dollars, 0 or more, got ${String(spendCap)}`, "Pass spendCap: 1 (or PROMIGENCE_SPEND_CAP=1): the most this run may spend.");
|
|
713
|
+
}
|
|
714
|
+
export function newIdempotencyKey() {
|
|
715
|
+
return `idk_${globalThis.crypto.randomUUID()}`;
|
|
716
|
+
}
|
|
717
|
+
function parseRetryAfter(header) {
|
|
718
|
+
if (!header)
|
|
719
|
+
return undefined;
|
|
720
|
+
const secs = Number(header.trim());
|
|
721
|
+
if (Number.isFinite(secs))
|
|
722
|
+
return Math.max(0, secs * 1000);
|
|
723
|
+
const at = Date.parse(header);
|
|
724
|
+
if (!Number.isNaN(at))
|
|
725
|
+
return Math.max(0, at - Date.now());
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
const MAX_RETRY_SLEEP_MS = 10_000;
|
|
729
|
+
const STREAM_IDLE_MS = 90_000;
|
|
730
|
+
const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
|
|
731
|
+
const POLLING_TICK_MS = 5;
|
|
732
|
+
const PARKED_TICK_MS = 50;
|
|
733
|
+
async function keepingLoopAwake(everyMs, fn) {
|
|
734
|
+
let tick;
|
|
735
|
+
try {
|
|
736
|
+
tick = setInterval(() => { }, everyMs);
|
|
737
|
+
tick.unref?.();
|
|
738
|
+
return await fn();
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
if (tick !== undefined)
|
|
742
|
+
clearInterval(tick);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const WAIT_MS_CAP = 30_000;
|
|
746
|
+
export const LONG_POLL_MS = 5_000;
|
|
747
|
+
const BUILD_POLL_MS = 10_000;
|
|
748
|
+
const PROGRESS_POLL_MS = 200;
|
|
749
|
+
const WAITED_HEADER = "promigence-waited-ms";
|
|
750
|
+
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
751
|
+
function waitQuery(waitMs) {
|
|
752
|
+
return waitMs !== undefined && waitMs > 0
|
|
753
|
+
? `?wait_ms=${Math.min(Math.floor(waitMs), WAIT_MS_CAP)}`
|
|
754
|
+
: "";
|
|
755
|
+
}
|
|
756
|
+
const notReleased = (what) => Promise.reject(new NotReleasedError(what));
|
|
757
|
+
let defaultUserAgent;
|
|
758
|
+
const APP_SPAN = /^\s*app\s*;(?:.*;)?\s*dur=([0-9]+(?:\.[0-9]+)?)/;
|
|
759
|
+
export function serverAppMs(header) {
|
|
760
|
+
if (!header)
|
|
761
|
+
return null;
|
|
762
|
+
if (!header.includes(",")) {
|
|
763
|
+
const m = APP_SPAN.exec(header);
|
|
764
|
+
return m ? Number(m[1]) : null;
|
|
765
|
+
}
|
|
766
|
+
for (const part of header.split(",")) {
|
|
767
|
+
const m = APP_SPAN.exec(part);
|
|
768
|
+
if (m)
|
|
769
|
+
return Number(m[1]);
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
function headerOf(res, name) {
|
|
774
|
+
return res instanceof H2Response ? res.header(name) : res.headers.get(name);
|
|
775
|
+
}
|
|
776
|
+
export class PromigenceClient {
|
|
777
|
+
apiUrl;
|
|
778
|
+
#apiKey;
|
|
779
|
+
#keyRefusal;
|
|
780
|
+
#bearer;
|
|
781
|
+
fetchImpl;
|
|
782
|
+
ownFetch;
|
|
783
|
+
h2;
|
|
784
|
+
requestTimeoutMs;
|
|
785
|
+
maxRetries;
|
|
786
|
+
retryBaseMs;
|
|
787
|
+
userAgent;
|
|
788
|
+
cliErrors;
|
|
789
|
+
caller;
|
|
790
|
+
stats = { requests: 0, retries: 0 };
|
|
791
|
+
lastTiming = null;
|
|
792
|
+
notice;
|
|
793
|
+
onNotice;
|
|
794
|
+
onTiming;
|
|
795
|
+
noticesSeen = new Set();
|
|
796
|
+
constructor(opts = {}) {
|
|
797
|
+
traceEvent("client:ctor");
|
|
798
|
+
const resolved = resolveCredentials(opts);
|
|
799
|
+
this.#apiKey = resolved.apiKey;
|
|
800
|
+
this.#keyRefusal = resolved.refusal;
|
|
801
|
+
this.apiUrl = resolved.apiUrl;
|
|
802
|
+
const env = opts.fetch === undefined ? transportEnv() : undefined;
|
|
803
|
+
const proxy = env !== undefined ? proxyFor(this.apiUrl, env) : undefined;
|
|
804
|
+
this.fetchImpl = opts.fetch ?? (proxy ? proxyFetch(proxy) : globalThis.fetch);
|
|
805
|
+
this.ownFetch = opts.fetch === undefined && proxy === undefined;
|
|
806
|
+
this.h2 = env !== undefined && this.apiUrl.startsWith("https:") && http2Allowed(opts.http2, env)
|
|
807
|
+
? new Http2Transport(this.fetchImpl)
|
|
808
|
+
: undefined;
|
|
809
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 30_000;
|
|
810
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
811
|
+
this.retryBaseMs = opts.retryBaseMs ?? 250;
|
|
812
|
+
this.caller = resolveCaller(opts.caller);
|
|
813
|
+
this.userAgent = userAgentWithCaller(opts.userAgent ?? (defaultUserAgent ??= `promigence/${VERSION} node/${process.versions.node}`), this.caller);
|
|
814
|
+
this.onNotice = opts.onNotice;
|
|
815
|
+
this.onTiming = opts.onTiming;
|
|
816
|
+
this.cliErrors = opts.cliErrors === true;
|
|
817
|
+
traceEvent("client:ready");
|
|
818
|
+
}
|
|
819
|
+
observeNotice(raw) {
|
|
820
|
+
if (!raw)
|
|
821
|
+
return;
|
|
822
|
+
let n;
|
|
823
|
+
try {
|
|
824
|
+
n = JSON.parse(raw);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (!n || typeof n.code !== "string")
|
|
830
|
+
return;
|
|
831
|
+
this.notice = n;
|
|
832
|
+
const key = `${n.code}|${n.effective_at ?? ""}`;
|
|
833
|
+
if (this.noticesSeen.has(key))
|
|
834
|
+
return;
|
|
835
|
+
this.noticesSeen.add(key);
|
|
836
|
+
if (this.onNotice)
|
|
837
|
+
this.onNotice(n);
|
|
838
|
+
else
|
|
839
|
+
process.emitWarning(`${n.message ?? n.code} ${n.fix ?? ""}`.trim(), { code: "PROMIGENCE_NOTICE" });
|
|
840
|
+
}
|
|
841
|
+
get apiKey() {
|
|
842
|
+
return this.#apiKey;
|
|
843
|
+
}
|
|
844
|
+
get authenticated() {
|
|
845
|
+
return typeof this.#apiKey === "string" && this.#apiKey.length > 0;
|
|
846
|
+
}
|
|
847
|
+
toJSON() {
|
|
848
|
+
return { apiUrl: this.apiUrl, authenticated: this.authenticated };
|
|
849
|
+
}
|
|
850
|
+
[inspect.custom](_depth, opts) {
|
|
851
|
+
return `PromigenceClient ${inspect(this.toJSON(), opts)}`;
|
|
852
|
+
}
|
|
853
|
+
get credentialsProblem() {
|
|
854
|
+
return this.#keyRefusal;
|
|
855
|
+
}
|
|
856
|
+
preconnect() {
|
|
857
|
+
try {
|
|
858
|
+
if (this.h2 === undefined)
|
|
859
|
+
return;
|
|
860
|
+
warmTls();
|
|
861
|
+
this.h2.preconnect(this.apiUrl);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
requireKey() {
|
|
867
|
+
if (this.#keyRefusal)
|
|
868
|
+
throw this.#keyRefusal;
|
|
869
|
+
if (!this.#apiKey)
|
|
870
|
+
throw new AuthError();
|
|
871
|
+
return this.#apiKey;
|
|
872
|
+
}
|
|
873
|
+
bearer() {
|
|
874
|
+
const key = this.requireKey();
|
|
875
|
+
return (this.#bearer ??= `Bearer ${key}`);
|
|
876
|
+
}
|
|
877
|
+
refusalBackoff(status, retryAfterMs, code, attempt, replayable, signal) {
|
|
878
|
+
if (attempt >= this.maxRetries || signal?.aborted)
|
|
879
|
+
return undefined;
|
|
880
|
+
const gateway = (status === 502 || status === 504) && replayable && code === undefined;
|
|
881
|
+
if (status !== 429 && status !== 503 && !gateway)
|
|
882
|
+
return undefined;
|
|
883
|
+
if ((retryAfterMs ?? 0) > MAX_RETRY_SLEEP_MS)
|
|
884
|
+
return undefined;
|
|
885
|
+
if (status === 503 && !replayable && retryAfterMs === undefined && !REFUSED_503_CODES.has(code ?? ""))
|
|
886
|
+
return undefined;
|
|
887
|
+
const jitter = Math.random();
|
|
888
|
+
return retryAfterMs !== undefined
|
|
889
|
+
? retryAfterMs + jitter * Math.min(250, this.retryBaseMs)
|
|
890
|
+
: Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt) * (0.5 + jitter * 0.5);
|
|
891
|
+
}
|
|
892
|
+
readExtras(opts) {
|
|
893
|
+
const waitMs = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(opts.waitMs, WAIT_MS_CAP) : 0;
|
|
894
|
+
return {
|
|
895
|
+
...(waitMs > 0 ? { timeoutMs: waitMs + this.requestTimeoutMs } : {}),
|
|
896
|
+
...(opts.onHeaders ? { onHeaders: opts.onHeaders } : {}),
|
|
897
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
guardDispatcher() {
|
|
901
|
+
if (!this.ownFetch || customDispatcher())
|
|
902
|
+
return undefined;
|
|
903
|
+
return guardedFetchDispatcher();
|
|
904
|
+
}
|
|
905
|
+
dispatcherFor(timeoutMs) {
|
|
906
|
+
const guarded = this.guardDispatcher();
|
|
907
|
+
if (guarded !== undefined)
|
|
908
|
+
return longRequestDispatcher(timeoutMs, guarded) ?? guarded;
|
|
909
|
+
return longRequestDispatcher(timeoutMs);
|
|
910
|
+
}
|
|
911
|
+
async request(opts) {
|
|
912
|
+
const tr = traceBegin(opts.method, opts.path);
|
|
913
|
+
const url = `${this.apiUrl}${opts.path}`;
|
|
914
|
+
const headers = {
|
|
915
|
+
accept: "application/json",
|
|
916
|
+
"user-agent": this.userAgent,
|
|
917
|
+
};
|
|
918
|
+
const extra = opts.headers;
|
|
919
|
+
if (extra !== undefined && extra !== null) {
|
|
920
|
+
for (const k of Object.keys(extra)) {
|
|
921
|
+
const name = k.toLowerCase();
|
|
922
|
+
if (name !== "authorization")
|
|
923
|
+
headers[name] = extra[k];
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (this.caller)
|
|
927
|
+
headers[CALLER_HEADER] = this.caller;
|
|
928
|
+
if (!opts.anonymous)
|
|
929
|
+
headers["authorization"] = this.bearer();
|
|
930
|
+
if (opts.body !== undefined)
|
|
931
|
+
headers["content-type"] = "application/json";
|
|
932
|
+
const keyedCreate = isKeyedCreate(opts.method, opts.path);
|
|
933
|
+
const idempotencyKey = opts.idempotencyKey ?? (keyedCreate ? newIdempotencyKey() : undefined);
|
|
934
|
+
if (idempotencyKey)
|
|
935
|
+
headers["idempotency-key"] = idempotencyKey;
|
|
936
|
+
const replayable = opts.method === "GET" || opts.method === "DELETE" || (keyedCreate && Boolean(idempotencyKey));
|
|
937
|
+
const again = async (attempt) => {
|
|
938
|
+
if (!keyedCreate || !idempotencyKey || attempt >= this.maxRetries || opts.signal?.aborted)
|
|
939
|
+
return false;
|
|
940
|
+
this.stats.retries++;
|
|
941
|
+
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
942
|
+
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
943
|
+
return true;
|
|
944
|
+
};
|
|
945
|
+
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
946
|
+
const dispatcher = this.dispatcherFor(timeoutMs);
|
|
947
|
+
let resentDelete = false;
|
|
948
|
+
for (let attempt = 0;; attempt++) {
|
|
949
|
+
let res;
|
|
950
|
+
this.stats.requests++;
|
|
951
|
+
const sentAt = performance.now();
|
|
952
|
+
try {
|
|
953
|
+
if (this.h2) {
|
|
954
|
+
const init = { method: opts.method, headers, timeoutMs };
|
|
955
|
+
if (opts.signal !== undefined)
|
|
956
|
+
init.signal = opts.signal;
|
|
957
|
+
if (opts.body !== undefined)
|
|
958
|
+
init.body = JSON.stringify(opts.body);
|
|
959
|
+
if (dispatcher !== undefined)
|
|
960
|
+
init.dispatcher = dispatcher;
|
|
961
|
+
if (tr !== undefined) {
|
|
962
|
+
init.trace = tr;
|
|
963
|
+
mark(tr, "init");
|
|
964
|
+
}
|
|
965
|
+
res = await this.h2.fetch(url, init, replayable);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
const signals = [AbortSignal.timeout(timeoutMs)];
|
|
969
|
+
if (opts.signal)
|
|
970
|
+
signals.push(opts.signal);
|
|
971
|
+
const init = {
|
|
972
|
+
method: opts.method,
|
|
973
|
+
headers,
|
|
974
|
+
signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals),
|
|
975
|
+
...(opts.body === undefined ? {} : { body: JSON.stringify(opts.body) }),
|
|
976
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
977
|
+
};
|
|
978
|
+
if (tr !== undefined)
|
|
979
|
+
mark(tr, "init");
|
|
980
|
+
res = await this.fetchImpl(url, init);
|
|
981
|
+
}
|
|
982
|
+
if (tr !== undefined)
|
|
983
|
+
mark(tr, "res");
|
|
984
|
+
this.lastTiming = {
|
|
985
|
+
method: opts.method,
|
|
986
|
+
path: opts.path,
|
|
987
|
+
totalMs: performance.now() - sentAt,
|
|
988
|
+
ourMs: serverAppMs(headerOf(res, "server-timing")),
|
|
989
|
+
};
|
|
990
|
+
this.onTiming?.(this.lastTiming);
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
const e = err;
|
|
994
|
+
if (await again(attempt))
|
|
995
|
+
continue;
|
|
996
|
+
const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
|
|
997
|
+
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
|
|
998
|
+
if (!timedOut && replayable && attempt < this.maxRetries && !opts.signal?.aborted) {
|
|
999
|
+
this.stats.retries++;
|
|
1000
|
+
if (opts.method === "DELETE")
|
|
1001
|
+
resentDelete = true;
|
|
1002
|
+
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1003
|
+
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (opts.signal?.aborted) {
|
|
1007
|
+
throw new TimeoutError(`request ${opts.method} ${opts.path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
|
|
1008
|
+
}
|
|
1009
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
1010
|
+
throw new TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
|
|
1011
|
+
"PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK. A --wait is bounded " +
|
|
1012
|
+
"by --wait-timeout and survives a slow request on its own.");
|
|
1013
|
+
}
|
|
1014
|
+
const code = e?.cause?.code;
|
|
1015
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
|
|
1016
|
+
throw new TimeoutError(`request ${opts.method} ${opts.path} was cut off by the HTTP client (${code}) inside its ${timeoutMs} ms bound`, "Raise headersTimeout and bodyTimeout on the undici dispatcher this process installed " +
|
|
1017
|
+
"to at least the request's bound, or retry.");
|
|
1018
|
+
}
|
|
1019
|
+
throw new UnreachableError(`cannot reach ${url}: ${transportDetail(e)}`);
|
|
1020
|
+
}
|
|
1021
|
+
const retryAfterMs = parseRetryAfter(headerOf(res, "retry-after"));
|
|
1022
|
+
const gateway = (res.status === 502 || res.status === 504) && replayable;
|
|
1023
|
+
let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
|
|
1024
|
+
let body;
|
|
1025
|
+
if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
|
|
1026
|
+
body = await res.text().catch(() => "");
|
|
1027
|
+
retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
|
|
1028
|
+
}
|
|
1029
|
+
if (retryable && gateway) {
|
|
1030
|
+
body = await res.text().catch(() => "");
|
|
1031
|
+
retryable = errorCodeOf(body) === undefined;
|
|
1032
|
+
}
|
|
1033
|
+
if (retryable && attempt < this.maxRetries) {
|
|
1034
|
+
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1035
|
+
const jitter = Math.random();
|
|
1036
|
+
const waitMs = retryAfterMs !== undefined
|
|
1037
|
+
? retryAfterMs + jitter * Math.min(250, this.retryBaseMs)
|
|
1038
|
+
: backoff * (0.5 + jitter * 0.5);
|
|
1039
|
+
this.stats.retries++;
|
|
1040
|
+
if (gateway && opts.method === "DELETE")
|
|
1041
|
+
resentDelete = true;
|
|
1042
|
+
if (body === undefined)
|
|
1043
|
+
try {
|
|
1044
|
+
await res.text();
|
|
1045
|
+
}
|
|
1046
|
+
catch { }
|
|
1047
|
+
await sleep(waitMs);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (tr !== undefined)
|
|
1051
|
+
mark(tr, "hdrs");
|
|
1052
|
+
let text;
|
|
1053
|
+
if (body !== undefined)
|
|
1054
|
+
text = body;
|
|
1055
|
+
else if (res.status === 204)
|
|
1056
|
+
text = "";
|
|
1057
|
+
else {
|
|
1058
|
+
try {
|
|
1059
|
+
text = res instanceof H2Response ? res.textNow() : await res.text();
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
if (!res.ok)
|
|
1063
|
+
text = "";
|
|
1064
|
+
else {
|
|
1065
|
+
const e = err;
|
|
1066
|
+
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError";
|
|
1067
|
+
if (!timedOut && replayable && attempt < this.maxRetries && !opts.signal?.aborted) {
|
|
1068
|
+
this.stats.retries++;
|
|
1069
|
+
if (opts.method === "DELETE")
|
|
1070
|
+
resentDelete = true;
|
|
1071
|
+
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1072
|
+
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (timedOut) {
|
|
1076
|
+
throw new TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms while its answer was arriving`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
|
|
1077
|
+
"PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK.");
|
|
1078
|
+
}
|
|
1079
|
+
throw new UnreachableError(`the connection to ${url} was lost while the answer to ${opts.method} ${opts.path} was arriving: ${transportDetail(e)}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (tr !== undefined)
|
|
1084
|
+
mark(tr, "txt");
|
|
1085
|
+
let payload = {};
|
|
1086
|
+
if (!res.ok) {
|
|
1087
|
+
try {
|
|
1088
|
+
payload = text ? JSON.parse(text) : {};
|
|
1089
|
+
}
|
|
1090
|
+
catch { }
|
|
1091
|
+
}
|
|
1092
|
+
if (res.status === 409 && keyedCreate && payload.error?.code === "idempotency_key_in_use" &&
|
|
1093
|
+
attempt < this.maxRetries && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS) {
|
|
1094
|
+
this.stats.retries++;
|
|
1095
|
+
await sleep((retryAfterMs ?? this.retryBaseMs) + Math.random() * Math.min(250, this.retryBaseMs));
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
opts.onHeaders?.(res.headers);
|
|
1099
|
+
this.observeNotice(headerOf(res, NOTICE_HEADER));
|
|
1100
|
+
if (res.status === 404 && opts.method === "DELETE" &&
|
|
1101
|
+
(resentDelete || (res instanceof H2Response && res.replayed))) {
|
|
1102
|
+
if (tr !== undefined)
|
|
1103
|
+
mark(tr, "ret");
|
|
1104
|
+
return { ok: true, deleted: true, already_gone: true };
|
|
1105
|
+
}
|
|
1106
|
+
if (!res.ok) {
|
|
1107
|
+
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1108
|
+
const message = payload.error?.message ?? `HTTP ${res.status} from ${opts.method} ${opts.path}`;
|
|
1109
|
+
const fix = payload.error?.fix ?? (code === "internal"
|
|
1110
|
+
? "Not your fault. Re-run; if it repeats, include the request_id when reporting."
|
|
1111
|
+
: "See the API response for details.");
|
|
1112
|
+
const extra = { status: res.status, ...bodyExtra(payload), cli: this.cliErrors };
|
|
1113
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1114
|
+
if (rid)
|
|
1115
|
+
extra.requestId = rid;
|
|
1116
|
+
if (retryAfterMs !== undefined)
|
|
1117
|
+
extra.retryAfterMs = retryAfterMs;
|
|
1118
|
+
throw errorFor(code, message, fix, extra);
|
|
1119
|
+
}
|
|
1120
|
+
if (!text) {
|
|
1121
|
+
if (tr !== undefined)
|
|
1122
|
+
mark(tr, "ret");
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const parsed = JSON.parse(text);
|
|
1127
|
+
if (tr !== undefined)
|
|
1128
|
+
mark(tr, "ret");
|
|
1129
|
+
return parsed;
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
throw new ApiError({
|
|
1133
|
+
code: "internal",
|
|
1134
|
+
message: `${opts.method} ${opts.path} returned non-JSON (${res.status})`,
|
|
1135
|
+
fix: "The API must answer JSON; check PROMIGENCE_API_URL points at /v1.",
|
|
1136
|
+
exitCode: 1,
|
|
1137
|
+
status: res.status,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async bytes(method, path, body, opts = {}) {
|
|
1143
|
+
const url = `${this.apiUrl}${path}`;
|
|
1144
|
+
const headers = {
|
|
1145
|
+
accept: "application/octet-stream, application/json",
|
|
1146
|
+
"user-agent": this.userAgent,
|
|
1147
|
+
authorization: `Bearer ${this.requireKey()}`,
|
|
1148
|
+
};
|
|
1149
|
+
if (this.caller)
|
|
1150
|
+
headers[CALLER_HEADER] = this.caller;
|
|
1151
|
+
if (body !== undefined)
|
|
1152
|
+
headers["content-type"] = "application/octet-stream";
|
|
1153
|
+
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1154
|
+
if (opts.signal)
|
|
1155
|
+
signals.push(opts.signal);
|
|
1156
|
+
const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1157
|
+
const dispatcher = this.guardDispatcher();
|
|
1158
|
+
for (let attempt = 0;; attempt++) {
|
|
1159
|
+
let res;
|
|
1160
|
+
this.stats.requests++;
|
|
1161
|
+
try {
|
|
1162
|
+
res = await this.fetchImpl(url, {
|
|
1163
|
+
method,
|
|
1164
|
+
headers,
|
|
1165
|
+
signal,
|
|
1166
|
+
...(body === undefined ? {} : { body: body }),
|
|
1167
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
const e = err;
|
|
1172
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
1173
|
+
throw new TimeoutError(`${method} ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs, or fetch it in ranges.");
|
|
1174
|
+
}
|
|
1175
|
+
throw new UnreachableError(`cannot reach ${url}: ${transportDetail(e)}`);
|
|
1176
|
+
}
|
|
1177
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
1178
|
+
if (res.ok)
|
|
1179
|
+
return buf;
|
|
1180
|
+
let payload = {};
|
|
1181
|
+
try {
|
|
1182
|
+
payload = JSON.parse(new TextDecoder().decode(buf));
|
|
1183
|
+
}
|
|
1184
|
+
catch { }
|
|
1185
|
+
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
|
|
1186
|
+
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, method === "GET", opts.signal);
|
|
1187
|
+
if (wait !== undefined) {
|
|
1188
|
+
this.stats.retries++;
|
|
1189
|
+
await sleep(wait);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1193
|
+
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1194
|
+
status: res.status,
|
|
1195
|
+
...bodyExtra(payload),
|
|
1196
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async stream(path, onChunk, opts = {}) {
|
|
1201
|
+
const url = `${this.apiUrl}${path}`;
|
|
1202
|
+
const headers = {
|
|
1203
|
+
accept: "application/octet-stream, application/json",
|
|
1204
|
+
"user-agent": this.userAgent,
|
|
1205
|
+
authorization: `Bearer ${this.requireKey()}`,
|
|
1206
|
+
};
|
|
1207
|
+
if (this.caller)
|
|
1208
|
+
headers[CALLER_HEADER] = this.caller;
|
|
1209
|
+
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1210
|
+
if (opts.signal)
|
|
1211
|
+
signals.push(opts.signal);
|
|
1212
|
+
const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1213
|
+
const timedOut = () => new TimeoutError(`GET ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs.");
|
|
1214
|
+
const dispatcher = this.guardDispatcher();
|
|
1215
|
+
for (let attempt = 0;; attempt++) {
|
|
1216
|
+
let res;
|
|
1217
|
+
this.stats.requests++;
|
|
1218
|
+
try {
|
|
1219
|
+
res = await this.fetchImpl(url, { method: "GET", headers, signal, ...(dispatcher === undefined ? {} : { dispatcher }) });
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
const e = err;
|
|
1223
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError")
|
|
1224
|
+
throw timedOut();
|
|
1225
|
+
throw new UnreachableError(`cannot reach ${url}: ${transportDetail(e)}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (res.ok) {
|
|
1228
|
+
let total = 0;
|
|
1229
|
+
if (!res.body)
|
|
1230
|
+
return { bytes: 0 };
|
|
1231
|
+
const reader = res.body.getReader();
|
|
1232
|
+
try {
|
|
1233
|
+
for (;;) {
|
|
1234
|
+
const { done, value } = await reader.read();
|
|
1235
|
+
if (done)
|
|
1236
|
+
break;
|
|
1237
|
+
if (value && value.length) {
|
|
1238
|
+
total += value.length;
|
|
1239
|
+
await onChunk(value);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch (err) {
|
|
1244
|
+
const e = err;
|
|
1245
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError")
|
|
1246
|
+
throw timedOut();
|
|
1247
|
+
throw new UnreachableError(`the download of ${path} broke off after ${total} bytes: ${transportDetail(err)}`);
|
|
1248
|
+
}
|
|
1249
|
+
finally {
|
|
1250
|
+
reader.releaseLock();
|
|
1251
|
+
}
|
|
1252
|
+
return { bytes: total };
|
|
1253
|
+
}
|
|
1254
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
1255
|
+
let payload = {};
|
|
1256
|
+
try {
|
|
1257
|
+
payload = JSON.parse(new TextDecoder().decode(buf));
|
|
1258
|
+
}
|
|
1259
|
+
catch { }
|
|
1260
|
+
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
|
|
1261
|
+
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, true, opts.signal);
|
|
1262
|
+
if (wait !== undefined) {
|
|
1263
|
+
this.stats.retries++;
|
|
1264
|
+
await sleep(wait);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1268
|
+
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from GET ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1269
|
+
status: res.status,
|
|
1270
|
+
...bodyExtra(payload),
|
|
1271
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
health(opts = {}) {
|
|
1276
|
+
return this.request({
|
|
1277
|
+
method: "GET",
|
|
1278
|
+
path: "/health",
|
|
1279
|
+
anonymous: true,
|
|
1280
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1281
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
status(opts = {}) {
|
|
1285
|
+
return this.request({
|
|
1286
|
+
method: "GET",
|
|
1287
|
+
path: "/status",
|
|
1288
|
+
anonymous: true,
|
|
1289
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1290
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
async pricing(opts = {}) {
|
|
1294
|
+
if (opts.newAccount === true) {
|
|
1295
|
+
throw new InvalidArgumentError("pricing({ newAccount: true }) is no longer supported: the price list needs a key", "Call pricing() with your API key for your organization's prices. The published list prices are on the pricing page, and `promigence pricing` prints them without a key.");
|
|
1296
|
+
}
|
|
1297
|
+
this.requireKey();
|
|
1298
|
+
return this.request({
|
|
1299
|
+
method: "GET",
|
|
1300
|
+
path: "/pricing",
|
|
1301
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1302
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
templates = {
|
|
1306
|
+
list: async (opts = {}) => {
|
|
1307
|
+
const r = await this.request({
|
|
1308
|
+
method: "GET",
|
|
1309
|
+
path: "/templates",
|
|
1310
|
+
anonymous: true,
|
|
1311
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1312
|
+
});
|
|
1313
|
+
return r?.templates ?? [];
|
|
1314
|
+
},
|
|
1315
|
+
get: (name, opts = {}) => this.request({
|
|
1316
|
+
method: "GET",
|
|
1317
|
+
path: `/templates/${seg(name)}`,
|
|
1318
|
+
anonymous: true,
|
|
1319
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1320
|
+
}),
|
|
1321
|
+
};
|
|
1322
|
+
me(opts = {}) {
|
|
1323
|
+
return this.request({ method: "GET", path: "/me", ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
1324
|
+
}
|
|
1325
|
+
outcomes(opts = {}) {
|
|
1326
|
+
return this.request({ method: "GET", path: "/me/outcomes?period=month", ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
1327
|
+
}
|
|
1328
|
+
billing = {
|
|
1329
|
+
usage: () => this.request({ method: "GET", path: "/billing" }),
|
|
1330
|
+
ledger: async (limit = 100) => (await this.billing.ledgerPage({ limit })).items,
|
|
1331
|
+
ledgerPage: async (opts = {}) => {
|
|
1332
|
+
const limit = opts.limit ?? 100;
|
|
1333
|
+
const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
|
|
1334
|
+
return pageOf(r, "entries", limit);
|
|
1335
|
+
},
|
|
1336
|
+
historyPage: async (opts = {}) => {
|
|
1337
|
+
const limit = opts.limit ?? 50;
|
|
1338
|
+
const q = new URLSearchParams(pageQuery(limit, 100, opts.cursor));
|
|
1339
|
+
if (opts.kind !== undefined && opts.kind !== "all")
|
|
1340
|
+
q.set("kind", opts.kind);
|
|
1341
|
+
const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
|
|
1342
|
+
return pageOf(r, "entries", limit);
|
|
1343
|
+
},
|
|
1344
|
+
checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
|
|
1345
|
+
portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
|
|
1346
|
+
referrals: () => this.request({ method: "GET", path: "/referrals" }),
|
|
1347
|
+
applyReferral: (code) => this.request({ method: "POST", path: "/referrals/apply", body: { code } }),
|
|
1348
|
+
};
|
|
1349
|
+
keys = {
|
|
1350
|
+
list: async (opts = {}) => {
|
|
1351
|
+
const r = await this.request({ method: "GET", path: `/keys${opts.all ? "?all=1" : ""}` });
|
|
1352
|
+
return r?.keys ?? [];
|
|
1353
|
+
},
|
|
1354
|
+
create: (opts = {}) => {
|
|
1355
|
+
const body = { ...expiryFields(opts) };
|
|
1356
|
+
if (opts.name !== undefined)
|
|
1357
|
+
body["name"] = opts.name.slice(0, 64);
|
|
1358
|
+
return this.request({ method: "POST", path: "/keys", body });
|
|
1359
|
+
},
|
|
1360
|
+
revoke: (keyId) => {
|
|
1361
|
+
if (!keyId?.trim())
|
|
1362
|
+
throw new InvalidArgumentError("keys.revoke needs a key id", "Pass a key_id from keys.list().");
|
|
1363
|
+
return this.request({ method: "DELETE", path: `/keys/${encodeURIComponent(keyId)}` });
|
|
1364
|
+
},
|
|
1365
|
+
};
|
|
1366
|
+
orgs = {
|
|
1367
|
+
list: async () => {
|
|
1368
|
+
const r = await this.request({ method: "GET", path: "/orgs" });
|
|
1369
|
+
return r?.orgs ?? [];
|
|
1370
|
+
},
|
|
1371
|
+
setDefault: (org) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/default`, body: {} }),
|
|
1372
|
+
createKey: (org, opts = {}) => {
|
|
1373
|
+
const body = { ...expiryFields(opts) };
|
|
1374
|
+
if (opts.name !== undefined)
|
|
1375
|
+
body["name"] = opts.name.slice(0, 64);
|
|
1376
|
+
if (opts.project !== undefined)
|
|
1377
|
+
body["project"] = opts.project;
|
|
1378
|
+
return this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/keys`, body });
|
|
1379
|
+
},
|
|
1380
|
+
members: async (org) => {
|
|
1381
|
+
const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/members` });
|
|
1382
|
+
return r?.members ?? [];
|
|
1383
|
+
},
|
|
1384
|
+
invites: async (org) => {
|
|
1385
|
+
const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/invites` });
|
|
1386
|
+
return r?.invites ?? [];
|
|
1387
|
+
},
|
|
1388
|
+
leave: (org) => this.request({ method: "DELETE", path: `/orgs/${orgSeg(org)}/members/me` }),
|
|
1389
|
+
projects: {
|
|
1390
|
+
list: async (org) => {
|
|
1391
|
+
const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/projects` });
|
|
1392
|
+
return r?.projects ?? [];
|
|
1393
|
+
},
|
|
1394
|
+
create: (org, name) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/projects`, body: { name } }),
|
|
1395
|
+
rename: (org, project, name) => this.request({
|
|
1396
|
+
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
|
|
1397
|
+
}),
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
credits = {
|
|
1401
|
+
offer: () => this.request({ method: "GET", path: "/credits/offer" }),
|
|
1402
|
+
};
|
|
1403
|
+
github = {
|
|
1404
|
+
status: () => this.request({ method: "GET", path: "/integrations/github" }),
|
|
1405
|
+
connect: () => this.request({ method: "POST", path: "/integrations/github/connect", body: {} }),
|
|
1406
|
+
ticket: () => this.request({ method: "POST", path: "/integrations/github/requests", body: {} }),
|
|
1407
|
+
request: (requestId) => this.request({
|
|
1408
|
+
method: "GET",
|
|
1409
|
+
path: `/integrations/github/requests/${encodeURIComponent(requestId)}`,
|
|
1410
|
+
}),
|
|
1411
|
+
disconnect: (installationId) => this.request({
|
|
1412
|
+
method: "DELETE",
|
|
1413
|
+
path: `/integrations/github/installations/${encodeURIComponent(String(installationId))}`,
|
|
1414
|
+
}),
|
|
1415
|
+
repos: (opts = {}) => {
|
|
1416
|
+
const q = new URLSearchParams();
|
|
1417
|
+
if (opts.page !== undefined)
|
|
1418
|
+
q.set("page", String(Math.max(1, Math.floor(opts.page))));
|
|
1419
|
+
if (opts.perPage !== undefined)
|
|
1420
|
+
q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
|
|
1421
|
+
const qs = q.toString();
|
|
1422
|
+
return this.request({ method: "GET", path: `/integrations/github/repos${qs ? `?${qs}` : ""}` });
|
|
1423
|
+
},
|
|
1424
|
+
untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/github/tracks/${encodeURIComponent(trackId)}` }),
|
|
1425
|
+
};
|
|
1426
|
+
gitlab = {
|
|
1427
|
+
status: () => this.request({ method: "GET", path: "/integrations/gitlab" }),
|
|
1428
|
+
connect: () => this.request({ method: "POST", path: "/integrations/gitlab/connect", body: {} }),
|
|
1429
|
+
ticket: () => this.request({ method: "POST", path: "/integrations/gitlab/requests", body: {} }),
|
|
1430
|
+
request: (requestId) => this.request({
|
|
1431
|
+
method: "GET",
|
|
1432
|
+
path: `/integrations/gitlab/requests/${encodeURIComponent(requestId)}`,
|
|
1433
|
+
}),
|
|
1434
|
+
disconnect: (connectionId) => this.request({
|
|
1435
|
+
method: "DELETE",
|
|
1436
|
+
path: `/integrations/gitlab/connections/${encodeURIComponent(connectionId)}`,
|
|
1437
|
+
}),
|
|
1438
|
+
repos: (opts = {}) => {
|
|
1439
|
+
const q = new URLSearchParams();
|
|
1440
|
+
if (opts.page !== undefined)
|
|
1441
|
+
q.set("page", String(Math.max(1, Math.floor(opts.page))));
|
|
1442
|
+
if (opts.perPage !== undefined)
|
|
1443
|
+
q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
|
|
1444
|
+
if (opts.connectionId !== undefined)
|
|
1445
|
+
q.set("connection_id", opts.connectionId);
|
|
1446
|
+
const qs = q.toString();
|
|
1447
|
+
return this.request({ method: "GET", path: `/integrations/gitlab/repos${qs ? `?${qs}` : ""}` });
|
|
1448
|
+
},
|
|
1449
|
+
untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/gitlab/tracks/${encodeURIComponent(trackId)}` }),
|
|
1450
|
+
};
|
|
1451
|
+
runs = {
|
|
1452
|
+
start: (opts) => {
|
|
1453
|
+
if (!opts.snapshot)
|
|
1454
|
+
throw new InvalidArgumentError("runs.start needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
|
|
1455
|
+
if (!Number.isInteger(opts.count) || opts.count < 1)
|
|
1456
|
+
throw new InvalidArgumentError("runs.start needs count >= 1", "Pass { count: 1 } or more.");
|
|
1457
|
+
const body = { snapshot: opts.snapshot, count: opts.count };
|
|
1458
|
+
if (opts.timeoutMs !== undefined)
|
|
1459
|
+
body["timeout_ms"] = opts.timeoutMs;
|
|
1460
|
+
if (opts.spendCap !== undefined)
|
|
1461
|
+
body["spend_cap"] = checkSpendCap(opts.spendCap);
|
|
1462
|
+
if (opts.allowPartial !== undefined)
|
|
1463
|
+
body["allow_partial"] = opts.allowPartial;
|
|
1464
|
+
if (opts.verifyEach !== undefined)
|
|
1465
|
+
body["verify_each"] = opts.verifyEach;
|
|
1466
|
+
if (opts.network !== undefined)
|
|
1467
|
+
body["network"] = opts.network;
|
|
1468
|
+
if (opts.optimizeFor !== undefined)
|
|
1469
|
+
body["optimize_for"] = opts.optimizeFor;
|
|
1470
|
+
if (opts.secrets?.length)
|
|
1471
|
+
body["secrets"] = opts.secrets;
|
|
1472
|
+
if (opts.labels && Object.keys(opts.labels).length)
|
|
1473
|
+
body["labels"] = opts.labels;
|
|
1474
|
+
if (opts.exec) {
|
|
1475
|
+
const e = {};
|
|
1476
|
+
if (opts.exec.argv)
|
|
1477
|
+
e["argv"] = opts.exec.argv;
|
|
1478
|
+
if (opts.exec.sh)
|
|
1479
|
+
e["sh"] = opts.exec.sh;
|
|
1480
|
+
if (opts.exec.cwd)
|
|
1481
|
+
e["cwd"] = opts.exec.cwd;
|
|
1482
|
+
if (opts.exec.env)
|
|
1483
|
+
e["env"] = opts.exec.env;
|
|
1484
|
+
const t = commandTimeoutMs(opts.exec.timeoutMs);
|
|
1485
|
+
if (t !== undefined)
|
|
1486
|
+
e["timeout_ms"] = t;
|
|
1487
|
+
if (opts.exec.maxOutputBytes !== undefined)
|
|
1488
|
+
e["max_output_bytes"] = opts.exec.maxOutputBytes;
|
|
1489
|
+
e["kill_on_exit"] = opts.exec.killOnExit ?? true;
|
|
1490
|
+
body["exec"] = e;
|
|
1491
|
+
}
|
|
1492
|
+
if (opts.grade) {
|
|
1493
|
+
if (!opts.grade.cmd)
|
|
1494
|
+
throw new InvalidArgumentError("runs.start grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
1495
|
+
const g = { cmd: opts.grade.cmd };
|
|
1496
|
+
if (opts.grade.holdoutCmd)
|
|
1497
|
+
g["holdout_cmd"] = opts.grade.holdoutCmd;
|
|
1498
|
+
if (opts.grade.timeoutMs !== undefined)
|
|
1499
|
+
g["timeout_ms"] = opts.grade.timeoutMs;
|
|
1500
|
+
if (opts.grade.score !== undefined)
|
|
1501
|
+
g["score"] = opts.grade.score;
|
|
1502
|
+
body["grade"] = g;
|
|
1503
|
+
}
|
|
1504
|
+
if (opts.gradeOutput !== undefined)
|
|
1505
|
+
body["grade_output"] = opts.gradeOutput;
|
|
1506
|
+
const waitMs = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(opts.waitMs, WAIT_MS_CAP) : 0;
|
|
1507
|
+
const req = { method: "POST", path: `/runs${waitQuery(waitMs)}`, body };
|
|
1508
|
+
if (waitMs > 0)
|
|
1509
|
+
req.timeoutMs = waitMs + this.requestTimeoutMs;
|
|
1510
|
+
if (opts.idempotencyKey)
|
|
1511
|
+
req.idempotencyKey = opts.idempotencyKey;
|
|
1512
|
+
if (opts.signal)
|
|
1513
|
+
req.signal = opts.signal;
|
|
1514
|
+
return this.request(req);
|
|
1515
|
+
},
|
|
1516
|
+
get: (runId, opts = {}) => this.request({
|
|
1517
|
+
method: "GET",
|
|
1518
|
+
path: `/runs/${encodeURIComponent(runId)}${waitQuery(opts.waitMs)}`,
|
|
1519
|
+
...this.readExtras(opts),
|
|
1520
|
+
}),
|
|
1521
|
+
list: async (limit = 20) => (await this.runs.listPage({ limit })).items,
|
|
1522
|
+
listPage: async (opts = {}) => {
|
|
1523
|
+
const limit = opts.limit ?? 20;
|
|
1524
|
+
const r = await this.request({ method: "GET", path: `/runs?${pageQuery(limit, 500, opts.cursor)}` });
|
|
1525
|
+
return pageOf(r, "runs", limit);
|
|
1526
|
+
},
|
|
1527
|
+
status: (runId, opts = {}) => this.runs.get(runId, opts),
|
|
1528
|
+
sandboxes: async (runId, opts = {}) => {
|
|
1529
|
+
const r = await this.runs.sandboxesWithCapacity(runId, opts);
|
|
1530
|
+
return r.sandboxes;
|
|
1531
|
+
},
|
|
1532
|
+
sandboxesWithCapacity: async (runId, opts = {}) => {
|
|
1533
|
+
const r = await this.request({
|
|
1534
|
+
method: "GET",
|
|
1535
|
+
path: `/runs/${encodeURIComponent(runId)}/sandboxes${waitQuery(opts.waitMs)}`,
|
|
1536
|
+
...this.readExtras(opts),
|
|
1537
|
+
});
|
|
1538
|
+
if (Array.isArray(r))
|
|
1539
|
+
return { sandboxes: r };
|
|
1540
|
+
const sandboxes = r?.sandboxes ?? [];
|
|
1541
|
+
return r?.capacity ? { sandboxes, capacity: r.capacity } : { sandboxes };
|
|
1542
|
+
},
|
|
1543
|
+
report: (runId) => this.request({ method: "GET", path: `/runs/${encodeURIComponent(runId)}/report` }),
|
|
1544
|
+
cancel: (runId) => this.request({ method: "POST", path: `/runs/${encodeURIComponent(runId)}/cancel`, body: {} }),
|
|
1545
|
+
kill: (runId) => this.request({ method: "DELETE", path: `/runs/${encodeURIComponent(runId)}` }),
|
|
1546
|
+
quote: (opts) => {
|
|
1547
|
+
if (!opts.snapshot)
|
|
1548
|
+
throw new InvalidArgumentError("runs.quote needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
|
|
1549
|
+
if (!Number.isInteger(opts.count) || opts.count < 1)
|
|
1550
|
+
throw new InvalidArgumentError("runs.quote needs count >= 1", "Pass { count: 1 } or more.");
|
|
1551
|
+
const body = { snapshot: opts.snapshot, count: opts.count };
|
|
1552
|
+
if (opts.timeoutMs !== undefined)
|
|
1553
|
+
body["timeout_ms"] = opts.timeoutMs;
|
|
1554
|
+
if (opts.tier !== undefined)
|
|
1555
|
+
body["tier"] = opts.tier;
|
|
1556
|
+
if (opts.spendCap !== undefined)
|
|
1557
|
+
body["spend_cap"] = checkSpendCap(opts.spendCap);
|
|
1558
|
+
return this.request({ method: "POST", path: "/runs/quote", body });
|
|
1559
|
+
},
|
|
1560
|
+
replay: (runId, sandboxIds, opts = {}) => this.request({
|
|
1561
|
+
method: "POST",
|
|
1562
|
+
path: `/runs/${encodeURIComponent(runId)}/replay`,
|
|
1563
|
+
body: {
|
|
1564
|
+
...(sandboxIds?.length ? { sandbox_ids: sandboxIds } : {}),
|
|
1565
|
+
...(opts.env && Object.keys(opts.env).length ? { env: opts.env } : {}),
|
|
1566
|
+
},
|
|
1567
|
+
}),
|
|
1568
|
+
captures: (runId) => this.request({
|
|
1569
|
+
method: "GET",
|
|
1570
|
+
path: `/runs/${encodeURIComponent(runId)}/captures`,
|
|
1571
|
+
}),
|
|
1572
|
+
open: (runId, opts = {}) => {
|
|
1573
|
+
const body = {};
|
|
1574
|
+
if (opts.sandboxId !== undefined)
|
|
1575
|
+
body["sandbox_id"] = opts.sandboxId;
|
|
1576
|
+
if (opts.spendCap !== undefined)
|
|
1577
|
+
body["spend_cap"] = opts.spendCap;
|
|
1578
|
+
if (opts.timeoutMs !== undefined)
|
|
1579
|
+
body["timeout_ms"] = opts.timeoutMs;
|
|
1580
|
+
return this.request({
|
|
1581
|
+
method: "POST",
|
|
1582
|
+
path: `/runs/${encodeURIComponent(runId)}/open`,
|
|
1583
|
+
body,
|
|
1584
|
+
});
|
|
1585
|
+
},
|
|
1586
|
+
waitReady: async (runId, opts = {}) => {
|
|
1587
|
+
const seen = new Set();
|
|
1588
|
+
const progress = opts.onReady !== undefined || opts.onCapacity !== undefined;
|
|
1589
|
+
const r = await this.waitLoop(opts, progress ? PROGRESS_POLL_MS : LONG_POLL_MS, [], async (read) => {
|
|
1590
|
+
const { sandboxes, capacity } = await this.runs.sandboxesWithCapacity(runId, read);
|
|
1591
|
+
if (capacity)
|
|
1592
|
+
opts.onCapacity?.(capacity);
|
|
1593
|
+
return sandboxes;
|
|
1594
|
+
}, (sandboxes) => {
|
|
1595
|
+
for (const s of sandboxes) {
|
|
1596
|
+
if (!isWaiting(s.state) && !seen.has(s.sandbox_id)) {
|
|
1597
|
+
seen.add(s.sandbox_id);
|
|
1598
|
+
opts.onReady?.(s);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return sandboxes.length > 0 && sandboxes.every((s) => !isWaiting(s.state));
|
|
1602
|
+
});
|
|
1603
|
+
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1604
|
+
},
|
|
1605
|
+
waitDone: async (runId, opts = {}) => {
|
|
1606
|
+
const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => {
|
|
1607
|
+
const terminal = run.state === "completed" || run.state === "cap_killed" || run.state === "aborted";
|
|
1608
|
+
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1609
|
+
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1610
|
+
return terminal || allDone;
|
|
1611
|
+
});
|
|
1612
|
+
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1615
|
+
async waitLoop(opts, maxWaitMs, empty, read, done) {
|
|
1616
|
+
const bound = opts.waitTimeoutMs ?? WAIT_TIMEOUT_MS;
|
|
1617
|
+
const started = Date.now();
|
|
1618
|
+
let longPoll = opts.pollMs === undefined;
|
|
1619
|
+
let unmarkedFast = 0;
|
|
1620
|
+
let value = empty;
|
|
1621
|
+
for (let i = 0;; i++) {
|
|
1622
|
+
const waitMs = longPoll ? Math.max(0, Math.min(bound - (Date.now() - started), maxWaitMs)) : 0;
|
|
1623
|
+
let parked;
|
|
1624
|
+
const t0 = Date.now();
|
|
1625
|
+
const readOpts = {
|
|
1626
|
+
...(waitMs > 0 ? { waitMs } : {}),
|
|
1627
|
+
onHeaders: (h) => {
|
|
1628
|
+
const v = h.get(WAITED_HEADER);
|
|
1629
|
+
if (v !== null)
|
|
1630
|
+
parked = Number(v);
|
|
1631
|
+
},
|
|
1632
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1633
|
+
};
|
|
1634
|
+
let timedOutRead = false;
|
|
1635
|
+
let comeBackMs = 0;
|
|
1636
|
+
try {
|
|
1637
|
+
value = await keepingLoopAwake(waitMs > 0 ? PARKED_TICK_MS : POLLING_TICK_MS, () => read(readOpts));
|
|
1638
|
+
}
|
|
1639
|
+
catch (err) {
|
|
1640
|
+
const elapsedNow = Date.now() - started;
|
|
1641
|
+
const refused = err instanceof RateLimitError ||
|
|
1642
|
+
(err instanceof PromigenceError && err.status === 503 && REFUSED_503_CODES.has(err.code));
|
|
1643
|
+
if (!(err instanceof TimeoutError || refused) || elapsedNow >= bound || opts.signal?.aborted)
|
|
1644
|
+
throw err;
|
|
1645
|
+
timedOutRead = true;
|
|
1646
|
+
if (refused)
|
|
1647
|
+
comeBackMs = err.retryAfterMs ?? 0;
|
|
1648
|
+
}
|
|
1649
|
+
const roundTripMs = Date.now() - t0;
|
|
1650
|
+
if (!timedOutRead && done(value))
|
|
1651
|
+
return { value, timedOut: false, waitedMs: Date.now() - started };
|
|
1652
|
+
const elapsed = Date.now() - started;
|
|
1653
|
+
if (elapsed >= bound || opts.signal?.aborted)
|
|
1654
|
+
return { value, timedOut: true, waitedMs: elapsed };
|
|
1655
|
+
if (timedOutRead) {
|
|
1656
|
+
await sleep(Math.min(Math.max(comeBackMs, opts.pollMs ?? pollInterval(i)), Math.max(1, bound - elapsed)));
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (waitMs > 0 && parked === undefined && roundTripMs < waitMs / 2 && ++unmarkedFast >= 2) {
|
|
1660
|
+
longPoll = false;
|
|
1661
|
+
}
|
|
1662
|
+
const served = parked !== undefined && parked >= Math.min(Math.floor(waitMs), WAIT_MS_CAP);
|
|
1663
|
+
if (served)
|
|
1664
|
+
continue;
|
|
1665
|
+
await sleep(Math.min(opts.pollMs ?? pollInterval(i), Math.max(1, bound - elapsed)));
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
sandboxes = {
|
|
1669
|
+
exec: (sandboxId, opts) => {
|
|
1670
|
+
if (!opts.argv?.length && !opts.sh) {
|
|
1671
|
+
throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
|
|
1672
|
+
}
|
|
1673
|
+
const body = {};
|
|
1674
|
+
if (opts.argv?.length)
|
|
1675
|
+
body["argv"] = opts.argv;
|
|
1676
|
+
if (opts.sh)
|
|
1677
|
+
body["sh"] = opts.sh;
|
|
1678
|
+
if (opts.cwd)
|
|
1679
|
+
body["cwd"] = opts.cwd;
|
|
1680
|
+
if (opts.env)
|
|
1681
|
+
body["env"] = opts.env;
|
|
1682
|
+
if (opts.user)
|
|
1683
|
+
body["user"] = opts.user;
|
|
1684
|
+
if (opts.secrets?.length)
|
|
1685
|
+
body["secrets"] = opts.secrets;
|
|
1686
|
+
if (opts.terminal)
|
|
1687
|
+
body["terminal"] = true;
|
|
1688
|
+
if (opts.killOnExit !== undefined)
|
|
1689
|
+
body["kill_on_exit"] = opts.killOnExit;
|
|
1690
|
+
const timeoutMs = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
|
|
1691
|
+
body["timeout_ms"] = timeoutMs;
|
|
1692
|
+
body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
|
|
1693
|
+
if (opts.encoding === "base64")
|
|
1694
|
+
body["encoding"] = "base64";
|
|
1695
|
+
const req = {
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
|
|
1698
|
+
body,
|
|
1699
|
+
timeoutMs: timeoutMs + 30_000,
|
|
1700
|
+
};
|
|
1701
|
+
if (opts.idempotencyKey)
|
|
1702
|
+
req.idempotencyKey = opts.idempotencyKey;
|
|
1703
|
+
if (opts.signal)
|
|
1704
|
+
req.signal = opts.signal;
|
|
1705
|
+
return this.request(req);
|
|
1706
|
+
},
|
|
1707
|
+
execStream: async (sandboxId, opts, on = {}) => {
|
|
1708
|
+
if (!opts.argv?.length && !opts.sh) {
|
|
1709
|
+
throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
|
|
1710
|
+
}
|
|
1711
|
+
if (opts.idempotencyKey) {
|
|
1712
|
+
throw new InvalidArgumentError("a streamed exec cannot take an idempotencyKey (its output cannot be replayed)", "Use sandboxes.exec (buffered) or sandboxes.execBackground with the key, or stream without one.");
|
|
1713
|
+
}
|
|
1714
|
+
const body = {};
|
|
1715
|
+
if (opts.argv?.length)
|
|
1716
|
+
body["argv"] = opts.argv;
|
|
1717
|
+
if (opts.sh)
|
|
1718
|
+
body["sh"] = opts.sh;
|
|
1719
|
+
if (opts.cwd)
|
|
1720
|
+
body["cwd"] = opts.cwd;
|
|
1721
|
+
if (opts.env)
|
|
1722
|
+
body["env"] = opts.env;
|
|
1723
|
+
if (opts.user)
|
|
1724
|
+
body["user"] = opts.user;
|
|
1725
|
+
if (opts.secrets?.length)
|
|
1726
|
+
body["secrets"] = opts.secrets;
|
|
1727
|
+
if (opts.terminal)
|
|
1728
|
+
body["terminal"] = true;
|
|
1729
|
+
if (opts.killOnExit !== undefined)
|
|
1730
|
+
body["kill_on_exit"] = opts.killOnExit;
|
|
1731
|
+
body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
|
|
1732
|
+
body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
|
|
1733
|
+
const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
|
|
1734
|
+
const idle = new AbortController();
|
|
1735
|
+
let silent = false;
|
|
1736
|
+
let reader;
|
|
1737
|
+
let timer;
|
|
1738
|
+
const arm = () => {
|
|
1739
|
+
clearTimeout(timer);
|
|
1740
|
+
timer = setTimeout(() => {
|
|
1741
|
+
silent = true;
|
|
1742
|
+
idle.abort();
|
|
1743
|
+
reader?.cancel().catch(() => { });
|
|
1744
|
+
}, STREAM_IDLE_MS);
|
|
1745
|
+
};
|
|
1746
|
+
let stream;
|
|
1747
|
+
const bearer = `Bearer ${this.requireKey()}`;
|
|
1748
|
+
const dispatcher = this.guardDispatcher();
|
|
1749
|
+
for (let attempt = 0; stream === undefined; attempt++) {
|
|
1750
|
+
let r;
|
|
1751
|
+
this.stats.requests++;
|
|
1752
|
+
arm();
|
|
1753
|
+
try {
|
|
1754
|
+
r = await this.fetchImpl(url, {
|
|
1755
|
+
method: "POST",
|
|
1756
|
+
headers: {
|
|
1757
|
+
accept: "text/event-stream",
|
|
1758
|
+
"content-type": "application/json",
|
|
1759
|
+
"user-agent": this.userAgent,
|
|
1760
|
+
authorization: bearer,
|
|
1761
|
+
...(this.caller ? { [CALLER_HEADER]: this.caller } : {}),
|
|
1762
|
+
},
|
|
1763
|
+
body: JSON.stringify(body),
|
|
1764
|
+
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
1765
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
catch (err) {
|
|
1769
|
+
clearTimeout(timer);
|
|
1770
|
+
const e = err;
|
|
1771
|
+
const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
|
|
1772
|
+
throw new UnreachableError(`cannot reach ${url}: ${why}`);
|
|
1773
|
+
}
|
|
1774
|
+
if (r.ok && r.body) {
|
|
1775
|
+
stream = r.body;
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
clearTimeout(timer);
|
|
1779
|
+
const text = await r.text().catch(() => "");
|
|
1780
|
+
let payload = {};
|
|
1781
|
+
try {
|
|
1782
|
+
payload = text ? JSON.parse(text) : {};
|
|
1783
|
+
}
|
|
1784
|
+
catch { }
|
|
1785
|
+
const retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
|
|
1786
|
+
const wait = this.refusalBackoff(r.status, retryAfterMs, payload.error?.code, attempt, false, opts.signal);
|
|
1787
|
+
if (wait !== undefined) {
|
|
1788
|
+
this.stats.retries++;
|
|
1789
|
+
await sleep(wait);
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const code = payload.error?.code ?? codeForStatus(r.status);
|
|
1793
|
+
throw errorFor(code, payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs`, payload.error?.fix ?? "See the API response for details.", {
|
|
1794
|
+
status: r.status,
|
|
1795
|
+
...bodyExtra(payload),
|
|
1796
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
const out = [];
|
|
1800
|
+
const errOut = [];
|
|
1801
|
+
let exit;
|
|
1802
|
+
let failure;
|
|
1803
|
+
const dec = new TextDecoder();
|
|
1804
|
+
let buf = "";
|
|
1805
|
+
const lost = (how) => new UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
|
|
1806
|
+
`Read it by id (\`promigence exec output <exec_id> --sandbox ${hintArg(sandboxId)}\`) or re-run.`);
|
|
1807
|
+
reader = stream.getReader();
|
|
1808
|
+
try {
|
|
1809
|
+
for (;;) {
|
|
1810
|
+
arm();
|
|
1811
|
+
let chunk;
|
|
1812
|
+
try {
|
|
1813
|
+
chunk = await reader.read();
|
|
1814
|
+
}
|
|
1815
|
+
catch (err) {
|
|
1816
|
+
if (opts.signal?.aborted)
|
|
1817
|
+
throw err;
|
|
1818
|
+
if (silent)
|
|
1819
|
+
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1820
|
+
const e = err;
|
|
1821
|
+
throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
|
|
1822
|
+
}
|
|
1823
|
+
if (chunk.done)
|
|
1824
|
+
break;
|
|
1825
|
+
const value = chunk.value;
|
|
1826
|
+
buf += dec.decode(value, { stream: true });
|
|
1827
|
+
let sep;
|
|
1828
|
+
while ((sep = buf.indexOf("\n\n")) >= 0) {
|
|
1829
|
+
const block = buf.slice(0, sep);
|
|
1830
|
+
buf = buf.slice(sep + 2);
|
|
1831
|
+
let event = "";
|
|
1832
|
+
let data = "";
|
|
1833
|
+
for (const line of block.split("\n")) {
|
|
1834
|
+
if (line.startsWith("event:"))
|
|
1835
|
+
event = line.slice(6).trim();
|
|
1836
|
+
else if (line.startsWith("data:"))
|
|
1837
|
+
data = line.slice(5).replace(/^ /, "");
|
|
1838
|
+
}
|
|
1839
|
+
if (!event)
|
|
1840
|
+
continue;
|
|
1841
|
+
if (event === "heartbeat") {
|
|
1842
|
+
on.heartbeat?.();
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
if (event === "reconnect") {
|
|
1846
|
+
reader.cancel().catch(() => { });
|
|
1847
|
+
throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
|
|
1848
|
+
}
|
|
1849
|
+
let parsed = {};
|
|
1850
|
+
try {
|
|
1851
|
+
parsed = JSON.parse(data);
|
|
1852
|
+
}
|
|
1853
|
+
catch { }
|
|
1854
|
+
if (event === "stdout" || event === "stderr") {
|
|
1855
|
+
const bytes = b64ToBytes(String(parsed["data"] ?? ""));
|
|
1856
|
+
if (event === "stdout") {
|
|
1857
|
+
out.push(bytes);
|
|
1858
|
+
on.stdout?.(bytes);
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
errOut.push(bytes);
|
|
1862
|
+
on.stderr?.(bytes);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
else if (event === "exit") {
|
|
1866
|
+
exit = { ...(exit ?? {}), ...parsed };
|
|
1867
|
+
}
|
|
1868
|
+
else if (event === "error") {
|
|
1869
|
+
failure = parsed;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
finally {
|
|
1875
|
+
clearTimeout(timer);
|
|
1876
|
+
}
|
|
1877
|
+
if (failure) {
|
|
1878
|
+
throw errorFor(failure.code ?? "internal", failure.message ?? "the exec stream failed", failure.fix ?? "Re-run; if it repeats, include the exec_id.", { status: 200, ...bodyExtra({ error: failure }) });
|
|
1879
|
+
}
|
|
1880
|
+
if (!exit && silent)
|
|
1881
|
+
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1882
|
+
if (!exit) {
|
|
1883
|
+
throw lost("ended without an exit event");
|
|
1884
|
+
}
|
|
1885
|
+
const rawCode = exit["exit_code"];
|
|
1886
|
+
const signal = exit["signal"];
|
|
1887
|
+
const exitCode = typeof rawCode === "number" && Number.isFinite(rawCode)
|
|
1888
|
+
? rawCode
|
|
1889
|
+
: typeof signal === "number" && signal > 0 ? 128 + signal : null;
|
|
1890
|
+
const join = (parts) => {
|
|
1891
|
+
const n = parts.reduce((a, p) => a + p.length, 0);
|
|
1892
|
+
const all = new Uint8Array(n);
|
|
1893
|
+
let at = 0;
|
|
1894
|
+
for (const p of parts) {
|
|
1895
|
+
all.set(p, at);
|
|
1896
|
+
at += p.length;
|
|
1897
|
+
}
|
|
1898
|
+
return new TextDecoder().decode(all);
|
|
1899
|
+
};
|
|
1900
|
+
return {
|
|
1901
|
+
exec_id: String(exit["exec_id"] ?? ""),
|
|
1902
|
+
sandbox_id: sandboxId,
|
|
1903
|
+
exit_code: exitCode,
|
|
1904
|
+
stdout: join(out),
|
|
1905
|
+
stderr: join(errOut),
|
|
1906
|
+
truncated: Boolean(exit["truncated"] ?? false),
|
|
1907
|
+
...(exit["timed_out"] === true ? { timed_out: true } : {}),
|
|
1908
|
+
...(exit["oom_killed"] === true ? { oom_killed: true } : {}),
|
|
1909
|
+
...(typeof exit["reason"] === "string" ? { reason: exit["reason"] } : {}),
|
|
1910
|
+
...(typeof signal === "number" ? { signal } : {}),
|
|
1911
|
+
};
|
|
1912
|
+
},
|
|
1913
|
+
execBackground: (sandboxId, opts) => {
|
|
1914
|
+
if (!opts.argv?.length && !opts.sh) {
|
|
1915
|
+
throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['npm','run','dev'] } or { sh: 'npm run dev' }.");
|
|
1916
|
+
}
|
|
1917
|
+
const body = { background: true };
|
|
1918
|
+
if (opts.argv?.length)
|
|
1919
|
+
body["argv"] = opts.argv;
|
|
1920
|
+
if (opts.sh)
|
|
1921
|
+
body["sh"] = opts.sh;
|
|
1922
|
+
if (opts.cwd)
|
|
1923
|
+
body["cwd"] = opts.cwd;
|
|
1924
|
+
if (opts.env)
|
|
1925
|
+
body["env"] = opts.env;
|
|
1926
|
+
if (opts.user)
|
|
1927
|
+
body["user"] = opts.user;
|
|
1928
|
+
if (opts.secrets?.length)
|
|
1929
|
+
body["secrets"] = opts.secrets;
|
|
1930
|
+
if (opts.stdin)
|
|
1931
|
+
body["stdin"] = true;
|
|
1932
|
+
if (opts.follow)
|
|
1933
|
+
body["follow"] = true;
|
|
1934
|
+
const timeoutMs = commandTimeoutMs(opts.timeoutMs);
|
|
1935
|
+
if (timeoutMs !== undefined)
|
|
1936
|
+
body["timeout_ms"] = timeoutMs;
|
|
1937
|
+
if (opts.maxOutputBytes !== undefined)
|
|
1938
|
+
body["max_output_bytes"] = opts.maxOutputBytes;
|
|
1939
|
+
return this.request({
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
|
|
1942
|
+
body,
|
|
1943
|
+
...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
|
|
1944
|
+
});
|
|
1945
|
+
},
|
|
1946
|
+
execs: {
|
|
1947
|
+
list: async (sandboxId) => {
|
|
1948
|
+
const r = await this.request({
|
|
1949
|
+
method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
|
|
1950
|
+
});
|
|
1951
|
+
return r.execs ?? [];
|
|
1952
|
+
},
|
|
1953
|
+
get: (sandboxId, execId, opts = {}) => {
|
|
1954
|
+
const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
|
|
1955
|
+
return this.request({
|
|
1956
|
+
method: "GET",
|
|
1957
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}${wait ? `?wait_ms=${wait}` : ""}`,
|
|
1958
|
+
...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
|
|
1959
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1960
|
+
});
|
|
1961
|
+
},
|
|
1962
|
+
read: async (sandboxId, execId, stream, opts = {}) => {
|
|
1963
|
+
const q = [`stream=${stream}`, `offset=${Math.max(0, Math.floor(opts.offset ?? 0))}`];
|
|
1964
|
+
if (opts.len !== undefined)
|
|
1965
|
+
q.push(`len=${Math.max(0, Math.floor(opts.len))}`);
|
|
1966
|
+
const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
|
|
1967
|
+
if (wait)
|
|
1968
|
+
q.push(`wait_ms=${wait}`);
|
|
1969
|
+
if (wait && opts.untilOutput)
|
|
1970
|
+
q.push("until=output");
|
|
1971
|
+
const r = await this.request({
|
|
1972
|
+
method: "GET",
|
|
1973
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}?${q.join("&")}`,
|
|
1974
|
+
...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
|
|
1975
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
1976
|
+
});
|
|
1977
|
+
return { data: b64ToBytes(r.data ?? ""), offset: r.offset ?? opts.offset ?? 0, eof: Boolean(r.eof), skipped: r.skipped ?? 0, record: r };
|
|
1978
|
+
},
|
|
1979
|
+
stdin: (sandboxId, execId, opts = {}) => {
|
|
1980
|
+
if (opts.data === undefined && !opts.eof) {
|
|
1981
|
+
throw new InvalidArgumentError("execs.stdin needs data, eof, or both", "Pass { data: 'y\\n' } to write, { eof: true } to close, or both to do each in one call.");
|
|
1982
|
+
}
|
|
1983
|
+
const bytes = typeof opts.data === "string" ? new TextEncoder().encode(opts.data) : opts.data;
|
|
1984
|
+
return this.request({
|
|
1985
|
+
method: "POST",
|
|
1986
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/stdin`,
|
|
1987
|
+
body: {
|
|
1988
|
+
...(bytes === undefined ? {} : { data: bytesToB64(bytes) }),
|
|
1989
|
+
...(opts.eof ? { eof: true } : {}),
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
},
|
|
1993
|
+
signal: (sandboxId, execId, signal = "SIGTERM") => this.request({
|
|
1994
|
+
method: "POST",
|
|
1995
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/signal`,
|
|
1996
|
+
body: { signal },
|
|
1997
|
+
}),
|
|
1998
|
+
forget: (sandboxId, execId) => this.request({
|
|
1999
|
+
method: "DELETE",
|
|
2000
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}`,
|
|
2001
|
+
}),
|
|
2002
|
+
},
|
|
2003
|
+
ports: {
|
|
2004
|
+
expose: (sandboxId, port) => {
|
|
2005
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2006
|
+
throw new InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
|
|
2007
|
+
}
|
|
2008
|
+
return withPreviewsFix(this.request({
|
|
2009
|
+
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`, body: { port },
|
|
2010
|
+
}), sandboxId, port);
|
|
2011
|
+
},
|
|
2012
|
+
list: async (sandboxId) => {
|
|
2013
|
+
const r = await withPreviewsFix(this.request({
|
|
2014
|
+
method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2015
|
+
}), sandboxId);
|
|
2016
|
+
return r.ports ?? [];
|
|
2017
|
+
},
|
|
2018
|
+
revoke: (sandboxId, tokenOrPort) => this.request({
|
|
2019
|
+
method: "DELETE",
|
|
2020
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
|
|
2021
|
+
}),
|
|
2022
|
+
},
|
|
2023
|
+
ssh: {
|
|
2024
|
+
open: (sandboxId, opts) => {
|
|
2025
|
+
if (!opts.publicKey?.trim()) {
|
|
2026
|
+
throw new InvalidArgumentError("publicKey is required", "Pass the contents of an ssh-ed25519 .pub file, or use `promigence sandbox ssh <id>`, which makes one.");
|
|
2027
|
+
}
|
|
2028
|
+
return this.request({
|
|
2029
|
+
method: "POST",
|
|
2030
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
|
|
2031
|
+
body: {
|
|
2032
|
+
public_key: opts.publicKey.trim(),
|
|
2033
|
+
...(opts.ttlS === undefined ? {} : { ttl_s: Math.floor(opts.ttlS) }),
|
|
2034
|
+
...(opts.idleTimeoutS === undefined
|
|
2035
|
+
? {}
|
|
2036
|
+
: { idle_timeout_s: Math.floor(opts.idleTimeoutS) }),
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
},
|
|
2040
|
+
list: (sandboxId) => this.request({
|
|
2041
|
+
method: "GET",
|
|
2042
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
|
|
2043
|
+
}),
|
|
2044
|
+
revoke: (sandboxId, keyIdOrFingerprint) => this.request({
|
|
2045
|
+
method: "DELETE",
|
|
2046
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh/${encodeURIComponent(keyIdOrFingerprint)}`,
|
|
2047
|
+
}),
|
|
2048
|
+
connect: (sandboxId, opts) => connectSshTunnel({
|
|
2049
|
+
url: `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/ssh/connect`,
|
|
2050
|
+
token: opts.token ?? this.requireKey(),
|
|
2051
|
+
userAgent: this.userAgent,
|
|
2052
|
+
...(this.caller ? { caller: this.caller } : {}),
|
|
2053
|
+
onData: opts.onData,
|
|
2054
|
+
onClose: opts.onClose,
|
|
2055
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
2056
|
+
refuse: (status, text) => {
|
|
2057
|
+
let payload = {};
|
|
2058
|
+
try {
|
|
2059
|
+
payload = text ? JSON.parse(text) : {};
|
|
2060
|
+
}
|
|
2061
|
+
catch { }
|
|
2062
|
+
const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
|
|
2063
|
+
return errorFor(code, payload.error?.message ?? `HTTP ${status} from the SSH tunnel`, payload.error?.fix ?? "See the API response for details.", { status, ...bodyExtra(payload) });
|
|
2064
|
+
},
|
|
2065
|
+
}),
|
|
2066
|
+
},
|
|
2067
|
+
pty: (sandboxId, opts = {}) => {
|
|
2068
|
+
const q = [];
|
|
2069
|
+
if (opts.cols !== undefined)
|
|
2070
|
+
q.push(`cols=${Math.floor(opts.cols)}`);
|
|
2071
|
+
if (opts.rows !== undefined)
|
|
2072
|
+
q.push(`rows=${Math.floor(opts.rows)}`);
|
|
2073
|
+
const path = `/sandboxes/${encodeURIComponent(sandboxId)}/pty${q.length ? `?${q.join("&")}` : ""}`;
|
|
2074
|
+
const open = () => connectPty({
|
|
2075
|
+
url: `${this.apiUrl}${path}`,
|
|
2076
|
+
apiKey: this.requireKey(),
|
|
2077
|
+
userAgent: this.userAgent,
|
|
2078
|
+
...(this.caller ? { caller: this.caller } : {}),
|
|
2079
|
+
...(opts.cmd ? { command: opts.cmd } : {}),
|
|
2080
|
+
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
2081
|
+
...(opts.onData ? { onData: opts.onData } : {}),
|
|
2082
|
+
...(opts.onExit ? { onExit: opts.onExit } : {}),
|
|
2083
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
2084
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
2085
|
+
...(opts.reconnect === undefined ? {} : { reconnect: opts.reconnect }),
|
|
2086
|
+
...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
|
|
2087
|
+
refuse: (status, text, retryAfter) => {
|
|
2088
|
+
let payload = {};
|
|
2089
|
+
try {
|
|
2090
|
+
payload = text ? JSON.parse(text) : {};
|
|
2091
|
+
}
|
|
2092
|
+
catch { }
|
|
2093
|
+
const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
|
|
2094
|
+
const retryAfterMs = parseRetryAfter(retryAfter ?? null);
|
|
2095
|
+
return errorFor(code, payload.error?.message ?? `HTTP ${status} from GET ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
2096
|
+
status,
|
|
2097
|
+
...bodyExtra(payload),
|
|
2098
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
2099
|
+
});
|
|
2100
|
+
},
|
|
2101
|
+
});
|
|
2102
|
+
return (async () => {
|
|
2103
|
+
for (let attempt = 0;; attempt++) {
|
|
2104
|
+
try {
|
|
2105
|
+
return await open();
|
|
2106
|
+
}
|
|
2107
|
+
catch (err) {
|
|
2108
|
+
const e = err instanceof PromigenceError ? err : undefined;
|
|
2109
|
+
const wait = e ? this.refusalBackoff(e.status, e.retryAfterMs, e.code, attempt, false) : undefined;
|
|
2110
|
+
if (wait === undefined)
|
|
2111
|
+
throw err;
|
|
2112
|
+
this.stats.retries++;
|
|
2113
|
+
await sleep(wait);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
})();
|
|
2117
|
+
},
|
|
2118
|
+
get: (sandboxId) => {
|
|
2119
|
+
checkSandboxId(sandboxId, "sandboxes.get");
|
|
2120
|
+
return this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}` });
|
|
2121
|
+
},
|
|
2122
|
+
pause: (sandboxId) => this.request({
|
|
2123
|
+
method: "POST",
|
|
2124
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/pause`,
|
|
2125
|
+
body: {},
|
|
2126
|
+
timeoutMs: 10 * 60_000,
|
|
2127
|
+
}),
|
|
2128
|
+
resume: (sandboxId, opts = {}) => this.request({
|
|
2129
|
+
method: "POST",
|
|
2130
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/resume`,
|
|
2131
|
+
body: {
|
|
2132
|
+
...(opts.optimizeFor === undefined ? {} : { optimize_for: opts.optimizeFor }),
|
|
2133
|
+
...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
|
|
2134
|
+
},
|
|
2135
|
+
}),
|
|
2136
|
+
snapshot: (sandboxId, opts = {}) => this.request({
|
|
2137
|
+
method: "POST",
|
|
2138
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
|
|
2139
|
+
body: {
|
|
2140
|
+
...(opts.alias === undefined ? {} : { alias: opts.alias }),
|
|
2141
|
+
...(opts.force === undefined ? {} : { force: opts.force }),
|
|
2142
|
+
...(opts.moveAlias === undefined ? {} : { move_alias: opts.moveAlias }),
|
|
2143
|
+
},
|
|
2144
|
+
timeoutMs: 10 * 60_000,
|
|
2145
|
+
}),
|
|
2146
|
+
list: async (opts = 50) => (await this.sandboxes.listPage(opts)).items,
|
|
2147
|
+
listPage: async (opts = 50) => {
|
|
2148
|
+
const o = typeof opts === "number" ? { limit: opts } : opts;
|
|
2149
|
+
const q = new URLSearchParams({ limit: String(Math.max(1, Math.trunc(o.limit ?? 50))) });
|
|
2150
|
+
const pairs = Object.entries(o.labels ?? {});
|
|
2151
|
+
if (pairs.length)
|
|
2152
|
+
q.set("labels", pairs.map(([k, v]) => `${k}=${v}`).join(","));
|
|
2153
|
+
if (o.state?.length)
|
|
2154
|
+
q.set("state", o.state.join(","));
|
|
2155
|
+
if (o.cursor)
|
|
2156
|
+
q.set("cursor", o.cursor);
|
|
2157
|
+
const r = await this.request({
|
|
2158
|
+
method: "GET",
|
|
2159
|
+
path: `/sandboxes?${q.toString()}`,
|
|
2160
|
+
});
|
|
2161
|
+
return { items: r.sandboxes ?? [], nextCursor: r.next_cursor ?? null, limit: r.limit ?? (o.limit ?? 50) };
|
|
2162
|
+
},
|
|
2163
|
+
probe: (sandboxId, opts = {}) => this.request({
|
|
2164
|
+
method: "POST",
|
|
2165
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/probe`,
|
|
2166
|
+
body: opts.paths?.length ? { paths: opts.paths } : {},
|
|
2167
|
+
}),
|
|
2168
|
+
kill: (sandboxId) => this.request({ method: "DELETE", path: `/sandboxes/${encodeURIComponent(sandboxId)}` }),
|
|
2169
|
+
network: (sandboxId) => this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/network` }),
|
|
2170
|
+
setNetworkNone: (sandboxId) => this.request({
|
|
2171
|
+
method: "POST",
|
|
2172
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/network`,
|
|
2173
|
+
body: { network: "none" },
|
|
2174
|
+
}),
|
|
2175
|
+
patch: (sandboxId, opts) => {
|
|
2176
|
+
const body = {};
|
|
2177
|
+
if (opts.timeoutMs !== undefined) {
|
|
2178
|
+
if (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs <= 0) {
|
|
2179
|
+
throw new InvalidArgumentError("sandboxes.patch needs a positive timeoutMs", "Pass { timeoutMs: 1_800_000 }.");
|
|
2180
|
+
}
|
|
2181
|
+
body["timeout_ms"] = opts.timeoutMs;
|
|
2182
|
+
}
|
|
2183
|
+
if (opts.autoPauseS !== undefined) {
|
|
2184
|
+
if (!Number.isInteger(opts.autoPauseS) || opts.autoPauseS < 0) {
|
|
2185
|
+
throw new InvalidArgumentError("sandboxes.patch needs a non-negative whole number of seconds for autoPauseS", "Pass { autoPauseS: 900 } to pause it after 15 idle minutes, or { autoPauseS: 0 } to turn that off.");
|
|
2186
|
+
}
|
|
2187
|
+
body["auto_pause_s"] = opts.autoPauseS;
|
|
2188
|
+
}
|
|
2189
|
+
if (opts.autoPauseMode !== undefined) {
|
|
2190
|
+
if (opts.autoPauseMode !== "requests" && opts.autoPauseMode !== "activity") {
|
|
2191
|
+
throw new InvalidArgumentError(`sandboxes.patch: autoPauseMode must be "requests" or "activity", got ${JSON.stringify(opts.autoPauseMode)}`, 'Pass { autoPauseMode: "activity" } for an agent that runs inside the sandbox.');
|
|
2192
|
+
}
|
|
2193
|
+
body["auto_pause_mode"] = opts.autoPauseMode;
|
|
2194
|
+
}
|
|
2195
|
+
if (Object.keys(body).length === 0) {
|
|
2196
|
+
throw new InvalidArgumentError("sandboxes.patch was given nothing to change", "Pass { timeoutMs } to extend it, { autoPauseS } to set its idle rule, or both.");
|
|
2197
|
+
}
|
|
2198
|
+
return this.request({
|
|
2199
|
+
method: "PATCH",
|
|
2200
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}`,
|
|
2201
|
+
body,
|
|
2202
|
+
});
|
|
2203
|
+
},
|
|
2204
|
+
files: {
|
|
2205
|
+
list: (sandboxId, path = "/", opts = {}) => {
|
|
2206
|
+
const q = [`path=${encodeURIComponent(path)}`];
|
|
2207
|
+
if (opts.recursive)
|
|
2208
|
+
q.push("recursive=1");
|
|
2209
|
+
if (opts.depth !== undefined)
|
|
2210
|
+
q.push(`depth=${Math.max(0, Math.floor(opts.depth))}`);
|
|
2211
|
+
if (opts.limit !== undefined)
|
|
2212
|
+
q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
|
|
2213
|
+
if (opts.hidden === false)
|
|
2214
|
+
q.push("hidden=0");
|
|
2215
|
+
return this.request({
|
|
2216
|
+
method: "GET",
|
|
2217
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list?${q.join("&")}`,
|
|
2218
|
+
});
|
|
2219
|
+
},
|
|
2220
|
+
search: (sandboxId, path, opts = {}) => {
|
|
2221
|
+
if (!opts.pattern && !opts.contains) {
|
|
2222
|
+
throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
|
|
2223
|
+
}
|
|
2224
|
+
const q = [`path=${encodeURIComponent(path)}`];
|
|
2225
|
+
if (opts.pattern)
|
|
2226
|
+
q.push(`pattern=${encodeURIComponent(opts.pattern)}`);
|
|
2227
|
+
if (opts.contains)
|
|
2228
|
+
q.push(`contains=${encodeURIComponent(opts.contains)}`);
|
|
2229
|
+
if (opts.ignoreCase)
|
|
2230
|
+
q.push("ignore_case=1");
|
|
2231
|
+
if (opts.limit !== undefined)
|
|
2232
|
+
q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
|
|
2233
|
+
if (opts.hidden === false)
|
|
2234
|
+
q.push("hidden=0");
|
|
2235
|
+
return this.request({
|
|
2236
|
+
method: "GET",
|
|
2237
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search?${q.join("&")}`,
|
|
2238
|
+
});
|
|
2239
|
+
},
|
|
2240
|
+
mkdir: (sandboxId, path, opts = {}) => this.request({
|
|
2241
|
+
method: "POST",
|
|
2242
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/mkdir`,
|
|
2243
|
+
body: {
|
|
2244
|
+
path,
|
|
2245
|
+
...(opts.mode === undefined ? {} : { mode: modeParam(opts.mode) }),
|
|
2246
|
+
...(opts.parents === undefined ? {} : { parents: opts.parents }),
|
|
2247
|
+
},
|
|
2248
|
+
}),
|
|
2249
|
+
move: (sandboxId, from, to, opts = {}) => this.request({
|
|
2250
|
+
method: "POST",
|
|
2251
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/move`,
|
|
2252
|
+
body: { from, to, ...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }) },
|
|
2253
|
+
}),
|
|
2254
|
+
copy: (sandboxId, from, to, opts = {}) => this.request({
|
|
2255
|
+
method: "POST",
|
|
2256
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/copy`,
|
|
2257
|
+
body: {
|
|
2258
|
+
from, to,
|
|
2259
|
+
...(opts.recursive === undefined ? {} : { recursive: opts.recursive }),
|
|
2260
|
+
...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }),
|
|
2261
|
+
},
|
|
2262
|
+
}),
|
|
2263
|
+
chmod: (sandboxId, path, opts) => {
|
|
2264
|
+
if (opts.mode === undefined && opts.uid === undefined && opts.gid === undefined) {
|
|
2265
|
+
throw new InvalidArgumentError("files.chmod needs one of mode, uid or gid", "Pass { mode: 0o755 }, { uid: 1000 }, { gid: 1000 }, or any combination.");
|
|
2266
|
+
}
|
|
2267
|
+
return this.request({
|
|
2268
|
+
method: "POST",
|
|
2269
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/chmod`,
|
|
2270
|
+
body: {
|
|
2271
|
+
path,
|
|
2272
|
+
...(opts.mode === undefined ? {} : { mode: modeParam(opts.mode) }),
|
|
2273
|
+
...(opts.uid === undefined ? {} : { uid: opts.uid }),
|
|
2274
|
+
...(opts.gid === undefined ? {} : { gid: opts.gid }),
|
|
2275
|
+
...(opts.recursive === undefined ? {} : { recursive: opts.recursive }),
|
|
2276
|
+
},
|
|
2277
|
+
});
|
|
2278
|
+
},
|
|
2279
|
+
archive: (sandboxId, path, opts = {}) => this.request({
|
|
2280
|
+
method: "POST",
|
|
2281
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/archive`,
|
|
2282
|
+
body: {
|
|
2283
|
+
path,
|
|
2284
|
+
...(opts.out === undefined ? {} : { out: opts.out }),
|
|
2285
|
+
...(opts.exclude?.length ? { exclude: opts.exclude } : {}),
|
|
2286
|
+
},
|
|
2287
|
+
}),
|
|
2288
|
+
extract: (sandboxId, archive, into, opts = {}) => this.request({
|
|
2289
|
+
method: "POST",
|
|
2290
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/extract`,
|
|
2291
|
+
body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
|
|
2292
|
+
}),
|
|
2293
|
+
stat: (sandboxId, path) => this.request({
|
|
2294
|
+
method: "GET",
|
|
2295
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat?path=${encodeURIComponent(path)}`,
|
|
2296
|
+
}),
|
|
2297
|
+
get: (sandboxId, path, opts = {}) => this.bytes("GET", `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, undefined, opts),
|
|
2298
|
+
download: (sandboxId, path, onChunk, opts = {}) => this.stream(`/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, onChunk, opts),
|
|
2299
|
+
text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
|
|
2300
|
+
put: async (sandboxId, path, data, opts = {}) => {
|
|
2301
|
+
const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
2302
|
+
return this.sandboxes.files.putFrom(sandboxId, path, {
|
|
2303
|
+
size: body.length,
|
|
2304
|
+
read: (offset, len) => body.subarray(offset, offset + len),
|
|
2305
|
+
}, opts);
|
|
2306
|
+
},
|
|
2307
|
+
putFrom: async (sandboxId, path, source, opts = {}) => {
|
|
2308
|
+
const mode = opts.mode === undefined ? "" : `&mode=${opts.mode.toString(8)}`;
|
|
2309
|
+
const base = `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}${mode}`;
|
|
2310
|
+
let sent = 0;
|
|
2311
|
+
let chunks = 0;
|
|
2312
|
+
let last;
|
|
2313
|
+
do {
|
|
2314
|
+
const slice = await source.read(sent, Math.min(FILE_PUT_CHUNK_BYTES, source.size - sent));
|
|
2315
|
+
if (slice.length === 0 && sent < source.size) {
|
|
2316
|
+
throw new InvalidArgumentError(`the source of ${path} ended at ${sent} of ${source.size} bytes`, "The file shrank while it was being sent; send it again once nothing is writing it.");
|
|
2317
|
+
}
|
|
2318
|
+
const raw = await this.bytes("PUT", sent === 0 ? base : `${base}&offset=${sent}`, slice, opts);
|
|
2319
|
+
last = JSON.parse(new TextDecoder().decode(raw));
|
|
2320
|
+
sent += slice.length;
|
|
2321
|
+
chunks += 1;
|
|
2322
|
+
} while (sent < source.size);
|
|
2323
|
+
return { ...last, bytes: source.size, chunks };
|
|
2324
|
+
},
|
|
2325
|
+
delete: (sandboxId, path, opts = {}) => this.request({
|
|
2326
|
+
method: "DELETE",
|
|
2327
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`
|
|
2328
|
+
+ (opts.recursive ? "&recursive=1" : ""),
|
|
2329
|
+
}),
|
|
2330
|
+
},
|
|
2331
|
+
output: async (execId, opts = {}) => {
|
|
2332
|
+
if (!execId)
|
|
2333
|
+
throw new InvalidArgumentError("exec output needs an exec_id", "Pass the `exec_id` the exec envelope printed.");
|
|
2334
|
+
let runId = opts.runId;
|
|
2335
|
+
const fromSandbox = async (sid) => {
|
|
2336
|
+
const rec = await this.request({
|
|
2337
|
+
method: "GET", path: `/sandboxes/${encodeURIComponent(sid)}`,
|
|
2338
|
+
});
|
|
2339
|
+
const hit = (rec?.execs ?? []).find((e) => e.exec_id === execId);
|
|
2340
|
+
if (hit && rec?.run_id)
|
|
2341
|
+
runId ??= rec.run_id;
|
|
2342
|
+
return hit;
|
|
2343
|
+
};
|
|
2344
|
+
let found;
|
|
2345
|
+
let sandboxId = opts.sandboxId;
|
|
2346
|
+
let routeMissing = false;
|
|
2347
|
+
try {
|
|
2348
|
+
found = await this.request({ method: "GET", path: `/execs/${encodeURIComponent(execId)}` });
|
|
2349
|
+
}
|
|
2350
|
+
catch (err) {
|
|
2351
|
+
if (!(err instanceof NotFoundError))
|
|
2352
|
+
throw err;
|
|
2353
|
+
found = undefined;
|
|
2354
|
+
routeMissing = true;
|
|
2355
|
+
}
|
|
2356
|
+
if (found && opts.runId && found.run_id && found.run_id !== opts.runId)
|
|
2357
|
+
found = undefined;
|
|
2358
|
+
let otherSandbox = false;
|
|
2359
|
+
if (found && opts.sandboxId && found.sandbox_id && found.sandbox_id !== opts.sandboxId) {
|
|
2360
|
+
found = undefined;
|
|
2361
|
+
otherSandbox = true;
|
|
2362
|
+
}
|
|
2363
|
+
if (found)
|
|
2364
|
+
runId ??= found.run_id;
|
|
2365
|
+
if (!found && (routeMissing || otherSandbox) && opts.sandboxId) {
|
|
2366
|
+
try {
|
|
2367
|
+
found = await fromSandbox(opts.sandboxId);
|
|
2368
|
+
}
|
|
2369
|
+
catch (err) {
|
|
2370
|
+
if (!(err instanceof NotFoundError))
|
|
2371
|
+
throw err;
|
|
2372
|
+
found = undefined;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
sandboxId ??= found?.sandbox_id;
|
|
2376
|
+
if (!found) {
|
|
2377
|
+
throw new NotFoundError(`exec ${execId} not found${sandboxId ? ` in sandbox ${sandboxId}` : opts.runId ? ` in run ${opts.runId}` : ""}`, "Check the `exec_id` from the exec envelope, and that the sandbox it ran in still exists (`promigence run report <run_id>`).");
|
|
2378
|
+
}
|
|
2379
|
+
return sliceOutput(found, sandboxId, runId, opts.range);
|
|
2380
|
+
},
|
|
2381
|
+
};
|
|
2382
|
+
sandbox(id) {
|
|
2383
|
+
return {
|
|
2384
|
+
id,
|
|
2385
|
+
exec: (opts) => this.sandboxes.exec(id, opts).then(toCommandResult),
|
|
2386
|
+
get: () => this.sandboxes.get(id),
|
|
2387
|
+
kill: () => this.sandboxes.kill(id),
|
|
2388
|
+
pause: () => this.sandboxes.pause(id),
|
|
2389
|
+
resume: (opts) => this.sandboxes.resume(id, opts),
|
|
2390
|
+
patch: (opts) => this.sandboxes.patch(id, opts),
|
|
2391
|
+
files: {
|
|
2392
|
+
get: (path) => this.sandboxes.files.get(id, path),
|
|
2393
|
+
text: (path) => this.sandboxes.files.text(id, path),
|
|
2394
|
+
put: (path, data, opts) => this.sandboxes.files.put(id, path, data, opts ?? {}),
|
|
2395
|
+
stat: (path) => this.sandboxes.files.stat(id, path),
|
|
2396
|
+
delete: (path) => this.sandboxes.files.delete(id, path),
|
|
2397
|
+
},
|
|
2398
|
+
probe: (opts = {}) => this.sandboxes.probe(id, opts),
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
secrets = {
|
|
2402
|
+
set: (name, value, opts = {}) => {
|
|
2403
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "")) {
|
|
2404
|
+
throw new InvalidArgumentError(`secrets.set: ${JSON.stringify(name ?? null)} is not usable as an environment variable`, "Use letters, digits and underscores, not starting with a digit: OPENAI_API_KEY.");
|
|
2405
|
+
}
|
|
2406
|
+
if (!value) {
|
|
2407
|
+
throw new InvalidArgumentError(`secrets.set: ${name} has an empty value`, `To remove it, call secrets.delete(${JSON.stringify(name)}).`);
|
|
2408
|
+
}
|
|
2409
|
+
const body = { name, value };
|
|
2410
|
+
if (opts.hosts !== undefined)
|
|
2411
|
+
body.hosts = opts.hosts;
|
|
2412
|
+
if (opts.headers !== undefined)
|
|
2413
|
+
body.headers = opts.headers;
|
|
2414
|
+
if (opts.raw !== undefined)
|
|
2415
|
+
body.raw = opts.raw;
|
|
2416
|
+
if (opts.scanResponses !== undefined)
|
|
2417
|
+
body.scan_responses = opts.scanResponses;
|
|
2418
|
+
return this.request({ method: "POST", path: "/secrets", body });
|
|
2419
|
+
},
|
|
2420
|
+
list: () => this.request({ method: "GET", path: "/secrets" }),
|
|
2421
|
+
delete: (name) => this.request({
|
|
2422
|
+
method: "DELETE",
|
|
2423
|
+
path: `/secrets/${encodeURIComponent(name)}`,
|
|
2424
|
+
}),
|
|
2425
|
+
};
|
|
2426
|
+
images = {
|
|
2427
|
+
resolve: (image, opts = {}) => {
|
|
2428
|
+
if (!image?.trim()) {
|
|
2429
|
+
throw new InvalidArgumentError("images.resolve needs an image reference", "Pass a name like 'python:3.12' or 'ghcr.io/org/app:v1'.");
|
|
2430
|
+
}
|
|
2431
|
+
const body = { image: image.trim() };
|
|
2432
|
+
if (opts.registrySecret)
|
|
2433
|
+
body["registry_secret"] = opts.registrySecret;
|
|
2434
|
+
return this.request({ method: "POST", path: "/images/resolve", body });
|
|
2435
|
+
},
|
|
2436
|
+
};
|
|
2437
|
+
captures = {
|
|
2438
|
+
delete: (snapshotId) => this.request({
|
|
2439
|
+
method: "DELETE",
|
|
2440
|
+
path: `/captures/${encodeURIComponent(snapshotId)}`,
|
|
2441
|
+
}),
|
|
2442
|
+
};
|
|
2443
|
+
snapshots = {
|
|
2444
|
+
create: (opts) => {
|
|
2445
|
+
const body = snapshotCreateBody(opts);
|
|
2446
|
+
return this.request({
|
|
2447
|
+
method: "POST", path: "/snapshots", body,
|
|
2448
|
+
...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
|
|
2449
|
+
});
|
|
2450
|
+
},
|
|
2451
|
+
status: (buildId, opts = {}) => this.request({
|
|
2452
|
+
method: "GET",
|
|
2453
|
+
path: `/snapshots/builds/${encodeURIComponent(buildId)}${waitQuery(opts.waitMs)}`,
|
|
2454
|
+
...this.readExtras(opts),
|
|
2455
|
+
}),
|
|
2456
|
+
buildLog: async (buildId, tail = 200) => {
|
|
2457
|
+
const r = await this.request({
|
|
2458
|
+
method: "GET",
|
|
2459
|
+
path: `/snapshots/builds/${encodeURIComponent(buildId)}/build-log?tail=${Math.max(1, Math.floor(tail))}`,
|
|
2460
|
+
});
|
|
2461
|
+
return r?.log ?? [];
|
|
2462
|
+
},
|
|
2463
|
+
builds: async (limit = 20) => (await this.snapshots.buildsPage({ limit })).items,
|
|
2464
|
+
buildsPage: async (opts = {}) => {
|
|
2465
|
+
const limit = opts.limit ?? 20;
|
|
2466
|
+
const r = await this.request({ method: "GET", path: `/snapshots/builds?${pageQuery(limit, 500, opts.cursor)}` });
|
|
2467
|
+
return pageOf(r, "builds", limit);
|
|
2468
|
+
},
|
|
2469
|
+
get: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}` }),
|
|
2470
|
+
list: async (opts = {}) => (await this.snapshots.listPage(opts)).items,
|
|
2471
|
+
listPage: async (opts = {}) => {
|
|
2472
|
+
const q = new URLSearchParams();
|
|
2473
|
+
if (opts.limit !== undefined)
|
|
2474
|
+
q.set("limit", String(Math.max(1, Math.trunc(opts.limit))));
|
|
2475
|
+
if (opts.cursor)
|
|
2476
|
+
q.set("cursor", opts.cursor);
|
|
2477
|
+
const qs = q.toString();
|
|
2478
|
+
const r = await this.request({
|
|
2479
|
+
method: "GET", path: `/snapshots${qs ? `?${qs}` : ""}`, ...(this.authenticated ? {} : { anonymous: true }),
|
|
2480
|
+
});
|
|
2481
|
+
if (Array.isArray(r))
|
|
2482
|
+
return { items: r, nextCursor: null, limit: opts.limit ?? r.length };
|
|
2483
|
+
return { items: r?.snapshots ?? [], nextCursor: r?.next_cursor ?? null, limit: r?.limit ?? (opts.limit ?? 0) };
|
|
2484
|
+
},
|
|
2485
|
+
public: async (limit = 20) => {
|
|
2486
|
+
const r = await this.request({
|
|
2487
|
+
method: "GET",
|
|
2488
|
+
path: `/snapshots?limit=${Math.max(1, Math.floor(limit))}`,
|
|
2489
|
+
anonymous: true,
|
|
2490
|
+
});
|
|
2491
|
+
const all = Array.isArray(r) ? r : (r?.snapshots ?? []);
|
|
2492
|
+
return all.filter((s) => s.visibility === undefined || s.visibility === "public").slice(0, limit);
|
|
2493
|
+
},
|
|
2494
|
+
delete: (idOrAlias) => {
|
|
2495
|
+
if (!idOrAlias)
|
|
2496
|
+
throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2497
|
+
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2498
|
+
},
|
|
2499
|
+
alias: (idOrAlias, alias) => this.request({
|
|
2500
|
+
method: "PUT",
|
|
2501
|
+
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2502
|
+
body: { alias },
|
|
2503
|
+
}),
|
|
2504
|
+
waitBuild: async (buildId, opts = {}) => {
|
|
2505
|
+
const r = await this.waitLoop(opts, BUILD_POLL_MS, { build_id: buildId, state: "running" }, (read) => this.snapshots.status(buildId, read), (b) => b.state !== "running" && b.state !== "queued");
|
|
2506
|
+
return { build: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
2507
|
+
},
|
|
2508
|
+
verify: (id, opts = {}) => {
|
|
2509
|
+
if (!id)
|
|
2510
|
+
throw new InvalidArgumentError("snapshots.verify needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2511
|
+
const body = {};
|
|
2512
|
+
if (opts.timeoutMs !== undefined)
|
|
2513
|
+
body["timeout_ms"] = opts.timeoutMs;
|
|
2514
|
+
if (opts.spendCap !== undefined)
|
|
2515
|
+
body["spend_cap"] = opts.spendCap;
|
|
2516
|
+
return this.request({
|
|
2517
|
+
method: "POST",
|
|
2518
|
+
path: `/snapshots/${encodeURIComponent(id)}/verify`,
|
|
2519
|
+
body,
|
|
2520
|
+
});
|
|
2521
|
+
},
|
|
2522
|
+
manifest: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}/manifest` }),
|
|
2523
|
+
bundle: async (idOrAlias, opts = {}) => {
|
|
2524
|
+
const [record, m] = await Promise.all([
|
|
2525
|
+
this.snapshots.get(idOrAlias).catch(() => undefined),
|
|
2526
|
+
this.snapshots.manifest(idOrAlias),
|
|
2527
|
+
]);
|
|
2528
|
+
if (!m?.manifest || typeof m.manifest !== "object") {
|
|
2529
|
+
throw new NotFoundError(`snapshot ${idOrAlias} has no manifest to export`, "Only a verified snapshot has one: check 'promigence snapshot inspect' first.");
|
|
2530
|
+
}
|
|
2531
|
+
const alias = opts.alias ?? record?.alias ?? undefined;
|
|
2532
|
+
const bundle = buildBundle(m.manifest, { ...(alias ? { alias } : {}) });
|
|
2533
|
+
return { bundle, served_by: "client", ...(record ? { record } : {}) };
|
|
2534
|
+
},
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
const POLL_LADDER_MS = [10, 25, 25, 25, 25, 25, 25, 25, 25, 50, 50, 50, 100, 100, 100, 200];
|
|
2538
|
+
function pollInterval(iteration) {
|
|
2539
|
+
const step = POLL_LADDER_MS[iteration];
|
|
2540
|
+
if (step !== undefined)
|
|
2541
|
+
return step;
|
|
2542
|
+
if (iteration < 30)
|
|
2543
|
+
return 500;
|
|
2544
|
+
return 1000;
|
|
2545
|
+
}
|
|
2546
|
+
function sliceOutput(e, sandboxId, runId, range) {
|
|
2547
|
+
const enc = new TextEncoder();
|
|
2548
|
+
const dec = new TextDecoder();
|
|
2549
|
+
const cut = (text) => {
|
|
2550
|
+
const all = enc.encode(text);
|
|
2551
|
+
if (!range)
|
|
2552
|
+
return { text, total: all.length, bytes: all.length };
|
|
2553
|
+
const from = Math.max(0, Math.min(range.from ?? 0, all.length));
|
|
2554
|
+
const to = Math.min(all.length - 1, range.to ?? all.length - 1);
|
|
2555
|
+
const part = to < from ? new Uint8Array(0) : all.subarray(from, to + 1);
|
|
2556
|
+
return { text: dec.decode(part), total: all.length, bytes: part.length };
|
|
2557
|
+
};
|
|
2558
|
+
const out = cut(e.stdout ?? "");
|
|
2559
|
+
const err = cut(e.stderr ?? "");
|
|
2560
|
+
return {
|
|
2561
|
+
exec_id: e.exec_id,
|
|
2562
|
+
...(sandboxId ?? e.sandbox_id ? { sandbox_id: (sandboxId ?? e.sandbox_id) } : {}),
|
|
2563
|
+
...(runId ?? e.run_id ? { run_id: (runId ?? e.run_id) } : {}),
|
|
2564
|
+
...(e.argv ? { argv: e.argv } : {}),
|
|
2565
|
+
exit_code: e.exit_code ?? null,
|
|
2566
|
+
stdout: out.text,
|
|
2567
|
+
stderr: err.text,
|
|
2568
|
+
truncated: e.truncated ?? false,
|
|
2569
|
+
bytes: out.bytes + err.bytes,
|
|
2570
|
+
total_bytes: out.total + err.total,
|
|
2571
|
+
...(range ? { range: { from: range.from ?? 0, to: range.to ?? Math.max(0, out.total - 1) } } : {}),
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
export function toCommandResult(r) {
|
|
2575
|
+
const signal = r["signal"];
|
|
2576
|
+
return {
|
|
2577
|
+
exitCode: r.exit_code ?? (typeof signal === "number" && signal > 0 ? 128 + signal : -1),
|
|
2578
|
+
stdout: r.stdout ?? "",
|
|
2579
|
+
stderr: r.stderr ?? "",
|
|
2580
|
+
...(r.truncated === undefined ? {} : { truncated: r.truncated }),
|
|
2581
|
+
...(r.exec_id === undefined ? {} : { execId: r.exec_id }),
|
|
2582
|
+
};
|
|
2583
|
+
}
|