@promigence/sdk 0.1.0-beta.14 → 0.1.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/caller.js +1 -1
- package/dist/cjs/caller.js +1 -1
- package/dist/cjs/client.d.ts +134 -23
- package/dist/cjs/client.js +202 -80
- package/dist/cjs/command-errors.d.ts +1 -0
- package/dist/cjs/command-errors.js +2 -0
- package/dist/cjs/context.d.ts +19 -4
- package/dist/cjs/context.js +77 -5
- package/dist/cjs/core.d.ts +32 -4
- package/dist/cjs/core.js +30 -5
- package/dist/cjs/http2.d.ts +8 -0
- package/dist/cjs/http2.js +22 -1
- package/dist/cjs/models.gen.d.ts +663 -43
- package/dist/cjs/pty.d.ts +11 -14
- package/dist/cjs/pty.js +15 -12
- package/dist/cjs/sandbox.d.ts +26 -8
- package/dist/cjs/sandbox.js +28 -4
- package/dist/cjs/transport-worker.js +10 -1
- package/dist/client.d.ts +134 -23
- package/dist/client.js +201 -77
- package/dist/command-errors.d.ts +1 -0
- package/dist/command-errors.js +3 -1
- package/dist/context.d.ts +19 -4
- package/dist/context.js +74 -4
- package/dist/core.d.ts +32 -4
- package/dist/core.js +26 -4
- package/dist/http2.d.ts +8 -0
- package/dist/http2.js +22 -1
- package/dist/models.gen.d.ts +663 -43
- package/dist/pty.d.ts +11 -14
- package/dist/pty.js +13 -11
- package/dist/sandbox.d.ts +26 -8
- package/dist/sandbox.js +28 -4
- package/dist/transport-worker.js +10 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { inspect } from "node:util";
|
|
4
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,
|
|
5
|
+
import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, SANDBOX_ENDED_FIX, SandboxEndedError, errorsDocsFor, exitForCode, previewsOff, sandboxEndedIn, withPreviewsFix, } from "./core.js";
|
|
6
6
|
import { buildBundle } from "./bundle.js";
|
|
7
7
|
import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
|
|
8
8
|
import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
|
|
@@ -42,6 +42,26 @@ function bytesToB64(b) {
|
|
|
42
42
|
function modeParam(mode) {
|
|
43
43
|
return typeof mode === "number" ? mode.toString(8) : mode;
|
|
44
44
|
}
|
|
45
|
+
function jsonBody(value) {
|
|
46
|
+
return { data: new TextEncoder().encode(JSON.stringify(value)), contentType: "application/json" };
|
|
47
|
+
}
|
|
48
|
+
function fileForm(fields, data) {
|
|
49
|
+
const boundary = `promigence-${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
50
|
+
let head = "";
|
|
51
|
+
for (const [name, value] of fields) {
|
|
52
|
+
head += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
|
53
|
+
}
|
|
54
|
+
head += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\n`
|
|
55
|
+
+ "Content-Type: application/octet-stream\r\n\r\n";
|
|
56
|
+
const enc = new TextEncoder();
|
|
57
|
+
const pre = enc.encode(head);
|
|
58
|
+
const post = enc.encode(`\r\n--${boundary}--\r\n`);
|
|
59
|
+
const out = new Uint8Array(pre.length + data.length + post.length);
|
|
60
|
+
out.set(pre, 0);
|
|
61
|
+
out.set(data, pre.length);
|
|
62
|
+
out.set(post, pre.length + data.length);
|
|
63
|
+
return { data: out, contentType: `multipart/form-data; boundary=${boundary}` };
|
|
64
|
+
}
|
|
45
65
|
export function execOutputBytes(r, stream = "stdout") {
|
|
46
66
|
const b64 = r[`${stream}_b64`];
|
|
47
67
|
if (typeof b64 === "string")
|
|
@@ -71,6 +91,32 @@ function pageQuery(limit, max, cursor) {
|
|
|
71
91
|
q.set("cursor", cursor);
|
|
72
92
|
return q.toString();
|
|
73
93
|
}
|
|
94
|
+
export const OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
95
|
+
function projectUpdateBody(u) {
|
|
96
|
+
const body = {};
|
|
97
|
+
if (u.name !== undefined)
|
|
98
|
+
body["name"] = u.name;
|
|
99
|
+
if (u.outputRetentionDays !== undefined) {
|
|
100
|
+
const d = u.outputRetentionDays;
|
|
101
|
+
if (d !== null && (!Number.isInteger(d) || d < 1 || d > OUTPUT_RETENTION_MAX_DAYS)) {
|
|
102
|
+
throw new InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, "Pass null for the organization's setting.");
|
|
103
|
+
}
|
|
104
|
+
body["output_retention_days"] = d;
|
|
105
|
+
}
|
|
106
|
+
if (u.failureCaptures !== undefined) {
|
|
107
|
+
if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
|
|
108
|
+
throw new InvalidArgumentError("failureCaptures must be true, false or null", "Pass null for the organization's setting.");
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(body).length) {
|
|
111
|
+
throw new InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
|
|
112
|
+
}
|
|
113
|
+
body["failure_captures"] = u.failureCaptures;
|
|
114
|
+
}
|
|
115
|
+
if (!Object.keys(body).length) {
|
|
116
|
+
throw new InvalidArgumentError("projects.update needs something to change", "Pass name, outputRetentionDays or failureCaptures.");
|
|
117
|
+
}
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
74
120
|
function expiryFields(o) {
|
|
75
121
|
const body = {};
|
|
76
122
|
if (o.expiresInDays !== undefined)
|
|
@@ -230,30 +276,6 @@ export function readCredentials(env = process.env) {
|
|
|
230
276
|
function storedName(store, env) {
|
|
231
277
|
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : storeLabel(store);
|
|
232
278
|
}
|
|
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
279
|
export function keyOrigin(env = process.env) {
|
|
258
280
|
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
259
281
|
return "PROMIGENCE_API_KEY";
|
|
@@ -280,12 +302,13 @@ function resolveCredentials(opts, env = process.env) {
|
|
|
280
302
|
const inputs = optKey === undefined && envKey === undefined
|
|
281
303
|
? undefined
|
|
282
304
|
: `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
|
|
283
|
-
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
|
|
284
|
-
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
|
|
305
|
+
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
|
|
306
|
+
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
|
|
307
|
+
}
|
|
285
308
|
const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
|
|
286
309
|
if (inputs !== undefined && worked.refusal === undefined) {
|
|
287
310
|
credsLastInputs = inputs;
|
|
288
|
-
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
|
|
311
|
+
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
|
|
289
312
|
}
|
|
290
313
|
return worked;
|
|
291
314
|
}
|
|
@@ -319,8 +342,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
|
|
|
319
342
|
if (refusal)
|
|
320
343
|
return { apiKey: undefined, apiUrl, refusal };
|
|
321
344
|
}
|
|
322
|
-
|
|
345
|
+
const origin = !key ? undefined
|
|
346
|
+
: source === "opts" ? { source: "option" }
|
|
347
|
+
: source === "env" ? { source: "env" }
|
|
348
|
+
: source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
|
|
349
|
+
: { source: "fallback" };
|
|
350
|
+
return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
|
|
323
351
|
}
|
|
352
|
+
export const NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
|
|
353
|
+
export const NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
|
|
324
354
|
function keyRefusal(key, from) {
|
|
325
355
|
if (/^[\x21-\x7E]+$/.test(key))
|
|
326
356
|
return undefined;
|
|
@@ -600,6 +630,8 @@ export function snapshotCreateBody(opts) {
|
|
|
600
630
|
}
|
|
601
631
|
if (o.alias)
|
|
602
632
|
body["alias"] = o.alias;
|
|
633
|
+
if (o.alias && o.moveAlias)
|
|
634
|
+
body["move_alias"] = true;
|
|
603
635
|
if (o.grade) {
|
|
604
636
|
if (!o.grade.cmd)
|
|
605
637
|
throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
@@ -634,6 +666,9 @@ const REFUSED_503_CODES = new Set([
|
|
|
634
666
|
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
635
667
|
"secrets_unavailable",
|
|
636
668
|
]);
|
|
669
|
+
const SDK_FIXES = {
|
|
670
|
+
cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
|
|
671
|
+
};
|
|
637
672
|
function withoutCliHelp(fix) {
|
|
638
673
|
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
639
674
|
if (out === fix.trim())
|
|
@@ -643,7 +678,7 @@ function withoutCliHelp(fix) {
|
|
|
643
678
|
export function errorFor(code, message, fix, extra = {}) {
|
|
644
679
|
const attach = (e) => {
|
|
645
680
|
const w = e;
|
|
646
|
-
w.docs = extra.docs
|
|
681
|
+
w.docs = extra.docs !== undefined && e.code === code ? extra.docs : errorsDocsFor(e.code);
|
|
647
682
|
if (extra.status !== undefined)
|
|
648
683
|
w.status = extra.status;
|
|
649
684
|
if (extra.requestId !== undefined)
|
|
@@ -659,6 +694,8 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
659
694
|
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && previewsOff(message)) {
|
|
660
695
|
return attach(new PreviewsUnavailableError());
|
|
661
696
|
}
|
|
697
|
+
if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
|
|
698
|
+
fix = SDK_FIXES[code];
|
|
662
699
|
switch (code) {
|
|
663
700
|
case "auth": return attach(new AuthError(message, fix));
|
|
664
701
|
case "not_found": return attach(new NotFoundError(message, fix));
|
|
@@ -672,8 +709,13 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
672
709
|
case "not_verified": return attach(new NotVerifiedError(message, fix));
|
|
673
710
|
case "timeout": return attach(new TimeoutError(message, fix));
|
|
674
711
|
case "usage":
|
|
675
|
-
case "invalid_argument":
|
|
712
|
+
case "invalid_argument": {
|
|
713
|
+
const ended = code === "usage" ? sandboxEndedIn(message) : undefined;
|
|
714
|
+
if (ended !== undefined) {
|
|
715
|
+
return attach(new SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : SANDBOX_ENDED_FIX));
|
|
716
|
+
}
|
|
676
717
|
return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
718
|
+
}
|
|
677
719
|
case "not_released": return attach(new NotReleasedError(message));
|
|
678
720
|
case "service_restart": return attach(new ServiceRestartError(message, fix));
|
|
679
721
|
default:
|
|
@@ -744,6 +786,21 @@ const WAIT_MS_CAP = 30_000;
|
|
|
744
786
|
export const LONG_POLL_MS = 5_000;
|
|
745
787
|
const BUILD_POLL_MS = 10_000;
|
|
746
788
|
const PROGRESS_POLL_MS = 200;
|
|
789
|
+
export const RUN_SETTLE_MS = 5_000;
|
|
790
|
+
export function runWorkDone(run, endedSince, now = Date.now()) {
|
|
791
|
+
if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
|
|
792
|
+
return true;
|
|
793
|
+
const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
|
|
794
|
+
const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
795
|
+
if (!allDone) {
|
|
796
|
+
delete endedSince.at;
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
if (sbs.some((s) => s.state !== "ended"))
|
|
800
|
+
return true;
|
|
801
|
+
endedSince.at ??= now;
|
|
802
|
+
return now - endedSince.at >= RUN_SETTLE_MS;
|
|
803
|
+
}
|
|
747
804
|
const WAITED_HEADER = "promigence-waited-ms";
|
|
748
805
|
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
749
806
|
function waitQuery(waitMs) {
|
|
@@ -781,6 +838,9 @@ export class PromigenceClient {
|
|
|
781
838
|
apiUrl;
|
|
782
839
|
#apiKey;
|
|
783
840
|
#keyRefusal;
|
|
841
|
+
#keyOrigin;
|
|
842
|
+
#onKeyUse;
|
|
843
|
+
#keyUseSaid = false;
|
|
784
844
|
#bearer;
|
|
785
845
|
fetchImpl;
|
|
786
846
|
ownFetch;
|
|
@@ -802,6 +862,8 @@ export class PromigenceClient {
|
|
|
802
862
|
const resolved = resolveCredentials(opts);
|
|
803
863
|
this.#apiKey = resolved.apiKey;
|
|
804
864
|
this.#keyRefusal = resolved.refusal;
|
|
865
|
+
this.#keyOrigin = resolved.origin;
|
|
866
|
+
this.#onKeyUse = opts.onKeyUse;
|
|
805
867
|
this.apiUrl = resolved.apiUrl;
|
|
806
868
|
const env = opts.fetch === undefined ? transportEnv() : undefined;
|
|
807
869
|
const proxy = env !== undefined ? proxyFor(this.apiUrl, env) : undefined;
|
|
@@ -867,11 +929,18 @@ export class PromigenceClient {
|
|
|
867
929
|
catch {
|
|
868
930
|
}
|
|
869
931
|
}
|
|
932
|
+
get keyOrigin() {
|
|
933
|
+
return this.#keyOrigin;
|
|
934
|
+
}
|
|
870
935
|
requireKey() {
|
|
871
936
|
if (this.#keyRefusal)
|
|
872
937
|
throw this.#keyRefusal;
|
|
873
938
|
if (!this.#apiKey)
|
|
874
|
-
throw new AuthError();
|
|
939
|
+
throw this.cliErrors ? new AuthError(NO_KEY_MESSAGE) : new AuthError(NO_KEY_MESSAGE, NO_KEY_FIX);
|
|
940
|
+
if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
|
|
941
|
+
this.#keyUseSaid = true;
|
|
942
|
+
this.#onKeyUse(this.#keyOrigin);
|
|
943
|
+
}
|
|
875
944
|
return this.#apiKey;
|
|
876
945
|
}
|
|
877
946
|
bearer() {
|
|
@@ -944,7 +1013,8 @@ export class PromigenceClient {
|
|
|
944
1013
|
const idempotencyKey = opts.idempotencyKey ?? (keyedCreate ? newIdempotencyKey() : undefined);
|
|
945
1014
|
if (idempotencyKey)
|
|
946
1015
|
headers["idempotency-key"] = idempotencyKey;
|
|
947
|
-
const replayable = opts.method === "GET" || opts.method === "DELETE" ||
|
|
1016
|
+
const replayable = opts.method === "GET" || opts.method === "DELETE" || opts.replayable === true
|
|
1017
|
+
|| (keyedCreate && Boolean(idempotencyKey));
|
|
948
1018
|
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
949
1019
|
let dispatcher;
|
|
950
1020
|
let resentDelete = false;
|
|
@@ -1141,7 +1211,7 @@ export class PromigenceClient {
|
|
|
1141
1211
|
}
|
|
1142
1212
|
}
|
|
1143
1213
|
}
|
|
1144
|
-
async bytes(method, path, body, opts = {}) {
|
|
1214
|
+
async bytes(method, path, body, opts = {}, read = method === "GET") {
|
|
1145
1215
|
const url = `${this.apiUrl}${path}`;
|
|
1146
1216
|
const headers = {
|
|
1147
1217
|
accept: "application/octet-stream, application/json",
|
|
@@ -1151,7 +1221,7 @@ export class PromigenceClient {
|
|
|
1151
1221
|
if (this.caller)
|
|
1152
1222
|
headers[CALLER_HEADER] = this.caller;
|
|
1153
1223
|
if (body !== undefined)
|
|
1154
|
-
headers["content-type"] =
|
|
1224
|
+
headers["content-type"] = body.contentType;
|
|
1155
1225
|
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1156
1226
|
if (opts.signal)
|
|
1157
1227
|
signals.push(opts.signal);
|
|
@@ -1165,7 +1235,7 @@ export class PromigenceClient {
|
|
|
1165
1235
|
method,
|
|
1166
1236
|
headers,
|
|
1167
1237
|
signal,
|
|
1168
|
-
...(body === undefined ? {} : { body: body }),
|
|
1238
|
+
...(body === undefined ? {} : { body: body.data }),
|
|
1169
1239
|
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1170
1240
|
});
|
|
1171
1241
|
}
|
|
@@ -1185,21 +1255,23 @@ export class PromigenceClient {
|
|
|
1185
1255
|
}
|
|
1186
1256
|
catch { }
|
|
1187
1257
|
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
|
|
1188
|
-
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt,
|
|
1258
|
+
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, read, opts.signal);
|
|
1189
1259
|
if (wait !== undefined) {
|
|
1190
1260
|
this.stats.retries++;
|
|
1191
1261
|
await sleep(wait);
|
|
1192
1262
|
continue;
|
|
1193
1263
|
}
|
|
1194
1264
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1265
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1195
1266
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1196
1267
|
status: res.status,
|
|
1197
1268
|
...bodyExtra(payload),
|
|
1198
1269
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1270
|
+
...(rid ? { requestId: rid } : {}),
|
|
1199
1271
|
});
|
|
1200
1272
|
}
|
|
1201
1273
|
}
|
|
1202
|
-
async stream(path, onChunk, opts = {}) {
|
|
1274
|
+
async stream(method, path, onChunk, opts = {}, body) {
|
|
1203
1275
|
const url = `${this.apiUrl}${path}`;
|
|
1204
1276
|
const headers = {
|
|
1205
1277
|
accept: "application/octet-stream, application/json",
|
|
@@ -1208,17 +1280,25 @@ export class PromigenceClient {
|
|
|
1208
1280
|
};
|
|
1209
1281
|
if (this.caller)
|
|
1210
1282
|
headers[CALLER_HEADER] = this.caller;
|
|
1283
|
+
if (body !== undefined)
|
|
1284
|
+
headers["content-type"] = body.contentType;
|
|
1211
1285
|
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1212
1286
|
if (opts.signal)
|
|
1213
1287
|
signals.push(opts.signal);
|
|
1214
1288
|
const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1215
|
-
const timedOut = () => new TimeoutError(
|
|
1289
|
+
const timedOut = () => new TimeoutError(`${method} ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs.");
|
|
1216
1290
|
const dispatcher = this.guardDispatcher();
|
|
1217
1291
|
for (let attempt = 0;; attempt++) {
|
|
1218
1292
|
let res;
|
|
1219
1293
|
this.stats.requests++;
|
|
1220
1294
|
try {
|
|
1221
|
-
res = await this.fetchImpl(url, {
|
|
1295
|
+
res = await this.fetchImpl(url, {
|
|
1296
|
+
method,
|
|
1297
|
+
headers,
|
|
1298
|
+
signal,
|
|
1299
|
+
...(body === undefined ? {} : { body: body.data }),
|
|
1300
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1301
|
+
});
|
|
1222
1302
|
}
|
|
1223
1303
|
catch (err) {
|
|
1224
1304
|
const e = err;
|
|
@@ -1267,10 +1347,12 @@ export class PromigenceClient {
|
|
|
1267
1347
|
continue;
|
|
1268
1348
|
}
|
|
1269
1349
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1270
|
-
|
|
1350
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1351
|
+
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1271
1352
|
status: res.status,
|
|
1272
1353
|
...bodyExtra(payload),
|
|
1273
1354
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1355
|
+
...(rid ? { requestId: rid } : {}),
|
|
1274
1356
|
});
|
|
1275
1357
|
}
|
|
1276
1358
|
}
|
|
@@ -1346,6 +1428,29 @@ export class PromigenceClient {
|
|
|
1346
1428
|
const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
|
|
1347
1429
|
return pageOf(r, "entries", limit);
|
|
1348
1430
|
},
|
|
1431
|
+
holds: () => this.request({ method: "GET", path: "/billing/holds" }),
|
|
1432
|
+
buyCredits: (amountUsd) => this.request({ method: "POST", path: "/billing/credits", body: { amount_usd: amountUsd } }),
|
|
1433
|
+
settleCredits: (sessionId) => {
|
|
1434
|
+
if (!sessionId?.trim())
|
|
1435
|
+
throw new InvalidArgumentError("billing.settleCredits needs a session id", "Pass the session_id buyCredits() answered.");
|
|
1436
|
+
return this.request({ method: "POST", path: `/billing/credits/${encodeURIComponent(sessionId)}/settle`, body: {} });
|
|
1437
|
+
},
|
|
1438
|
+
autoReload: () => this.request({ method: "GET", path: "/billing/auto-reload" }),
|
|
1439
|
+
setAutoReload: (opts) => this.request({
|
|
1440
|
+
method: "PUT",
|
|
1441
|
+
path: "/billing/auto-reload",
|
|
1442
|
+
body: {
|
|
1443
|
+
on: opts.on,
|
|
1444
|
+
...(opts.thresholdUsd === undefined ? {} : { threshold_usd: opts.thresholdUsd }),
|
|
1445
|
+
...(opts.amountUsd === undefined ? {} : { amount_usd: opts.amountUsd }),
|
|
1446
|
+
...(opts.authorizationVersion === undefined ? {} : { authorization_version: opts.authorizationVersion }),
|
|
1447
|
+
},
|
|
1448
|
+
}),
|
|
1449
|
+
repayDispute: (disputeId) => {
|
|
1450
|
+
if (!disputeId?.trim())
|
|
1451
|
+
throw new InvalidArgumentError("billing.repayDispute needs a dispute id", "Pass the hold's dispute_id from billing.usage().");
|
|
1452
|
+
return this.request({ method: "POST", path: `/billing/disputes/${encodeURIComponent(disputeId)}/repay`, body: {} });
|
|
1453
|
+
},
|
|
1349
1454
|
checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
|
|
1350
1455
|
portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
|
|
1351
1456
|
referrals: () => this.request({ method: "GET", path: "/referrals" }),
|
|
@@ -1404,6 +1509,9 @@ export class PromigenceClient {
|
|
|
1404
1509
|
rename: (org, project, name) => this.request({
|
|
1405
1510
|
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
|
|
1406
1511
|
}),
|
|
1512
|
+
update: async (org, project, update) => this.request({
|
|
1513
|
+
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
|
|
1514
|
+
}),
|
|
1407
1515
|
},
|
|
1408
1516
|
};
|
|
1409
1517
|
}
|
|
@@ -1620,12 +1728,8 @@ export class PromigenceClient {
|
|
|
1620
1728
|
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1621
1729
|
},
|
|
1622
1730
|
waitDone: async (runId, opts = {}) => {
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1626
|
-
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1627
|
-
return terminal || allDone;
|
|
1628
|
-
});
|
|
1731
|
+
const endedSince = {};
|
|
1732
|
+
const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
|
|
1629
1733
|
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1630
1734
|
},
|
|
1631
1735
|
};
|
|
@@ -2034,10 +2138,19 @@ export class PromigenceClient {
|
|
|
2034
2138
|
}), sandboxId);
|
|
2035
2139
|
return r.ports ?? [];
|
|
2036
2140
|
},
|
|
2037
|
-
revoke: (sandboxId, tokenOrPort) =>
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2141
|
+
revoke: (sandboxId, tokenOrPort) => {
|
|
2142
|
+
const which = String(tokenOrPort).trim();
|
|
2143
|
+
return /^[0-9]+$/.test(which)
|
|
2144
|
+
? this.request({
|
|
2145
|
+
method: "DELETE",
|
|
2146
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
|
|
2147
|
+
})
|
|
2148
|
+
: this.request({
|
|
2149
|
+
method: "POST",
|
|
2150
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
|
|
2151
|
+
body: { token: which },
|
|
2152
|
+
});
|
|
2153
|
+
},
|
|
2041
2154
|
},
|
|
2042
2155
|
ssh: {
|
|
2043
2156
|
open: (sandboxId, opts) => {
|
|
@@ -2152,6 +2265,7 @@ export class PromigenceClient {
|
|
|
2152
2265
|
...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
|
|
2153
2266
|
},
|
|
2154
2267
|
}),
|
|
2268
|
+
resumePausedForCredits: () => this.request({ method: "POST", path: "/sandboxes/resume", body: { paused_for: "credits" } }),
|
|
2155
2269
|
snapshot: (sandboxId, opts = {}) => this.request({
|
|
2156
2270
|
method: "POST",
|
|
2157
2271
|
path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
|
|
@@ -2222,38 +2336,42 @@ export class PromigenceClient {
|
|
|
2222
2336
|
},
|
|
2223
2337
|
files: {
|
|
2224
2338
|
list: (sandboxId, path = "/", opts = {}) => {
|
|
2225
|
-
const
|
|
2339
|
+
const body = { path };
|
|
2226
2340
|
if (opts.recursive)
|
|
2227
|
-
|
|
2341
|
+
body.recursive = true;
|
|
2228
2342
|
if (opts.depth !== undefined)
|
|
2229
|
-
|
|
2343
|
+
body.depth = Math.max(0, Math.floor(opts.depth));
|
|
2230
2344
|
if (opts.limit !== undefined)
|
|
2231
|
-
|
|
2345
|
+
body.limit = Math.max(1, Math.floor(opts.limit));
|
|
2232
2346
|
if (opts.hidden === false)
|
|
2233
|
-
|
|
2347
|
+
body.hidden = false;
|
|
2234
2348
|
return this.request({
|
|
2235
|
-
method: "
|
|
2236
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list
|
|
2349
|
+
method: "POST",
|
|
2350
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list`,
|
|
2351
|
+
body,
|
|
2352
|
+
replayable: true,
|
|
2237
2353
|
});
|
|
2238
2354
|
},
|
|
2239
2355
|
search: (sandboxId, path, opts = {}) => {
|
|
2240
2356
|
if (!opts.pattern && !opts.contains) {
|
|
2241
2357
|
throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
|
|
2242
2358
|
}
|
|
2243
|
-
const
|
|
2359
|
+
const body = { path };
|
|
2244
2360
|
if (opts.pattern)
|
|
2245
|
-
|
|
2361
|
+
body.pattern = opts.pattern;
|
|
2246
2362
|
if (opts.contains)
|
|
2247
|
-
|
|
2363
|
+
body.contains = opts.contains;
|
|
2248
2364
|
if (opts.ignoreCase)
|
|
2249
|
-
|
|
2365
|
+
body.ignore_case = true;
|
|
2250
2366
|
if (opts.limit !== undefined)
|
|
2251
|
-
|
|
2367
|
+
body.limit = Math.max(1, Math.floor(opts.limit));
|
|
2252
2368
|
if (opts.hidden === false)
|
|
2253
|
-
|
|
2369
|
+
body.hidden = false;
|
|
2254
2370
|
return this.request({
|
|
2255
|
-
method: "
|
|
2256
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search
|
|
2371
|
+
method: "POST",
|
|
2372
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search`,
|
|
2373
|
+
body,
|
|
2374
|
+
replayable: true,
|
|
2257
2375
|
});
|
|
2258
2376
|
},
|
|
2259
2377
|
mkdir: (sandboxId, path, opts = {}) => this.request({
|
|
@@ -2310,11 +2428,13 @@ export class PromigenceClient {
|
|
|
2310
2428
|
body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
|
|
2311
2429
|
}),
|
|
2312
2430
|
stat: (sandboxId, path) => this.request({
|
|
2313
|
-
method: "
|
|
2314
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat
|
|
2431
|
+
method: "POST",
|
|
2432
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat`,
|
|
2433
|
+
body: { path },
|
|
2434
|
+
replayable: true,
|
|
2315
2435
|
}),
|
|
2316
|
-
get: (sandboxId, path, opts = {}) => this.bytes("
|
|
2317
|
-
download: (sandboxId, path, onChunk, opts = {}) => this.stream(`/sandboxes/${encodeURIComponent(sandboxId)}/files
|
|
2436
|
+
get: (sandboxId, path, opts = {}) => this.bytes("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, jsonBody({ path }), opts, true),
|
|
2437
|
+
download: (sandboxId, path, onChunk, opts = {}) => this.stream("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, onChunk, opts, jsonBody({ path })),
|
|
2318
2438
|
text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
|
|
2319
2439
|
put: async (sandboxId, path, data, opts = {}) => {
|
|
2320
2440
|
const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
@@ -2324,8 +2444,7 @@ export class PromigenceClient {
|
|
|
2324
2444
|
}, opts);
|
|
2325
2445
|
},
|
|
2326
2446
|
putFrom: async (sandboxId, path, source, opts = {}) => {
|
|
2327
|
-
const
|
|
2328
|
-
const base = `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}${mode}`;
|
|
2447
|
+
const url = `/sandboxes/${encodeURIComponent(sandboxId)}/files`;
|
|
2329
2448
|
let sent = 0;
|
|
2330
2449
|
let chunks = 0;
|
|
2331
2450
|
let last;
|
|
@@ -2334,7 +2453,12 @@ export class PromigenceClient {
|
|
|
2334
2453
|
if (slice.length === 0 && sent < source.size) {
|
|
2335
2454
|
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.");
|
|
2336
2455
|
}
|
|
2337
|
-
const
|
|
2456
|
+
const fields = [["path", path]];
|
|
2457
|
+
if (opts.mode !== undefined)
|
|
2458
|
+
fields.push(["mode", opts.mode.toString(8)]);
|
|
2459
|
+
if (sent > 0)
|
|
2460
|
+
fields.push(["offset", String(sent)]);
|
|
2461
|
+
const raw = await this.bytes("PUT", url, fileForm(fields, slice), opts);
|
|
2338
2462
|
last = JSON.parse(new TextDecoder().decode(raw));
|
|
2339
2463
|
sent += slice.length;
|
|
2340
2464
|
chunks += 1;
|
|
@@ -2343,8 +2467,8 @@ export class PromigenceClient {
|
|
|
2343
2467
|
},
|
|
2344
2468
|
delete: (sandboxId, path, opts = {}) => this.request({
|
|
2345
2469
|
method: "DELETE",
|
|
2346
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files
|
|
2347
|
-
|
|
2470
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files`,
|
|
2471
|
+
body: { path, ...(opts.recursive ? { recursive: true } : {}) },
|
|
2348
2472
|
}),
|
|
2349
2473
|
},
|
|
2350
2474
|
output: async (execId, opts = {}) => {
|
|
@@ -2523,10 +2647,10 @@ export class PromigenceClient {
|
|
|
2523
2647
|
throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2524
2648
|
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2525
2649
|
},
|
|
2526
|
-
alias: (idOrAlias, alias) => this.request({
|
|
2650
|
+
alias: (idOrAlias, alias, opts = {}) => this.request({
|
|
2527
2651
|
method: "PUT",
|
|
2528
2652
|
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2529
|
-
body: { alias },
|
|
2653
|
+
body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
|
|
2530
2654
|
}),
|
|
2531
2655
|
waitBuild: async (buildId, opts = {}) => {
|
|
2532
2656
|
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");
|
package/dist/command-errors.d.ts
CHANGED
package/dist/command-errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CommandExitError } from "./core.js";
|
|
1
|
+
import { CommandExitError, errorsDocsFor } from "./core.js";
|
|
2
2
|
import { hintArg } from "./hint.js";
|
|
3
3
|
export function secondsText(ms) {
|
|
4
4
|
const s = ms / 1000;
|
|
@@ -10,9 +10,11 @@ export class CommandTimeoutError extends CommandExitError {
|
|
|
10
10
|
code;
|
|
11
11
|
fix;
|
|
12
12
|
guide;
|
|
13
|
+
docs;
|
|
13
14
|
constructor(result, timeoutMs) {
|
|
14
15
|
super(result);
|
|
15
16
|
this.code = "task_timeout";
|
|
17
|
+
this.docs = errorsDocsFor(this.code);
|
|
16
18
|
this.timeoutMs = timeoutMs;
|
|
17
19
|
this.message = timeoutMs === undefined
|
|
18
20
|
? `command timed out (killed at its timeout, exit ${result.exitCode})`
|
package/dist/context.d.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* - Empty directories and permission bits travel as they are on disk; a set-user-ID or
|
|
16
16
|
* set-group-ID bit is cleared, with a note.
|
|
17
17
|
* - Files that look like credentials (`.env`, private keys, cloud credentials, ...) are refused,
|
|
18
|
-
* by name, unless `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* by name, unless `allowSecretFiles` names them (each one) or `allowSecrets` says otherwise. The
|
|
19
|
+
* check goes by file name and reads only two kinds of file, to clear the plain false
|
|
20
|
+
* positives: a `.pem` holding certificates or public keys and no private key, and an `.npmrc`
|
|
21
|
+
* with no token or password in it. It is best-effort, so look over the list (`listContext`)
|
|
22
|
+
* before you send a directory.
|
|
21
23
|
* - Symbolic links travel as links, never as the file they point at.
|
|
22
24
|
*/
|
|
23
25
|
/** The prefix a packed directory travels under in `dockerfile.context`. */
|
|
@@ -65,9 +67,17 @@ export declare function dockerignoreMatcher(text: string): {
|
|
|
65
67
|
/**
|
|
66
68
|
* Does this path look like a credential (`.env`, a private key, cloud or registry credentials, a
|
|
67
69
|
* token file, ...)? By name only: best-effort, it cannot see what a file holds. Templates such as
|
|
68
|
-
* `.env.example` are not.
|
|
70
|
+
* `.env.example` are not. [`secretLikeFile`] also reads the few files whose contents can clear them.
|
|
69
71
|
*/
|
|
70
72
|
export declare function secretLike(path: string): boolean;
|
|
73
|
+
/** The most of a file [`secretLikeFile`] reads; a larger one is judged by its name alone. */
|
|
74
|
+
export declare const CONTENT_CHECK_MAX_BYTES: number;
|
|
75
|
+
/**
|
|
76
|
+
* [`secretLike`] for a file on disk at `abs` (`rel` is its path in the upload): a name that looks
|
|
77
|
+
* like a credential, unless what the file holds clears it ([`clearedByContent`]). A link, a file
|
|
78
|
+
* over [`CONTENT_CHECK_MAX_BYTES`] and a file that cannot be read keep the name's verdict.
|
|
79
|
+
*/
|
|
80
|
+
export declare function secretLikeFile(rel: string, abs: string): boolean;
|
|
71
81
|
/** What a build context is packed from and sent with. */
|
|
72
82
|
export interface ContextOptions {
|
|
73
83
|
/**
|
|
@@ -84,6 +94,11 @@ export interface ContextOptions {
|
|
|
84
94
|
* nothing is sent.
|
|
85
95
|
*/
|
|
86
96
|
allowSecrets?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Send these files that look like credentials, and only these: paths inside the context,
|
|
99
|
+
* `/`-separated, as `listContext` names them. Every other one is still refused.
|
|
100
|
+
*/
|
|
101
|
+
allowSecretFiles?: readonly string[];
|
|
87
102
|
/** Called with each note: what was left out by default, or changed on the way. */
|
|
88
103
|
onNote?: (note: string) => void;
|
|
89
104
|
}
|