@promigence/sdk 0.1.0-beta.15 → 0.1.0-beta.17
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 +173 -19
- package/dist/cjs/client.js +451 -129
- package/dist/cjs/command-errors.d.ts +1 -0
- package/dist/cjs/command-errors.js +2 -0
- package/dist/cjs/context.d.ts +20 -4
- package/dist/cjs/context.js +82 -5
- package/dist/cjs/core.d.ts +56 -5
- package/dist/cjs/core.js +49 -5
- package/dist/cjs/handles.js +2 -1
- package/dist/cjs/http2.d.ts +8 -0
- package/dist/cjs/http2.js +27 -1
- package/dist/cjs/models.gen.d.ts +269 -30
- package/dist/cjs/proxy.js +32 -3
- package/dist/cjs/sandbox.d.ts +38 -13
- package/dist/cjs/sandbox.js +29 -5
- package/dist/cjs/transport-worker.js +10 -1
- package/dist/client.d.ts +173 -19
- package/dist/client.js +450 -126
- package/dist/command-errors.d.ts +1 -0
- package/dist/command-errors.js +3 -1
- package/dist/context.d.ts +20 -4
- package/dist/context.js +79 -4
- package/dist/core.d.ts +56 -5
- package/dist/core.js +44 -4
- package/dist/handles.js +2 -1
- package/dist/http2.d.ts +8 -0
- package/dist/http2.js +27 -1
- package/dist/models.gen.d.ts +269 -30
- package/dist/proxy.js +33 -4
- package/dist/sandbox.d.ts +38 -13
- package/dist/sandbox.js +29 -5
- package/dist/transport-worker.js +10 -1
- package/package.json +1 -1
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,12 @@ 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, and a name that
|
|
100
|
+
* is no file of the context is refused too, so a mistyped or stale name is seen.
|
|
101
|
+
*/
|
|
102
|
+
allowSecretFiles?: readonly string[];
|
|
87
103
|
/** Called with each note: what was left out by default, or changed on the way. */
|
|
88
104
|
onNote?: (note: string) => void;
|
|
89
105
|
}
|
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 = {}) {
|
|
@@ -514,12 +582,19 @@ export function packContext(dir, opts = {}) {
|
|
|
514
582
|
const c = collect(dir, opts);
|
|
515
583
|
for (const n of c.notes)
|
|
516
584
|
opts.onNote?.(n);
|
|
585
|
+
const files = new Set(c.entries.filter((e) => e.kind !== "dir").map((e) => e.rel));
|
|
586
|
+
const unknown = [...new Set((opts.allowSecretFiles ?? []).map(insidePath))].filter((p) => !files.has(p));
|
|
587
|
+
if (unknown.length > 0) {
|
|
588
|
+
throw new InvalidArgumentError(`allowSecretFiles ${unknown.join(", ")}: no such file in this upload`, 'Name each file as listContext names it (its path inside the context sent), e.g. { allowSecretFiles: ["certs/test.pem"] }.');
|
|
589
|
+
}
|
|
517
590
|
if (c.bytes > max) {
|
|
518
591
|
const ig = c.ignore?.file;
|
|
519
592
|
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
593
|
}
|
|
521
|
-
|
|
522
|
-
|
|
594
|
+
const allowed = new Set((opts.allowSecretFiles ?? []).map(insidePath));
|
|
595
|
+
const refused = c.secrets.filter((s) => !allowed.has(s));
|
|
596
|
+
if (refused.length > 0 && !opts.allowSecrets) {
|
|
597
|
+
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
598
|
}
|
|
524
599
|
if (c.entries.length === 0) {
|
|
525
600
|
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.17";
|
|
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);
|
|
@@ -201,6 +229,20 @@ export declare class TimeoutError extends PromigenceError {
|
|
|
201
229
|
export declare class ServiceRestartError extends PromigenceError {
|
|
202
230
|
constructor(message: string, fix: string);
|
|
203
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The connection dropped while a command was running (`sandboxes.exec`, `execStream`,
|
|
234
|
+
* `commands.run`), and the command could not be picked up again: no client came back for it in
|
|
235
|
+
* time, or the service no longer keeps it. The sandbox keeps running. `retryable` is `false`: the
|
|
236
|
+
* command may have done part of its work before it stopped, so run it again only if that is safe.
|
|
237
|
+
* exit 3
|
|
238
|
+
*/
|
|
239
|
+
export declare class ExecLostError extends PromigenceError {
|
|
240
|
+
/** The sandbox the command ran in. */
|
|
241
|
+
readonly sandboxId: string | undefined;
|
|
242
|
+
constructor(message: string, ids?: {
|
|
243
|
+
sandboxId?: string;
|
|
244
|
+
}, lost?: "gone" | "not_reattached" | "unreachable");
|
|
245
|
+
}
|
|
204
246
|
/** An API error with no more specific class (5xx, unknown code). exit from the table. */
|
|
205
247
|
export declare class ApiError extends PromigenceError {
|
|
206
248
|
}
|
|
@@ -238,6 +280,12 @@ export interface CommandResult {
|
|
|
238
280
|
truncated?: boolean;
|
|
239
281
|
/** `exec_id`, for `promigence exec output <exec_id>` / `GET /sandboxes/:id/execs/:exec_id`. */
|
|
240
282
|
execId?: string;
|
|
283
|
+
/**
|
|
284
|
+
* `true` when the command was killed at its own time bound (see `timeoutMs`), `false` for any
|
|
285
|
+
* other exit. With `throwOnExit: false` this is how a result tells a timeout from any other
|
|
286
|
+
* exit 137, such as an out-of-memory kill or a `kill -9`. Set on every result the SDK returns.
|
|
287
|
+
*/
|
|
288
|
+
timedOut?: boolean;
|
|
241
289
|
}
|
|
242
290
|
export interface CommandRunOpts {
|
|
243
291
|
cwd?: string;
|
|
@@ -249,7 +297,10 @@ export interface CommandRunOpts {
|
|
|
249
297
|
* (or the default 15 minutes).
|
|
250
298
|
*/
|
|
251
299
|
timeoutMs?: number;
|
|
252
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Throw `CommandExitError` on non-zero exit (default true). With `false`, a foreground command's
|
|
302
|
+
* result comes back instead, and its `timedOut` says whether it was killed at its time bound.
|
|
303
|
+
*/
|
|
253
304
|
throwOnExit?: boolean;
|
|
254
305
|
/** Inline output budget; `truncated` beyond it (default 64 KiB). */
|
|
255
306
|
maxOutputBytes?: number;
|
package/dist/core.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { hintArg } from "./hint.js";
|
|
2
|
-
export const VERSION = "0.1.0-beta.
|
|
2
|
+
export const VERSION = "0.1.0-beta.17";
|
|
3
3
|
export const DOCS_URL = "https://promigence.ai";
|
|
4
4
|
export const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
|
|
5
|
+
export function errorsDocsFor(code) {
|
|
6
|
+
return `${ERRORS_DOCS_URL}#${code}`;
|
|
7
|
+
}
|
|
5
8
|
export const DEFAULT_API_URL = "https://api.promigence.ai/v1";
|
|
6
9
|
export const KEY_CONTACT = "support@promigence.ai";
|
|
7
10
|
export const WAIT_TIMEOUT_MS = 100_000;
|
|
@@ -10,10 +13,11 @@ export const IMAGE_BUILD_LIST_PRICE = 0.1656;
|
|
|
10
13
|
export const MAX_OUTPUT_BYTES_DEFAULT = 64 * 1024;
|
|
11
14
|
export const FILE_PUT_CHUNK_BYTES = 16 * 1024 * 1024;
|
|
12
15
|
const PREVIEWS_OFF = /^previews are not enabled on this deployment\.?$/i;
|
|
13
|
-
export const PREVIEWS_UNAVAILABLE = "preview URLs are not
|
|
16
|
+
export const PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
|
|
14
17
|
export function previewsFix(sandboxId = "SID", port = 8000) {
|
|
15
18
|
return `Reach the port over SSH instead: \`promigence sandbox ssh ${hintArg(sandboxId)} -- -N -L ${hintArg(`${port}:localhost:${port}`)}\` ` +
|
|
16
|
-
`forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C
|
|
19
|
+
`forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C. ` +
|
|
20
|
+
"Write to support@promigence.ai to have preview URLs turned on.";
|
|
17
21
|
}
|
|
18
22
|
export async function withPreviewsFix(call, sandboxId, port = "PORT") {
|
|
19
23
|
try {
|
|
@@ -52,7 +56,7 @@ export class PromigenceError extends Error {
|
|
|
52
56
|
this.name = new.target.name;
|
|
53
57
|
this.code = code;
|
|
54
58
|
this.fix = previewsOff ? previewsFix() : fixBeyondMessage(init.message, init.fix);
|
|
55
|
-
this.docs = init.docs ??
|
|
59
|
+
this.docs = init.docs ?? errorsDocsFor(code);
|
|
56
60
|
this.exitCode = init.exitCode ?? 1;
|
|
57
61
|
this.cliExit = init.exitCode ?? 1;
|
|
58
62
|
this.guide = `promigence agent-guide --section errors --code ${hintArg(code)}`;
|
|
@@ -99,6 +103,26 @@ export class PreviewsUnavailableError extends InvalidArgumentError {
|
|
|
99
103
|
super(PREVIEWS_UNAVAILABLE, previewsFix(sandboxId, port), "unsupported");
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
const SANDBOX_ENDED = /^sandbox (\S+) has ended \(`([a-z_]+)`\)/;
|
|
107
|
+
const SANDBOX_EXPIRED = /^sandbox (\S+) reached its timeout and ended\b/;
|
|
108
|
+
export function sandboxEndedIn(message) {
|
|
109
|
+
const text = message.trim();
|
|
110
|
+
const m = SANDBOX_ENDED.exec(text);
|
|
111
|
+
if (m)
|
|
112
|
+
return { sandboxId: m[1], state: m[2] };
|
|
113
|
+
const x = SANDBOX_EXPIRED.exec(text);
|
|
114
|
+
return x ? { sandboxId: x[1], state: "ended" } : undefined;
|
|
115
|
+
}
|
|
116
|
+
export const SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
|
|
117
|
+
export class SandboxEndedError extends InvalidArgumentError {
|
|
118
|
+
sandboxId;
|
|
119
|
+
state;
|
|
120
|
+
constructor(message, sandboxId, state, fix = SANDBOX_ENDED_FIX) {
|
|
121
|
+
super(message, fix, "usage");
|
|
122
|
+
this.sandboxId = sandboxId;
|
|
123
|
+
this.state = state;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
102
126
|
export class EnvFailedError extends PromigenceError {
|
|
103
127
|
constructor(code, message, fix) {
|
|
104
128
|
super({ code, message, fix, exitCode: 3 });
|
|
@@ -199,6 +223,20 @@ export class ServiceRestartError extends PromigenceError {
|
|
|
199
223
|
super({ code: "service_restart", message, fix, exitCode: 3, category: "env", retryable: false });
|
|
200
224
|
}
|
|
201
225
|
}
|
|
226
|
+
const EXEC_LOST_FIX = "The sandbox is still running; the command was stopped, maybe after part of its work: run it again if that is safe. " +
|
|
227
|
+
"Billed unless the drop was ours: then email support@promigence.ai with the sandbox id.";
|
|
228
|
+
export class ExecLostError extends PromigenceError {
|
|
229
|
+
sandboxId;
|
|
230
|
+
lost;
|
|
231
|
+
constructor(message, ids = {}, lost = "gone") {
|
|
232
|
+
super({
|
|
233
|
+
code: "exec_lost", message, fix: EXEC_LOST_FIX, docs: `${ERRORS_DOCS_URL}#exec_lost`,
|
|
234
|
+
exitCode: 3, category: "env", retryable: false,
|
|
235
|
+
});
|
|
236
|
+
this.sandboxId = ids.sandboxId;
|
|
237
|
+
this.lost = lost;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
202
240
|
export class ApiError extends PromigenceError {
|
|
203
241
|
}
|
|
204
242
|
export class NotReleasedError extends PromigenceError {
|
|
@@ -216,6 +254,8 @@ export const EXIT_BY_CODE = {
|
|
|
216
254
|
env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
|
|
217
255
|
not_connected: 7,
|
|
218
256
|
service_restart: 3,
|
|
257
|
+
digest_mismatch: 4,
|
|
258
|
+
exec_lost: 3,
|
|
219
259
|
fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
|
|
220
260
|
verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
|
|
221
261
|
disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
|
package/dist/handles.js
CHANGED
|
@@ -111,9 +111,10 @@ export class CommandHandle {
|
|
|
111
111
|
stderr: this.stderr,
|
|
112
112
|
truncated: Boolean(this.record.truncated),
|
|
113
113
|
execId: this.pid,
|
|
114
|
+
timedOut: exitTimedOut(this.record.exit, this.opts.timeoutMs),
|
|
114
115
|
};
|
|
115
116
|
if (result.exitCode !== 0) {
|
|
116
|
-
if (
|
|
117
|
+
if (result.timedOut)
|
|
117
118
|
throw new CommandTimeoutError(result, this.opts.timeoutMs);
|
|
118
119
|
throw new CommandExitError(result);
|
|
119
120
|
}
|
package/dist/http2.d.ts
CHANGED
|
@@ -197,10 +197,18 @@ export declare const __http2Internals: {
|
|
|
197
197
|
gapUs: number;
|
|
198
198
|
pace: string;
|
|
199
199
|
};
|
|
200
|
+
/** The offset that turns the thread's stamps into trace times (`clockOffset`), read from the two clocks given. */
|
|
201
|
+
clockOffset(hr: () => number, now: () => number): number;
|
|
200
202
|
/** Tests: make the transport thread stop answering (it reads nothing more), as a stuck one would. */
|
|
201
203
|
wedgeThreadForTest(): void;
|
|
202
204
|
/** Tests: make the transport thread fail as a crash would. */
|
|
203
205
|
crashThreadForTest(): void;
|
|
206
|
+
/**
|
|
207
|
+
* Tests: the transport thread gives no spaced request its turn until `releaseTurnsForTest`. Both go as messages, in order
|
|
208
|
+
* with the requests and their cancels, so what is sent between them reaches the thread while every spaced request waits.
|
|
209
|
+
*/
|
|
210
|
+
holdTurnsForTest(): void;
|
|
211
|
+
releaseTurnsForTest(): void;
|
|
204
212
|
/** The early lookup, for tests: start one with a platform, DNS servers and hosts file of the test's own. */
|
|
205
213
|
early: {
|
|
206
214
|
start(host: string): boolean;
|
package/dist/http2.js
CHANGED
|
@@ -31,6 +31,7 @@ const FETCH_AT_LOAD = globalThis.fetch;
|
|
|
31
31
|
const PROXY_VARS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"];
|
|
32
32
|
const OFF = new Set(["0", "false", "off", "no"]);
|
|
33
33
|
const NOT_IN_H2 = new Set(["connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "host", "te"]);
|
|
34
|
+
const ACCEPT_ENCODING = "br, gzip";
|
|
34
35
|
export function http2Allowed(opt, env = process.env) {
|
|
35
36
|
try {
|
|
36
37
|
const envFlag = env["PROMIGENCE_HTTP2"]?.trim().toLowerCase();
|
|
@@ -1089,6 +1090,8 @@ function exchange(pool, entry, target, init, deadline) {
|
|
|
1089
1090
|
if (!NOT_IN_H2.has(name))
|
|
1090
1091
|
headers[name] = init.headers[k];
|
|
1091
1092
|
}
|
|
1093
|
+
if (headers["accept-encoding"] === undefined)
|
|
1094
|
+
headers["accept-encoding"] = ACCEPT_ENCODING;
|
|
1092
1095
|
headers[http2.sensitiveHeaders] = ["authorization"];
|
|
1093
1096
|
const body = init.body === undefined ? undefined : Buffer.from(init.body, "utf8");
|
|
1094
1097
|
if (body !== undefined)
|
|
@@ -1340,6 +1343,12 @@ const THREAD_GRACE_MS = 1_000;
|
|
|
1340
1343
|
const THREAD_SWEEP_MS = 250;
|
|
1341
1344
|
const THREAD_MARKS = 4096;
|
|
1342
1345
|
const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
|
|
1346
|
+
function clockOffset(hr = () => Number(process.hrtime.bigint()) / 1e6, now = () => performance.now(), n = 5) {
|
|
1347
|
+
let best = -Infinity;
|
|
1348
|
+
for (let i = 0; i < n; i++)
|
|
1349
|
+
best = Math.max(best, hr() - now());
|
|
1350
|
+
return best;
|
|
1351
|
+
}
|
|
1343
1352
|
class TransportThread {
|
|
1344
1353
|
origin;
|
|
1345
1354
|
worker;
|
|
@@ -1365,7 +1374,7 @@ class TransportThread {
|
|
|
1365
1374
|
posts = 0;
|
|
1366
1375
|
answered = 0;
|
|
1367
1376
|
pace = THREAD_DEFAULTS.pace;
|
|
1368
|
-
hrOffset =
|
|
1377
|
+
hrOffset = clockOffset();
|
|
1369
1378
|
constructor(origin, worker, port, acks) {
|
|
1370
1379
|
this.origin = origin;
|
|
1371
1380
|
this.worker = worker;
|
|
@@ -1646,6 +1655,8 @@ class TransportThread {
|
|
|
1646
1655
|
if (!NOT_IN_H2.has(name))
|
|
1647
1656
|
headers[name] = init.headers[k];
|
|
1648
1657
|
}
|
|
1658
|
+
if (headers["accept-encoding"] === undefined)
|
|
1659
|
+
headers["accept-encoding"] = ACCEPT_ENCODING;
|
|
1649
1660
|
if (init.body !== undefined)
|
|
1650
1661
|
headers["content-length"] = Buffer.byteLength(init.body, "utf8");
|
|
1651
1662
|
const id = this.nextId++;
|
|
@@ -2054,6 +2065,9 @@ export const __http2Internals = {
|
|
|
2054
2065
|
threadKnobs(env) {
|
|
2055
2066
|
return threadKnobs(env);
|
|
2056
2067
|
},
|
|
2068
|
+
clockOffset(hr, now) {
|
|
2069
|
+
return clockOffset(hr, now);
|
|
2070
|
+
},
|
|
2057
2071
|
wedgeThreadForTest() {
|
|
2058
2072
|
try {
|
|
2059
2073
|
transportThread?.port.postMessage({ t: "wedge-for-test" });
|
|
@@ -2066,6 +2080,18 @@ export const __http2Internals = {
|
|
|
2066
2080
|
}
|
|
2067
2081
|
catch { }
|
|
2068
2082
|
},
|
|
2083
|
+
holdTurnsForTest() {
|
|
2084
|
+
try {
|
|
2085
|
+
transportThread?.port.postMessage({ t: "hold-turns-for-test" });
|
|
2086
|
+
}
|
|
2087
|
+
catch { }
|
|
2088
|
+
},
|
|
2089
|
+
releaseTurnsForTest() {
|
|
2090
|
+
try {
|
|
2091
|
+
transportThread?.port.postMessage({ t: "release-turns-for-test" });
|
|
2092
|
+
}
|
|
2093
|
+
catch { }
|
|
2094
|
+
},
|
|
2069
2095
|
early: {
|
|
2070
2096
|
start(host) {
|
|
2071
2097
|
return earlyLookupFor(host.toLowerCase()) !== undefined;
|