@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 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", "e2b-compat", "computesdk", "mcp",
6
+ "cli", "harbor", "inspect", "computesdk", "mcp",
7
7
  ];
8
8
  let globalCaller;
9
9
  let defaultCaller;
@@ -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", "e2b-compat", "computesdk", "mcp",
16
+ "cli", "harbor", "inspect", "computesdk", "mcp",
17
17
  ];
18
18
  let globalCaller;
19
19
  let defaultCaller;
@@ -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
- /** A rejected build names the guard and the measured value; nothing was stored. */
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 nothing inside used CPU or sent or received a byte, and no network
1157
- * connection to the outside is open — for an agent that runs inside the sandbox on its own.
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
- /** `PUT /snapshots/:id/alias` — the sidecar's one mutable field. */
3053
- alias: (idOrAlias: string, alias: string | null) => Promise<SnapshotRecord>;
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
  *
@@ -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
- return { apiKey: key, apiUrl };
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 ?? `${core_js_1.ERRORS_DOCS_URL}#${code}`;
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 r = await this.waitLoop(opts, exports.LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => {
1737
- const terminal = run.state === "completed" || run.state === "cap_killed" || run.state === "aborted";
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) => this.request({
2151
- method: "DELETE",
2152
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
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");
@@ -18,6 +18,7 @@ export declare class CommandTimeoutError extends CommandExitError {
18
18
  readonly code: string;
19
19
  readonly fix: string;
20
20
  readonly guide: string;
21
+ readonly docs: string;
21
22
  constructor(result: CommandResult, timeoutMs: number | undefined);
22
23
  }
23
24
  /**
@@ -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})`
@@ -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 `allowSecrets` says otherwise. The check goes by file name: it is
19
- * best-effort and cannot see what a file holds, so look over the list (`listContext`) before
20
- * you send a directory.
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
  }
@@ -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 cannot see what a file holds, so look over what you send.";
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" && secretLike(e.rel)).map((e) => e.rel);
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
- if (c.secrets.length > 0 && !opts.allowSecrets) {
532
- throw new core_js_1.InvalidArgumentError(credentialRefusalMessage(c.secrets), `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 pass { allowSecrets: true } if they are not real credentials. ${exports.CREDENTIAL_CHECK_NOTE}`);
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.");