@promigence/sdk 0.1.0-beta.15 → 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 +50 -9
- package/dist/cjs/client.js +77 -47
- 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 +28 -5
- package/dist/cjs/http2.d.ts +8 -0
- package/dist/cjs/http2.js +22 -1
- package/dist/cjs/models.gen.d.ts +64 -7
- 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 +50 -9
- package/dist/client.js +76 -44
- 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 +24 -4
- package/dist/http2.d.ts +8 -0
- package/dist/http2.js +22 -1
- package/dist/models.gen.d.ts +64 -7
- 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/caller.js
CHANGED
|
@@ -3,7 +3,7 @@ export const CALLER_ENV = "PROMIGENCE_CALLER";
|
|
|
3
3
|
export const CALLER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
|
|
4
4
|
export const CALLER_VERSION_RE = /^[A-Za-z0-9.+_~-]{0,31}$/;
|
|
5
5
|
export const RESERVED_CALLERS = [
|
|
6
|
-
"cli", "harbor", "inspect", "
|
|
6
|
+
"cli", "harbor", "inspect", "computesdk", "mcp",
|
|
7
7
|
];
|
|
8
8
|
let globalCaller;
|
|
9
9
|
let defaultCaller;
|
package/dist/cjs/caller.js
CHANGED
|
@@ -13,7 +13,7 @@ exports.CALLER_ENV = "PROMIGENCE_CALLER";
|
|
|
13
13
|
exports.CALLER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
|
|
14
14
|
exports.CALLER_VERSION_RE = /^[A-Za-z0-9.+_~-]{0,31}$/;
|
|
15
15
|
exports.RESERVED_CALLERS = [
|
|
16
|
-
"cli", "harbor", "inspect", "
|
|
16
|
+
"cli", "harbor", "inspect", "computesdk", "mcp",
|
|
17
17
|
];
|
|
18
18
|
let globalCaller;
|
|
19
19
|
let defaultCaller;
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { inspect, type InspectOptions } from "node:util";
|
|
3
3
|
import { PromigenceError, type CommandResult, type NetworkMode, type OptimizeFor, type Tier } from "./core.js";
|
|
4
4
|
import { type Bundle } from "./bundle.js";
|
|
5
|
+
import { type CredentialStore } from "./credstore.js";
|
|
5
6
|
import { type PtyExit, type PtySession } from "./pty.js";
|
|
6
7
|
import { type SshTunnel } from "./ssh.js";
|
|
7
8
|
import type * as M from "./models.gen.js";
|
|
@@ -280,6 +281,10 @@ export interface ReportSandbox extends QueuedInfo {
|
|
|
280
281
|
sandbox_id: string;
|
|
281
282
|
state: SandboxState;
|
|
282
283
|
attempt?: number;
|
|
284
|
+
/** `PROMIGENCE_INDEX`: its place in the run, from 0. Absent when the run's command set it itself. */
|
|
285
|
+
index?: number;
|
|
286
|
+
/** `PROMIGENCE_SEED`: the number its commands were given. Absent when the run's command set it itself. */
|
|
287
|
+
seed?: number;
|
|
283
288
|
queued_ms?: number;
|
|
284
289
|
ready_ms?: number;
|
|
285
290
|
e2e_ms?: number;
|
|
@@ -319,11 +324,16 @@ export interface RunReport {
|
|
|
319
324
|
snapshot_id?: string;
|
|
320
325
|
alias?: string;
|
|
321
326
|
labels?: Record<string, string>;
|
|
327
|
+
/**
|
|
328
|
+
* What the run asked for. `network` is `"allow"` for a run behind an allowlist, whose hosts are
|
|
329
|
+
* `allow` (a service before it said `"egress"` for one).
|
|
330
|
+
*/
|
|
322
331
|
requested?: {
|
|
323
332
|
count: number;
|
|
324
333
|
timeout_ms: number;
|
|
325
334
|
verify_each: boolean;
|
|
326
|
-
network: NetworkMode;
|
|
335
|
+
network: NetworkMode | "allow";
|
|
336
|
+
allow?: string[];
|
|
327
337
|
allow_partial: boolean;
|
|
328
338
|
secrets?: string[];
|
|
329
339
|
grade?: GradeSpec;
|
|
@@ -437,13 +447,19 @@ export interface SnapshotBuild {
|
|
|
437
447
|
repo?: string;
|
|
438
448
|
commit?: string;
|
|
439
449
|
verification?: Record<string, unknown>;
|
|
440
|
-
/**
|
|
450
|
+
/**
|
|
451
|
+
* A rejected build names the guard and the measured value; nothing was stored. `log_tail` is
|
|
452
|
+
* your build's own output: for a step of yours that failed (its start command, a setup step, its
|
|
453
|
+
* verify), that step's last lines. `fix`, when present, is what to change.
|
|
454
|
+
*/
|
|
441
455
|
error?: {
|
|
442
456
|
code?: string;
|
|
443
457
|
guard?: string;
|
|
444
458
|
message?: string;
|
|
445
459
|
values?: number[];
|
|
446
460
|
stored?: boolean;
|
|
461
|
+
log_tail?: string[];
|
|
462
|
+
fix?: string;
|
|
447
463
|
} | null;
|
|
448
464
|
/** Set when an identical build already existed. */
|
|
449
465
|
deduped?: boolean;
|
|
@@ -1153,8 +1169,9 @@ export interface SandboxPatchResponse {
|
|
|
1153
1169
|
/**
|
|
1154
1170
|
* What the idle rule counts as idle. `"requests"` (the default): nobody has called the sandbox
|
|
1155
1171
|
* for the window and no command, terminal or SSH session is open. `"activity"`: all of that, and
|
|
1156
|
-
* for the whole window
|
|
1157
|
-
* connection to the outside is open — for an agent
|
|
1172
|
+
* for the whole window the sandbox used under 5% of one core, sent and received no network byte
|
|
1173
|
+
* (disk writes do not count), and no network connection to the outside is open — for an agent
|
|
1174
|
+
* that runs inside the sandbox on its own.
|
|
1158
1175
|
*/
|
|
1159
1176
|
export type AutoPauseMode = M.AutoPauseMode;
|
|
1160
1177
|
/** `POST /snapshots/:id/verify` — re-run the recorded `verify` in a fresh fork. */
|
|
@@ -1372,6 +1389,8 @@ export interface PortRecord {
|
|
|
1372
1389
|
/** The same token as a DNS label, for a wildcard-DNS deployment (`<label>.<suffix>`). */
|
|
1373
1390
|
host_url_label?: string;
|
|
1374
1391
|
created_unix_ns?: number;
|
|
1392
|
+
/** When the URL stops working on its own (`ttl_s`); `null` for one that works until revoked. */
|
|
1393
|
+
expires_unix_ns?: number | null;
|
|
1375
1394
|
}
|
|
1376
1395
|
/** A key named without its secret (`/me`'s `key`, the key lists). */
|
|
1377
1396
|
export interface KeyRef {
|
|
@@ -1518,6 +1537,12 @@ export interface PromigenceClientOpts {
|
|
|
1518
1537
|
*/
|
|
1519
1538
|
onTiming?: (timing: Timing) => void;
|
|
1520
1539
|
}
|
|
1540
|
+
/** Where a client's key came from: the option, PROMIGENCE_API_KEY (`env`), the stored sign-in, or a fallback. */
|
|
1541
|
+
export interface KeyOrigin {
|
|
1542
|
+
source: "option" | "env" | "stored" | "fallback";
|
|
1543
|
+
/** The stored sign-in's store, for `stored`. */
|
|
1544
|
+
store?: CredentialStore;
|
|
1545
|
+
}
|
|
1521
1546
|
/** `GET /sandboxes/:id/network`. */
|
|
1522
1547
|
export interface SandboxNetwork {
|
|
1523
1548
|
sandbox_id: string;
|
|
@@ -1756,6 +1781,11 @@ export interface SnapshotCreateOpts {
|
|
|
1756
1781
|
*/
|
|
1757
1782
|
setupUser?: string;
|
|
1758
1783
|
alias?: string;
|
|
1784
|
+
/**
|
|
1785
|
+
* With `alias`: take the name from the snapshot that has it, which stays, unnamed. Without it, an
|
|
1786
|
+
* alias another snapshot has is refused (`usage`, 400) and nothing is built.
|
|
1787
|
+
*/
|
|
1788
|
+
moveAlias?: boolean;
|
|
1759
1789
|
/**
|
|
1760
1790
|
* A DEFAULT grader for this snapshot: a run on it with no `grade` of its own uses this one.
|
|
1761
1791
|
* The same shape as `runs.start({ grade })`.
|
|
@@ -1841,11 +1871,12 @@ export declare function credentialsExposure(path: string): string | undefined;
|
|
|
1841
1871
|
* `credentialsTrustProblem`).
|
|
1842
1872
|
*/
|
|
1843
1873
|
export declare function readCredentials(env?: NodeJS.ProcessEnv): Credentials;
|
|
1844
|
-
export declare function apiUrlFromCompatUrl(url: string | undefined): string | undefined;
|
|
1845
|
-
export declare function apiUrlFromCompatDomain(domain: string | undefined): string | undefined;
|
|
1846
|
-
export declare function compatApiKey(key: string | undefined): string | undefined;
|
|
1847
1874
|
/** Which variable a key came from, for `promigence doctor`. */
|
|
1848
1875
|
export declare function keyOrigin(env?: NodeJS.ProcessEnv): string | undefined;
|
|
1876
|
+
/** A request with no key found anywhere: none passed, none in the environment, none stored. */
|
|
1877
|
+
export declare const NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
|
|
1878
|
+
/** What a caller of this SDK does about it. */
|
|
1879
|
+
export declare const NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
|
|
1849
1880
|
/** `name@sha256:<64 hex>`, optionally with a tag before the `@`: a reference already pinned. */
|
|
1850
1881
|
export declare const IMAGE_DIGEST_RE: RegExp;
|
|
1851
1882
|
/**
|
|
@@ -2510,6 +2541,10 @@ export declare class PromigenceClient {
|
|
|
2510
2541
|
/**
|
|
2511
2542
|
* Preview URLs for ports inside the sandbox. A URL is a capability — anyone holding it reaches
|
|
2512
2543
|
* that port — and dies with `revoke`, with a pause, or with the sandbox.
|
|
2544
|
+
*
|
|
2545
|
+
* If your account's preview URLs aren't on, `expose` throws `PreviewsUnavailableError`; write to
|
|
2546
|
+
* support@promigence.ai to have them turned on, and meanwhile reach the port with
|
|
2547
|
+
* `promigence sandbox ssh <sid> -- -N -L <port>:localhost:<port>`.
|
|
2513
2548
|
*/
|
|
2514
2549
|
ports: {
|
|
2515
2550
|
expose: (sandboxId: string, port: number) => Promise<PortRecord>;
|
|
@@ -3049,8 +3084,14 @@ export declare class PromigenceClient {
|
|
|
3049
3084
|
alias?: string | null;
|
|
3050
3085
|
deleted: boolean;
|
|
3051
3086
|
}>;
|
|
3052
|
-
/**
|
|
3053
|
-
|
|
3087
|
+
/**
|
|
3088
|
+
* `PUT /snapshots/:id/alias` — the sidecar's one mutable field. A name another snapshot
|
|
3089
|
+
* has is refused (`usage`, 400) unless `moveAlias: true`, which takes it; that one stays,
|
|
3090
|
+
* unnamed.
|
|
3091
|
+
*/
|
|
3092
|
+
alias: (idOrAlias: string, alias: string | null, opts?: {
|
|
3093
|
+
moveAlias?: boolean;
|
|
3094
|
+
}) => Promise<SnapshotRecord>;
|
|
3054
3095
|
/**
|
|
3055
3096
|
* Wait for a build to leave `running`, bounded by `waitTimeoutMs` (default 100 s).
|
|
3056
3097
|
*
|
package/dist/cjs/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PromigenceClient = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
|
|
3
|
+
exports.PromigenceClient = exports.RUN_SETTLE_MS = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.NO_KEY_FIX = exports.NO_KEY_MESSAGE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
|
|
4
4
|
exports.execOutputBytes = execOutputBytes;
|
|
5
5
|
exports.configDir = configDir;
|
|
6
6
|
exports.credentialsPath = credentialsPath;
|
|
@@ -10,9 +10,6 @@ exports.storedCredentials = storedCredentials;
|
|
|
10
10
|
exports.storedCredentialsText = storedCredentialsText;
|
|
11
11
|
exports.readCredentialsWithStore = readCredentialsWithStore;
|
|
12
12
|
exports.readCredentials = readCredentials;
|
|
13
|
-
exports.apiUrlFromCompatUrl = apiUrlFromCompatUrl;
|
|
14
|
-
exports.apiUrlFromCompatDomain = apiUrlFromCompatDomain;
|
|
15
|
-
exports.compatApiKey = compatApiKey;
|
|
16
13
|
exports.keyOrigin = keyOrigin;
|
|
17
14
|
exports.imageRefProblem = imageRefProblem;
|
|
18
15
|
exports.checkImageRef = checkImageRef;
|
|
@@ -27,6 +24,7 @@ exports.errorFor = errorFor;
|
|
|
27
24
|
exports.checkSandboxId = checkSandboxId;
|
|
28
25
|
exports.checkSpendCap = checkSpendCap;
|
|
29
26
|
exports.newIdempotencyKey = newIdempotencyKey;
|
|
27
|
+
exports.runWorkDone = runWorkDone;
|
|
30
28
|
exports.serverAppMs = serverAppMs;
|
|
31
29
|
exports.toCommandResult = toCommandResult;
|
|
32
30
|
const node_fs_1 = require("node:fs");
|
|
@@ -309,30 +307,6 @@ function readCredentials(env = process.env) {
|
|
|
309
307
|
function storedName(store, env) {
|
|
310
308
|
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : (0, credstore_js_1.storeLabel)(store);
|
|
311
309
|
}
|
|
312
|
-
const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
|
|
313
|
-
function apiUrlFromCompatUrl(url) {
|
|
314
|
-
const u = (url ?? "").trim();
|
|
315
|
-
if (!u)
|
|
316
|
-
return undefined;
|
|
317
|
-
let host = "";
|
|
318
|
-
try {
|
|
319
|
-
host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
|
|
325
|
-
}
|
|
326
|
-
function apiUrlFromCompatDomain(domain) {
|
|
327
|
-
const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
328
|
-
if (!d || COMPAT_REFUSED_HOSTS.test(d))
|
|
329
|
-
return undefined;
|
|
330
|
-
return `https://api.${d}/v1`;
|
|
331
|
-
}
|
|
332
|
-
function compatApiKey(key) {
|
|
333
|
-
const k = key?.trim();
|
|
334
|
-
return k && !k.startsWith("e2b_") ? k : undefined;
|
|
335
|
-
}
|
|
336
310
|
function keyOrigin(env = process.env) {
|
|
337
311
|
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
338
312
|
return "PROMIGENCE_API_KEY";
|
|
@@ -359,12 +333,13 @@ function resolveCredentials(opts, env = process.env) {
|
|
|
359
333
|
const inputs = optKey === undefined && envKey === undefined
|
|
360
334
|
? undefined
|
|
361
335
|
: `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
|
|
362
|
-
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
|
|
363
|
-
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
|
|
336
|
+
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
|
|
337
|
+
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
|
|
338
|
+
}
|
|
364
339
|
const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
|
|
365
340
|
if (inputs !== undefined && worked.refusal === undefined) {
|
|
366
341
|
credsLastInputs = inputs;
|
|
367
|
-
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
|
|
342
|
+
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
|
|
368
343
|
}
|
|
369
344
|
return worked;
|
|
370
345
|
}
|
|
@@ -398,8 +373,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
|
|
|
398
373
|
if (refusal)
|
|
399
374
|
return { apiKey: undefined, apiUrl, refusal };
|
|
400
375
|
}
|
|
401
|
-
|
|
376
|
+
const origin = !key ? undefined
|
|
377
|
+
: source === "opts" ? { source: "option" }
|
|
378
|
+
: source === "env" ? { source: "env" }
|
|
379
|
+
: source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
|
|
380
|
+
: { source: "fallback" };
|
|
381
|
+
return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
|
|
402
382
|
}
|
|
383
|
+
exports.NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
|
|
384
|
+
exports.NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
|
|
403
385
|
function keyRefusal(key, from) {
|
|
404
386
|
if (/^[\x21-\x7E]+$/.test(key))
|
|
405
387
|
return undefined;
|
|
@@ -679,6 +661,8 @@ function snapshotCreateBody(opts) {
|
|
|
679
661
|
}
|
|
680
662
|
if (o.alias)
|
|
681
663
|
body["alias"] = o.alias;
|
|
664
|
+
if (o.alias && o.moveAlias)
|
|
665
|
+
body["move_alias"] = true;
|
|
682
666
|
if (o.grade) {
|
|
683
667
|
if (!o.grade.cmd)
|
|
684
668
|
throw new core_js_1.InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
@@ -713,6 +697,9 @@ const REFUSED_503_CODES = new Set([
|
|
|
713
697
|
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
714
698
|
"secrets_unavailable",
|
|
715
699
|
]);
|
|
700
|
+
const SDK_FIXES = {
|
|
701
|
+
cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
|
|
702
|
+
};
|
|
716
703
|
function withoutCliHelp(fix) {
|
|
717
704
|
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
718
705
|
if (out === fix.trim())
|
|
@@ -722,7 +709,7 @@ function withoutCliHelp(fix) {
|
|
|
722
709
|
function errorFor(code, message, fix, extra = {}) {
|
|
723
710
|
const attach = (e) => {
|
|
724
711
|
const w = e;
|
|
725
|
-
w.docs = extra.docs
|
|
712
|
+
w.docs = extra.docs !== undefined && e.code === code ? extra.docs : (0, core_js_1.errorsDocsFor)(e.code);
|
|
726
713
|
if (extra.status !== undefined)
|
|
727
714
|
w.status = extra.status;
|
|
728
715
|
if (extra.requestId !== undefined)
|
|
@@ -738,6 +725,8 @@ function errorFor(code, message, fix, extra = {}) {
|
|
|
738
725
|
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && (0, core_js_1.previewsOff)(message)) {
|
|
739
726
|
return attach(new core_js_1.PreviewsUnavailableError());
|
|
740
727
|
}
|
|
728
|
+
if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
|
|
729
|
+
fix = SDK_FIXES[code];
|
|
741
730
|
switch (code) {
|
|
742
731
|
case "auth": return attach(new core_js_1.AuthError(message, fix));
|
|
743
732
|
case "not_found": return attach(new core_js_1.NotFoundError(message, fix));
|
|
@@ -751,8 +740,13 @@ function errorFor(code, message, fix, extra = {}) {
|
|
|
751
740
|
case "not_verified": return attach(new core_js_1.NotVerifiedError(message, fix));
|
|
752
741
|
case "timeout": return attach(new core_js_1.TimeoutError(message, fix));
|
|
753
742
|
case "usage":
|
|
754
|
-
case "invalid_argument":
|
|
743
|
+
case "invalid_argument": {
|
|
744
|
+
const ended = code === "usage" ? (0, core_js_1.sandboxEndedIn)(message) : undefined;
|
|
745
|
+
if (ended !== undefined) {
|
|
746
|
+
return attach(new core_js_1.SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : core_js_1.SANDBOX_ENDED_FIX));
|
|
747
|
+
}
|
|
755
748
|
return attach(new core_js_1.InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
749
|
+
}
|
|
756
750
|
case "not_released": return attach(new core_js_1.NotReleasedError(message));
|
|
757
751
|
case "service_restart": return attach(new core_js_1.ServiceRestartError(message, fix));
|
|
758
752
|
default:
|
|
@@ -822,6 +816,21 @@ const WAIT_MS_CAP = 30_000;
|
|
|
822
816
|
exports.LONG_POLL_MS = 5_000;
|
|
823
817
|
const BUILD_POLL_MS = 10_000;
|
|
824
818
|
const PROGRESS_POLL_MS = 200;
|
|
819
|
+
exports.RUN_SETTLE_MS = 5_000;
|
|
820
|
+
function runWorkDone(run, endedSince, now = Date.now()) {
|
|
821
|
+
if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
|
|
822
|
+
return true;
|
|
823
|
+
const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
|
|
824
|
+
const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
825
|
+
if (!allDone) {
|
|
826
|
+
delete endedSince.at;
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if (sbs.some((s) => s.state !== "ended"))
|
|
830
|
+
return true;
|
|
831
|
+
endedSince.at ??= now;
|
|
832
|
+
return now - endedSince.at >= exports.RUN_SETTLE_MS;
|
|
833
|
+
}
|
|
825
834
|
const WAITED_HEADER = "promigence-waited-ms";
|
|
826
835
|
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
827
836
|
function waitQuery(waitMs) {
|
|
@@ -859,6 +868,9 @@ class PromigenceClient {
|
|
|
859
868
|
apiUrl;
|
|
860
869
|
#apiKey;
|
|
861
870
|
#keyRefusal;
|
|
871
|
+
#keyOrigin;
|
|
872
|
+
#onKeyUse;
|
|
873
|
+
#keyUseSaid = false;
|
|
862
874
|
#bearer;
|
|
863
875
|
fetchImpl;
|
|
864
876
|
ownFetch;
|
|
@@ -880,6 +892,8 @@ class PromigenceClient {
|
|
|
880
892
|
const resolved = resolveCredentials(opts);
|
|
881
893
|
this.#apiKey = resolved.apiKey;
|
|
882
894
|
this.#keyRefusal = resolved.refusal;
|
|
895
|
+
this.#keyOrigin = resolved.origin;
|
|
896
|
+
this.#onKeyUse = opts.onKeyUse;
|
|
883
897
|
this.apiUrl = resolved.apiUrl;
|
|
884
898
|
const env = opts.fetch === undefined ? (0, proxy_js_1.transportEnv)() : undefined;
|
|
885
899
|
const proxy = env !== undefined ? (0, proxy_js_1.proxyFor)(this.apiUrl, env) : undefined;
|
|
@@ -945,11 +959,18 @@ class PromigenceClient {
|
|
|
945
959
|
catch {
|
|
946
960
|
}
|
|
947
961
|
}
|
|
962
|
+
get keyOrigin() {
|
|
963
|
+
return this.#keyOrigin;
|
|
964
|
+
}
|
|
948
965
|
requireKey() {
|
|
949
966
|
if (this.#keyRefusal)
|
|
950
967
|
throw this.#keyRefusal;
|
|
951
968
|
if (!this.#apiKey)
|
|
952
|
-
throw new core_js_1.AuthError();
|
|
969
|
+
throw this.cliErrors ? new core_js_1.AuthError(exports.NO_KEY_MESSAGE) : new core_js_1.AuthError(exports.NO_KEY_MESSAGE, exports.NO_KEY_FIX);
|
|
970
|
+
if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
|
|
971
|
+
this.#keyUseSaid = true;
|
|
972
|
+
this.#onKeyUse(this.#keyOrigin);
|
|
973
|
+
}
|
|
953
974
|
return this.#apiKey;
|
|
954
975
|
}
|
|
955
976
|
bearer() {
|
|
@@ -1271,10 +1292,12 @@ class PromigenceClient {
|
|
|
1271
1292
|
continue;
|
|
1272
1293
|
}
|
|
1273
1294
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1295
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1274
1296
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1275
1297
|
status: res.status,
|
|
1276
1298
|
...bodyExtra(payload),
|
|
1277
1299
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1300
|
+
...(rid ? { requestId: rid } : {}),
|
|
1278
1301
|
});
|
|
1279
1302
|
}
|
|
1280
1303
|
}
|
|
@@ -1354,10 +1377,12 @@ class PromigenceClient {
|
|
|
1354
1377
|
continue;
|
|
1355
1378
|
}
|
|
1356
1379
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1380
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1357
1381
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1358
1382
|
status: res.status,
|
|
1359
1383
|
...bodyExtra(payload),
|
|
1360
1384
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1385
|
+
...(rid ? { requestId: rid } : {}),
|
|
1361
1386
|
});
|
|
1362
1387
|
}
|
|
1363
1388
|
}
|
|
@@ -1733,12 +1758,8 @@ class PromigenceClient {
|
|
|
1733
1758
|
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1734
1759
|
},
|
|
1735
1760
|
waitDone: async (runId, opts = {}) => {
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1739
|
-
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1740
|
-
return terminal || allDone;
|
|
1741
|
-
});
|
|
1761
|
+
const endedSince = {};
|
|
1762
|
+
const r = await this.waitLoop(opts, exports.LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
|
|
1742
1763
|
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1743
1764
|
},
|
|
1744
1765
|
};
|
|
@@ -2147,10 +2168,19 @@ class PromigenceClient {
|
|
|
2147
2168
|
}), sandboxId);
|
|
2148
2169
|
return r.ports ?? [];
|
|
2149
2170
|
},
|
|
2150
|
-
revoke: (sandboxId, tokenOrPort) =>
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2171
|
+
revoke: (sandboxId, tokenOrPort) => {
|
|
2172
|
+
const which = String(tokenOrPort).trim();
|
|
2173
|
+
return /^[0-9]+$/.test(which)
|
|
2174
|
+
? this.request({
|
|
2175
|
+
method: "DELETE",
|
|
2176
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
|
|
2177
|
+
})
|
|
2178
|
+
: this.request({
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
|
|
2181
|
+
body: { token: which },
|
|
2182
|
+
});
|
|
2183
|
+
},
|
|
2154
2184
|
},
|
|
2155
2185
|
ssh: {
|
|
2156
2186
|
open: (sandboxId, opts) => {
|
|
@@ -2647,10 +2677,10 @@ class PromigenceClient {
|
|
|
2647
2677
|
throw new core_js_1.InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2648
2678
|
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2649
2679
|
},
|
|
2650
|
-
alias: (idOrAlias, alias) => this.request({
|
|
2680
|
+
alias: (idOrAlias, alias, opts = {}) => this.request({
|
|
2651
2681
|
method: "PUT",
|
|
2652
2682
|
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2653
|
-
body: { alias },
|
|
2683
|
+
body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
|
|
2654
2684
|
}),
|
|
2655
2685
|
waitBuild: async (buildId, opts = {}) => {
|
|
2656
2686
|
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");
|
|
@@ -16,9 +16,11 @@ class CommandTimeoutError extends core_js_1.CommandExitError {
|
|
|
16
16
|
code;
|
|
17
17
|
fix;
|
|
18
18
|
guide;
|
|
19
|
+
docs;
|
|
19
20
|
constructor(result, timeoutMs) {
|
|
20
21
|
super(result);
|
|
21
22
|
this.code = "task_timeout";
|
|
23
|
+
this.docs = (0, core_js_1.errorsDocsFor)(this.code);
|
|
22
24
|
this.timeoutMs = timeoutMs;
|
|
23
25
|
this.message = timeoutMs === undefined
|
|
24
26
|
? `command timed out (killed at its timeout, exit ${result.exitCode})`
|
package/dist/cjs/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
|
}
|
package/dist/cjs/context.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CREDENTIAL_CHECK_NOTE = exports.CONTEXT_SKIP = exports.CONTEXT_MAX_BYTES = exports.CONTEXT_TAR_PREFIX = void 0;
|
|
3
|
+
exports.CREDENTIAL_CHECK_NOTE = exports.CONTENT_CHECK_MAX_BYTES = exports.CONTEXT_SKIP = exports.CONTEXT_MAX_BYTES = exports.CONTEXT_TAR_PREFIX = void 0;
|
|
4
4
|
exports.parseDockerignore = parseDockerignore;
|
|
5
5
|
exports.dockerignoreMatcher = dockerignoreMatcher;
|
|
6
6
|
exports.secretLike = secretLike;
|
|
7
|
+
exports.clearedByContent = clearedByContent;
|
|
8
|
+
exports.secretLikeFile = secretLikeFile;
|
|
7
9
|
exports.credentialRefusalMessage = credentialRefusalMessage;
|
|
8
10
|
exports.listContext = listContext;
|
|
9
11
|
exports.packContext = packContext;
|
|
@@ -397,6 +399,74 @@ function secretLike(path) {
|
|
|
397
399
|
return false;
|
|
398
400
|
return SECRET_NAMES.some((re) => re.test(name));
|
|
399
401
|
}
|
|
402
|
+
exports.CONTENT_CHECK_MAX_BYTES = 1024 * 1024;
|
|
403
|
+
const PUBLIC_PEM = new Set([
|
|
404
|
+
"CERTIFICATE", "TRUSTED CERTIFICATE", "X509 CERTIFICATE", "X509 CRL", "CERTIFICATE REQUEST",
|
|
405
|
+
"NEW CERTIFICATE REQUEST", "PUBLIC KEY", "RSA PUBLIC KEY", "PKCS7", "CMS", "DH PARAMETERS",
|
|
406
|
+
"X9.42 DH PARAMETERS", "EC PARAMETERS",
|
|
407
|
+
]);
|
|
408
|
+
function publicPem(text) {
|
|
409
|
+
if (/private[\s_-]*key/i.test(text))
|
|
410
|
+
return false;
|
|
411
|
+
const types = [...text.matchAll(/-----BEGIN ([^\r\n-]+)-----/g)].map((m) => m[1].trim().toUpperCase());
|
|
412
|
+
if (types.length === 0 || !types.every((t) => PUBLIC_PEM.has(t)))
|
|
413
|
+
return false;
|
|
414
|
+
const outside = text.replace(/-----BEGIN ([^\r\n-]+)-----[\s\S]*?-----END \1-----/g, "");
|
|
415
|
+
if (/-----(BEGIN|END) /.test(outside))
|
|
416
|
+
return false;
|
|
417
|
+
return !outside.split(/\r?\n/).some((l) => /^(?=.*[A-Za-z0-9+/])[A-Za-z0-9+/=]{40,}$/.test(l.trim()));
|
|
418
|
+
}
|
|
419
|
+
const NPMRC_CREDENTIAL = /_auth|token|passw|secret|credential|certfile|keyfile|otp|username/i;
|
|
420
|
+
function npmrcWithoutCredentials(text) {
|
|
421
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
422
|
+
const line = raw.trim().replace(/^[;#]+\s*/, "");
|
|
423
|
+
if (!line)
|
|
424
|
+
continue;
|
|
425
|
+
if (NPMRC_CREDENTIAL.test(line) || /:\/\/[^\s/]*@/.test(line))
|
|
426
|
+
return false;
|
|
427
|
+
const eq = line.indexOf("=");
|
|
428
|
+
const key = (eq < 0 ? line : line.slice(0, eq)).trim();
|
|
429
|
+
const own = key.startsWith("//") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
430
|
+
if (/^(key|cert|email)$/i.test(own.trim()))
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
function clearedByContent(path, content) {
|
|
436
|
+
const name = path.split("/").filter(Boolean).pop() ?? "";
|
|
437
|
+
let text;
|
|
438
|
+
if (typeof content === "string") {
|
|
439
|
+
text = content;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
try {
|
|
443
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (text.includes("\u0000"))
|
|
450
|
+
return false;
|
|
451
|
+
if (/\.pem$/i.test(name))
|
|
452
|
+
return publicPem(text);
|
|
453
|
+
if (/^\.npmrc$/i.test(name))
|
|
454
|
+
return npmrcWithoutCredentials(text);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
function secretLikeFile(rel, abs) {
|
|
458
|
+
if (!secretLike(rel))
|
|
459
|
+
return false;
|
|
460
|
+
try {
|
|
461
|
+
const st = (0, node_fs_1.lstatSync)(abs);
|
|
462
|
+
if (!st.isFile() || st.size > exports.CONTENT_CHECK_MAX_BYTES)
|
|
463
|
+
return true;
|
|
464
|
+
return !clearedByContent(rel, (0, node_fs_1.readFileSync)(abs));
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
400
470
|
function namedList(items) {
|
|
401
471
|
const more = items.length > NAMED ? ` and ${items.length - NAMED} more` : "";
|
|
402
472
|
return items.slice(0, NAMED).join(", ") + more;
|
|
@@ -405,7 +475,7 @@ function credentialRefusalMessage(hits) {
|
|
|
405
475
|
const one = hits.length === 1;
|
|
406
476
|
return `${hits.length} file${one ? " looks" : "s look"} like ${one ? "a credential" : "credentials"} and ${one ? "was" : "were"} not uploaded: ${namedList(hits)}; nothing was created`;
|
|
407
477
|
}
|
|
408
|
-
exports.CREDENTIAL_CHECK_NOTE = "The check goes by file name and is best-effort: it
|
|
478
|
+
exports.CREDENTIAL_CHECK_NOTE = "The check goes by file name and is best-effort: it reads only a .pem (one with no private key passes) and an .npmrc (one with no token or password passes), so look over what you send.";
|
|
409
479
|
function insidePath(p) {
|
|
410
480
|
return goClean(p.split(node_path_1.sep).join("/").replace(/^\.\/+/, ""));
|
|
411
481
|
}
|
|
@@ -505,7 +575,7 @@ function collect(dir, opts) {
|
|
|
505
575
|
if (special.length > 0) {
|
|
506
576
|
notes.push(`Left out ${namedList(special)}: a socket, device or pipe; a build context holds only files, directories and links.`);
|
|
507
577
|
}
|
|
508
|
-
const secrets = entries.filter((e) => e.kind === "file" &&
|
|
578
|
+
const secrets = entries.filter((e) => e.kind === "file" && secretLikeFile(e.rel, e.abs)).map((e) => e.rel);
|
|
509
579
|
return { entries, bytes, notes, secrets, ignore };
|
|
510
580
|
}
|
|
511
581
|
function listContext(dir, opts = {}) {
|
|
@@ -528,8 +598,10 @@ function packContext(dir, opts = {}) {
|
|
|
528
598
|
const ig = c.ignore?.file;
|
|
529
599
|
throw new core_js_1.InvalidArgumentError(`the build context is over ${max} bytes${ig ? ` even without what its ${ig} excludes` : ""}`, `${ig ? `Exclude more in ${ig}` : "Add a .dockerignore"}, point --context at a smaller directory, or pass a git URL pinned to a full 40-character sha.`);
|
|
530
600
|
}
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
const allowed = new Set((opts.allowSecretFiles ?? []).map(insidePath));
|
|
602
|
+
const refused = c.secrets.filter((s) => !allowed.has(s));
|
|
603
|
+
if (refused.length > 0 && !opts.allowSecrets) {
|
|
604
|
+
throw new core_js_1.InvalidArgumentError(credentialRefusalMessage(refused), `A snapshot keeps every file it holds, and every sandbox started from it has them. Exclude them in ${c.ignore?.file ?? ".dockerignore"}, pass the values as secrets, or name each that is not a real credential in { allowSecretFiles: [...] } ({ allowSecrets: true } for all of them). ${exports.CREDENTIAL_CHECK_NOTE}`);
|
|
533
605
|
}
|
|
534
606
|
if (c.entries.length === 0) {
|
|
535
607
|
throw new core_js_1.InvalidArgumentError(`the build context ${JSON.stringify(dir)} is empty`, "Point --context at the directory holding your Dockerfile.");
|