@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/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/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";
|
|
@@ -276,30 +276,6 @@ export function readCredentials(env = process.env) {
|
|
|
276
276
|
function storedName(store, env) {
|
|
277
277
|
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : storeLabel(store);
|
|
278
278
|
}
|
|
279
|
-
const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
|
|
280
|
-
export function apiUrlFromCompatUrl(url) {
|
|
281
|
-
const u = (url ?? "").trim();
|
|
282
|
-
if (!u)
|
|
283
|
-
return undefined;
|
|
284
|
-
let host = "";
|
|
285
|
-
try {
|
|
286
|
-
host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
|
|
292
|
-
}
|
|
293
|
-
export function apiUrlFromCompatDomain(domain) {
|
|
294
|
-
const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
295
|
-
if (!d || COMPAT_REFUSED_HOSTS.test(d))
|
|
296
|
-
return undefined;
|
|
297
|
-
return `https://api.${d}/v1`;
|
|
298
|
-
}
|
|
299
|
-
export function compatApiKey(key) {
|
|
300
|
-
const k = key?.trim();
|
|
301
|
-
return k && !k.startsWith("e2b_") ? k : undefined;
|
|
302
|
-
}
|
|
303
279
|
export function keyOrigin(env = process.env) {
|
|
304
280
|
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
305
281
|
return "PROMIGENCE_API_KEY";
|
|
@@ -326,12 +302,13 @@ function resolveCredentials(opts, env = process.env) {
|
|
|
326
302
|
const inputs = optKey === undefined && envKey === undefined
|
|
327
303
|
? undefined
|
|
328
304
|
: `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
|
|
329
|
-
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
|
|
330
|
-
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
|
+
}
|
|
331
308
|
const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
|
|
332
309
|
if (inputs !== undefined && worked.refusal === undefined) {
|
|
333
310
|
credsLastInputs = inputs;
|
|
334
|
-
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
|
|
311
|
+
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
|
|
335
312
|
}
|
|
336
313
|
return worked;
|
|
337
314
|
}
|
|
@@ -365,8 +342,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
|
|
|
365
342
|
if (refusal)
|
|
366
343
|
return { apiKey: undefined, apiUrl, refusal };
|
|
367
344
|
}
|
|
368
|
-
|
|
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 } : {}) };
|
|
369
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.";
|
|
370
354
|
function keyRefusal(key, from) {
|
|
371
355
|
if (/^[\x21-\x7E]+$/.test(key))
|
|
372
356
|
return undefined;
|
|
@@ -646,6 +630,8 @@ export function snapshotCreateBody(opts) {
|
|
|
646
630
|
}
|
|
647
631
|
if (o.alias)
|
|
648
632
|
body["alias"] = o.alias;
|
|
633
|
+
if (o.alias && o.moveAlias)
|
|
634
|
+
body["move_alias"] = true;
|
|
649
635
|
if (o.grade) {
|
|
650
636
|
if (!o.grade.cmd)
|
|
651
637
|
throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
@@ -680,6 +666,9 @@ const REFUSED_503_CODES = new Set([
|
|
|
680
666
|
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
681
667
|
"secrets_unavailable",
|
|
682
668
|
]);
|
|
669
|
+
const SDK_FIXES = {
|
|
670
|
+
cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
|
|
671
|
+
};
|
|
683
672
|
function withoutCliHelp(fix) {
|
|
684
673
|
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
685
674
|
if (out === fix.trim())
|
|
@@ -689,7 +678,7 @@ function withoutCliHelp(fix) {
|
|
|
689
678
|
export function errorFor(code, message, fix, extra = {}) {
|
|
690
679
|
const attach = (e) => {
|
|
691
680
|
const w = e;
|
|
692
|
-
w.docs = extra.docs
|
|
681
|
+
w.docs = extra.docs !== undefined && e.code === code ? extra.docs : errorsDocsFor(e.code);
|
|
693
682
|
if (extra.status !== undefined)
|
|
694
683
|
w.status = extra.status;
|
|
695
684
|
if (extra.requestId !== undefined)
|
|
@@ -705,6 +694,8 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
705
694
|
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && previewsOff(message)) {
|
|
706
695
|
return attach(new PreviewsUnavailableError());
|
|
707
696
|
}
|
|
697
|
+
if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
|
|
698
|
+
fix = SDK_FIXES[code];
|
|
708
699
|
switch (code) {
|
|
709
700
|
case "auth": return attach(new AuthError(message, fix));
|
|
710
701
|
case "not_found": return attach(new NotFoundError(message, fix));
|
|
@@ -718,8 +709,13 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
718
709
|
case "not_verified": return attach(new NotVerifiedError(message, fix));
|
|
719
710
|
case "timeout": return attach(new TimeoutError(message, fix));
|
|
720
711
|
case "usage":
|
|
721
|
-
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
|
+
}
|
|
722
717
|
return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
718
|
+
}
|
|
723
719
|
case "not_released": return attach(new NotReleasedError(message));
|
|
724
720
|
case "service_restart": return attach(new ServiceRestartError(message, fix));
|
|
725
721
|
default:
|
|
@@ -790,6 +786,21 @@ const WAIT_MS_CAP = 30_000;
|
|
|
790
786
|
export const LONG_POLL_MS = 5_000;
|
|
791
787
|
const BUILD_POLL_MS = 10_000;
|
|
792
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
|
+
}
|
|
793
804
|
const WAITED_HEADER = "promigence-waited-ms";
|
|
794
805
|
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
795
806
|
function waitQuery(waitMs) {
|
|
@@ -827,6 +838,9 @@ export class PromigenceClient {
|
|
|
827
838
|
apiUrl;
|
|
828
839
|
#apiKey;
|
|
829
840
|
#keyRefusal;
|
|
841
|
+
#keyOrigin;
|
|
842
|
+
#onKeyUse;
|
|
843
|
+
#keyUseSaid = false;
|
|
830
844
|
#bearer;
|
|
831
845
|
fetchImpl;
|
|
832
846
|
ownFetch;
|
|
@@ -848,6 +862,8 @@ export class PromigenceClient {
|
|
|
848
862
|
const resolved = resolveCredentials(opts);
|
|
849
863
|
this.#apiKey = resolved.apiKey;
|
|
850
864
|
this.#keyRefusal = resolved.refusal;
|
|
865
|
+
this.#keyOrigin = resolved.origin;
|
|
866
|
+
this.#onKeyUse = opts.onKeyUse;
|
|
851
867
|
this.apiUrl = resolved.apiUrl;
|
|
852
868
|
const env = opts.fetch === undefined ? transportEnv() : undefined;
|
|
853
869
|
const proxy = env !== undefined ? proxyFor(this.apiUrl, env) : undefined;
|
|
@@ -913,11 +929,18 @@ export class PromigenceClient {
|
|
|
913
929
|
catch {
|
|
914
930
|
}
|
|
915
931
|
}
|
|
932
|
+
get keyOrigin() {
|
|
933
|
+
return this.#keyOrigin;
|
|
934
|
+
}
|
|
916
935
|
requireKey() {
|
|
917
936
|
if (this.#keyRefusal)
|
|
918
937
|
throw this.#keyRefusal;
|
|
919
938
|
if (!this.#apiKey)
|
|
920
|
-
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
|
+
}
|
|
921
944
|
return this.#apiKey;
|
|
922
945
|
}
|
|
923
946
|
bearer() {
|
|
@@ -1239,10 +1262,12 @@ export class PromigenceClient {
|
|
|
1239
1262
|
continue;
|
|
1240
1263
|
}
|
|
1241
1264
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1265
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1242
1266
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1243
1267
|
status: res.status,
|
|
1244
1268
|
...bodyExtra(payload),
|
|
1245
1269
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1270
|
+
...(rid ? { requestId: rid } : {}),
|
|
1246
1271
|
});
|
|
1247
1272
|
}
|
|
1248
1273
|
}
|
|
@@ -1322,10 +1347,12 @@ export class PromigenceClient {
|
|
|
1322
1347
|
continue;
|
|
1323
1348
|
}
|
|
1324
1349
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1350
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1325
1351
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1326
1352
|
status: res.status,
|
|
1327
1353
|
...bodyExtra(payload),
|
|
1328
1354
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1355
|
+
...(rid ? { requestId: rid } : {}),
|
|
1329
1356
|
});
|
|
1330
1357
|
}
|
|
1331
1358
|
}
|
|
@@ -1701,12 +1728,8 @@ export class PromigenceClient {
|
|
|
1701
1728
|
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1702
1729
|
},
|
|
1703
1730
|
waitDone: async (runId, opts = {}) => {
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1707
|
-
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1708
|
-
return terminal || allDone;
|
|
1709
|
-
});
|
|
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));
|
|
1710
1733
|
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1711
1734
|
},
|
|
1712
1735
|
};
|
|
@@ -2115,10 +2138,19 @@ export class PromigenceClient {
|
|
|
2115
2138
|
}), sandboxId);
|
|
2116
2139
|
return r.ports ?? [];
|
|
2117
2140
|
},
|
|
2118
|
-
revoke: (sandboxId, tokenOrPort) =>
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
+
},
|
|
2122
2154
|
},
|
|
2123
2155
|
ssh: {
|
|
2124
2156
|
open: (sandboxId, opts) => {
|
|
@@ -2615,10 +2647,10 @@ export class PromigenceClient {
|
|
|
2615
2647
|
throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2616
2648
|
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2617
2649
|
},
|
|
2618
|
-
alias: (idOrAlias, alias) => this.request({
|
|
2650
|
+
alias: (idOrAlias, alias, opts = {}) => this.request({
|
|
2619
2651
|
method: "PUT",
|
|
2620
2652
|
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2621
|
-
body: { alias },
|
|
2653
|
+
body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
|
|
2622
2654
|
}),
|
|
2623
2655
|
waitBuild: async (buildId, opts = {}) => {
|
|
2624
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
|
}
|
package/dist/context.js
CHANGED
|
@@ -387,6 +387,74 @@ export function secretLike(path) {
|
|
|
387
387
|
return false;
|
|
388
388
|
return SECRET_NAMES.some((re) => re.test(name));
|
|
389
389
|
}
|
|
390
|
+
export const CONTENT_CHECK_MAX_BYTES = 1024 * 1024;
|
|
391
|
+
const PUBLIC_PEM = new Set([
|
|
392
|
+
"CERTIFICATE", "TRUSTED CERTIFICATE", "X509 CERTIFICATE", "X509 CRL", "CERTIFICATE REQUEST",
|
|
393
|
+
"NEW CERTIFICATE REQUEST", "PUBLIC KEY", "RSA PUBLIC KEY", "PKCS7", "CMS", "DH PARAMETERS",
|
|
394
|
+
"X9.42 DH PARAMETERS", "EC PARAMETERS",
|
|
395
|
+
]);
|
|
396
|
+
function publicPem(text) {
|
|
397
|
+
if (/private[\s_-]*key/i.test(text))
|
|
398
|
+
return false;
|
|
399
|
+
const types = [...text.matchAll(/-----BEGIN ([^\r\n-]+)-----/g)].map((m) => m[1].trim().toUpperCase());
|
|
400
|
+
if (types.length === 0 || !types.every((t) => PUBLIC_PEM.has(t)))
|
|
401
|
+
return false;
|
|
402
|
+
const outside = text.replace(/-----BEGIN ([^\r\n-]+)-----[\s\S]*?-----END \1-----/g, "");
|
|
403
|
+
if (/-----(BEGIN|END) /.test(outside))
|
|
404
|
+
return false;
|
|
405
|
+
return !outside.split(/\r?\n/).some((l) => /^(?=.*[A-Za-z0-9+/])[A-Za-z0-9+/=]{40,}$/.test(l.trim()));
|
|
406
|
+
}
|
|
407
|
+
const NPMRC_CREDENTIAL = /_auth|token|passw|secret|credential|certfile|keyfile|otp|username/i;
|
|
408
|
+
function npmrcWithoutCredentials(text) {
|
|
409
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
410
|
+
const line = raw.trim().replace(/^[;#]+\s*/, "");
|
|
411
|
+
if (!line)
|
|
412
|
+
continue;
|
|
413
|
+
if (NPMRC_CREDENTIAL.test(line) || /:\/\/[^\s/]*@/.test(line))
|
|
414
|
+
return false;
|
|
415
|
+
const eq = line.indexOf("=");
|
|
416
|
+
const key = (eq < 0 ? line : line.slice(0, eq)).trim();
|
|
417
|
+
const own = key.startsWith("//") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
418
|
+
if (/^(key|cert|email)$/i.test(own.trim()))
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
export function clearedByContent(path, content) {
|
|
424
|
+
const name = path.split("/").filter(Boolean).pop() ?? "";
|
|
425
|
+
let text;
|
|
426
|
+
if (typeof content === "string") {
|
|
427
|
+
text = content;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
try {
|
|
431
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (text.includes("\u0000"))
|
|
438
|
+
return false;
|
|
439
|
+
if (/\.pem$/i.test(name))
|
|
440
|
+
return publicPem(text);
|
|
441
|
+
if (/^\.npmrc$/i.test(name))
|
|
442
|
+
return npmrcWithoutCredentials(text);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
export function secretLikeFile(rel, abs) {
|
|
446
|
+
if (!secretLike(rel))
|
|
447
|
+
return false;
|
|
448
|
+
try {
|
|
449
|
+
const st = lstatSync(abs);
|
|
450
|
+
if (!st.isFile() || st.size > CONTENT_CHECK_MAX_BYTES)
|
|
451
|
+
return true;
|
|
452
|
+
return !clearedByContent(rel, readFileSync(abs));
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
390
458
|
function namedList(items) {
|
|
391
459
|
const more = items.length > NAMED ? ` and ${items.length - NAMED} more` : "";
|
|
392
460
|
return items.slice(0, NAMED).join(", ") + more;
|
|
@@ -395,7 +463,7 @@ export function credentialRefusalMessage(hits) {
|
|
|
395
463
|
const one = hits.length === 1;
|
|
396
464
|
return `${hits.length} file${one ? " looks" : "s look"} like ${one ? "a credential" : "credentials"} and ${one ? "was" : "were"} not uploaded: ${namedList(hits)}; nothing was created`;
|
|
397
465
|
}
|
|
398
|
-
export const CREDENTIAL_CHECK_NOTE = "The check goes by file name and is best-effort: it
|
|
466
|
+
export const 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.";
|
|
399
467
|
function insidePath(p) {
|
|
400
468
|
return goClean(p.split(sep).join("/").replace(/^\.\/+/, ""));
|
|
401
469
|
}
|
|
@@ -495,7 +563,7 @@ function collect(dir, opts) {
|
|
|
495
563
|
if (special.length > 0) {
|
|
496
564
|
notes.push(`Left out ${namedList(special)}: a socket, device or pipe; a build context holds only files, directories and links.`);
|
|
497
565
|
}
|
|
498
|
-
const secrets = entries.filter((e) => e.kind === "file" &&
|
|
566
|
+
const secrets = entries.filter((e) => e.kind === "file" && secretLikeFile(e.rel, e.abs)).map((e) => e.rel);
|
|
499
567
|
return { entries, bytes, notes, secrets, ignore };
|
|
500
568
|
}
|
|
501
569
|
export function listContext(dir, opts = {}) {
|
|
@@ -518,8 +586,10 @@ export function packContext(dir, opts = {}) {
|
|
|
518
586
|
const ig = c.ignore?.file;
|
|
519
587
|
throw new 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.`);
|
|
520
588
|
}
|
|
521
|
-
|
|
522
|
-
|
|
589
|
+
const allowed = new Set((opts.allowSecretFiles ?? []).map(insidePath));
|
|
590
|
+
const refused = c.secrets.filter((s) => !allowed.has(s));
|
|
591
|
+
if (refused.length > 0 && !opts.allowSecrets) {
|
|
592
|
+
throw new 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). ${CREDENTIAL_CHECK_NOTE}`);
|
|
523
593
|
}
|
|
524
594
|
if (c.entries.length === 0) {
|
|
525
595
|
throw new InvalidArgumentError(`the build context ${JSON.stringify(dir)} is empty`, "Point --context at the directory holding your Dockerfile.");
|
package/dist/core.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Promigence SDK — constants, error hierarchy and shared types. */
|
|
2
|
-
export declare const VERSION = "0.1.0-beta.
|
|
2
|
+
export declare const VERSION = "0.1.0-beta.16";
|
|
3
3
|
export declare const DOCS_URL = "https://promigence.ai";
|
|
4
4
|
/** The error reference; each code is an anchor on it (the API's `docs` link has the same form). */
|
|
5
5
|
export declare const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
|
|
6
|
+
/** The row of `code` in the error reference. */
|
|
7
|
+
export declare function errorsDocsFor(code: string): string;
|
|
6
8
|
export declare const DEFAULT_API_URL = "https://api.promigence.ai/v1";
|
|
7
9
|
/** Where to write about a key. A key is self-serve: `promigence login` signs up or in and stores one. */
|
|
8
10
|
export declare const KEY_CONTACT = "support@promigence.ai";
|
|
@@ -43,8 +45,8 @@ export interface PromigenceErrorInit {
|
|
|
43
45
|
/** The API error's `retryable`: nothing ran and the same request, sent again as is, can succeed. */
|
|
44
46
|
retryable?: boolean;
|
|
45
47
|
}
|
|
46
|
-
/** What a caller reads instead: a fact about
|
|
47
|
-
export declare const PREVIEWS_UNAVAILABLE = "preview URLs are not
|
|
48
|
+
/** What a caller reads instead: a fact about their account, not a mistake of theirs. */
|
|
49
|
+
export declare const PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
|
|
48
50
|
export declare function previewsFix(sandboxId?: string, port?: string | number): string;
|
|
49
51
|
/**
|
|
50
52
|
* A `fix` that opens by repeating the message says only what it adds: the API's `usage` fix is
|
|
@@ -97,7 +99,13 @@ export declare class PromigenceError extends Error {
|
|
|
97
99
|
guide: string;
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Bad arguments (e.g. an unknown template, a missing spend cap). exit 2
|
|
104
|
+
*
|
|
105
|
+
* `code` says who refused: `invalid_argument` is the SDK, before anything was sent; `usage` is the
|
|
106
|
+
* API, refusing what it was sent. Nothing ran and nothing was billed either way, so catch this
|
|
107
|
+
* class (or exit 2) to handle both.
|
|
108
|
+
*/
|
|
101
109
|
export declare class InvalidArgumentError extends PromigenceError {
|
|
102
110
|
/**
|
|
103
111
|
* `code` defaults to `invalid_argument` — what the SDK raises for a bad argument it catches
|
|
@@ -122,6 +130,26 @@ export declare function previewsOff(message: string): boolean;
|
|
|
122
130
|
export declare class PreviewsUnavailableError extends InvalidArgumentError {
|
|
123
131
|
constructor(sandboxId?: string, port?: string | number);
|
|
124
132
|
}
|
|
133
|
+
/** The sandbox and its state when an API message is the "has ended" refusal, else `undefined`. */
|
|
134
|
+
export declare function sandboxEndedIn(message: string): {
|
|
135
|
+
sandboxId: string;
|
|
136
|
+
state: string;
|
|
137
|
+
} | undefined;
|
|
138
|
+
/** What a caller of this SDK does about an ended sandbox. */
|
|
139
|
+
export declare const SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
|
|
140
|
+
/**
|
|
141
|
+
* A call to a sandbox that has ended (`state` `ended` or `failed`): it does not come back. Still an
|
|
142
|
+
* `InvalidArgumentError` with code `usage` and exit 2, as the API answers it, so a catch written
|
|
143
|
+
* against earlier releases still catches it; catch this class to tell an ended sandbox from a bad
|
|
144
|
+
* argument. `sandboxId` and `state` name the sandbox and how it stands.
|
|
145
|
+
*/
|
|
146
|
+
export declare class SandboxEndedError extends InvalidArgumentError {
|
|
147
|
+
/** The sandbox that has ended. */
|
|
148
|
+
readonly sandboxId: string;
|
|
149
|
+
/** Its state: `ended`, or `failed` for one that ended on a failure. */
|
|
150
|
+
readonly state: string;
|
|
151
|
+
constructor(message: string, sandboxId: string, state: string, fix?: string);
|
|
152
|
+
}
|
|
125
153
|
/** The environment failed (our fault, never the task's). exit 3 */
|
|
126
154
|
export declare class EnvFailedError extends PromigenceError {
|
|
127
155
|
constructor(code: string, message: string, fix: string);
|