@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.
@@ -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,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
  }
@@ -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 = {}) {
@@ -524,12 +594,19 @@ function packContext(dir, opts = {}) {
524
594
  const c = collect(dir, opts);
525
595
  for (const n of c.notes)
526
596
  opts.onNote?.(n);
597
+ const files = new Set(c.entries.filter((e) => e.kind !== "dir").map((e) => e.rel));
598
+ const unknown = [...new Set((opts.allowSecretFiles ?? []).map(insidePath))].filter((p) => !files.has(p));
599
+ if (unknown.length > 0) {
600
+ throw new core_js_1.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"] }.');
601
+ }
527
602
  if (c.bytes > max) {
528
603
  const ig = c.ignore?.file;
529
604
  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
605
  }
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}`);
606
+ const allowed = new Set((opts.allowSecretFiles ?? []).map(insidePath));
607
+ const refused = c.secrets.filter((s) => !allowed.has(s));
608
+ if (refused.length > 0 && !opts.allowSecrets) {
609
+ 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
610
  }
534
611
  if (c.entries.length === 0) {
535
612
  throw new core_js_1.InvalidArgumentError(`the build context ${JSON.stringify(dir)} is empty`, "Point --context at the directory holding your Dockerfile.");
@@ -1,8 +1,10 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.15";
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 this beta, not a mistake of theirs. */
47
- export declare const PREVIEWS_UNAVAILABLE = "preview URLs are not available in this beta yet";
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
- /** Bad arguments (e.g. an unknown template, a missing spend cap). exit 2 */
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
- /** Throw `CommandExitError` on non-zero exit (default true). */
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/cjs/core.js CHANGED
@@ -1,15 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EXIT_BY_CODE = exports.NotReleasedError = exports.ApiError = exports.ServiceRestartError = exports.TimeoutError = exports.RateLimitError = exports.UnreachableError = exports.NotFoundError = exports.AuthError = exports.NotVerifiedError = exports.SpendCapError = exports.CommandExitError = exports.EnvFailedError = exports.PreviewsUnavailableError = exports.UnsupportedError = exports.InvalidArgumentError = exports.PromigenceError = exports.PREVIEWS_UNAVAILABLE = exports.FILE_PUT_CHUNK_BYTES = exports.MAX_OUTPUT_BYTES_DEFAULT = exports.IMAGE_BUILD_LIST_PRICE = exports.IMAGE_BUILD_TIMEOUT_MS = exports.WAIT_TIMEOUT_MS = exports.KEY_CONTACT = exports.DEFAULT_API_URL = exports.ERRORS_DOCS_URL = exports.DOCS_URL = exports.VERSION = void 0;
3
+ exports.EXIT_BY_CODE = exports.NotReleasedError = exports.ApiError = exports.ExecLostError = exports.ServiceRestartError = exports.TimeoutError = exports.RateLimitError = exports.UnreachableError = exports.NotFoundError = exports.AuthError = exports.NotVerifiedError = exports.SpendCapError = exports.CommandExitError = exports.EnvFailedError = exports.SandboxEndedError = exports.SANDBOX_ENDED_FIX = exports.PreviewsUnavailableError = exports.UnsupportedError = exports.InvalidArgumentError = exports.PromigenceError = exports.PREVIEWS_UNAVAILABLE = exports.FILE_PUT_CHUNK_BYTES = exports.MAX_OUTPUT_BYTES_DEFAULT = exports.IMAGE_BUILD_LIST_PRICE = exports.IMAGE_BUILD_TIMEOUT_MS = exports.WAIT_TIMEOUT_MS = exports.KEY_CONTACT = exports.DEFAULT_API_URL = exports.ERRORS_DOCS_URL = exports.DOCS_URL = exports.VERSION = void 0;
4
+ exports.errorsDocsFor = errorsDocsFor;
4
5
  exports.previewsFix = previewsFix;
5
6
  exports.withPreviewsFix = withPreviewsFix;
6
7
  exports.fixBeyondMessage = fixBeyondMessage;
7
8
  exports.previewsOff = previewsOff;
9
+ exports.sandboxEndedIn = sandboxEndedIn;
8
10
  exports.exitForCode = exitForCode;
9
11
  const hint_js_1 = require("./hint.js");
10
- exports.VERSION = "0.1.0-beta.15";
12
+ exports.VERSION = "0.1.0-beta.17";
11
13
  exports.DOCS_URL = "https://promigence.ai";
12
14
  exports.ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
15
+ function errorsDocsFor(code) {
16
+ return `${exports.ERRORS_DOCS_URL}#${code}`;
17
+ }
13
18
  exports.DEFAULT_API_URL = "https://api.promigence.ai/v1";
14
19
  exports.KEY_CONTACT = "support@promigence.ai";
15
20
  exports.WAIT_TIMEOUT_MS = 100_000;
@@ -18,10 +23,11 @@ exports.IMAGE_BUILD_LIST_PRICE = 0.1656;
18
23
  exports.MAX_OUTPUT_BYTES_DEFAULT = 64 * 1024;
19
24
  exports.FILE_PUT_CHUNK_BYTES = 16 * 1024 * 1024;
20
25
  const PREVIEWS_OFF = /^previews are not enabled on this deployment\.?$/i;
21
- exports.PREVIEWS_UNAVAILABLE = "preview URLs are not available in this beta yet";
26
+ exports.PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
22
27
  function previewsFix(sandboxId = "SID", port = 8000) {
23
28
  return `Reach the port over SSH instead: \`promigence sandbox ssh ${(0, hint_js_1.hintArg)(sandboxId)} -- -N -L ${(0, hint_js_1.hintArg)(`${port}:localhost:${port}`)}\` ` +
24
- `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C.`;
29
+ `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C. ` +
30
+ "Write to support@promigence.ai to have preview URLs turned on.";
25
31
  }
26
32
  async function withPreviewsFix(call, sandboxId, port = "PORT") {
27
33
  try {
@@ -60,7 +66,7 @@ class PromigenceError extends Error {
60
66
  this.name = new.target.name;
61
67
  this.code = code;
62
68
  this.fix = previewsOff ? previewsFix() : fixBeyondMessage(init.message, init.fix);
63
- this.docs = init.docs ?? exports.DOCS_URL;
69
+ this.docs = init.docs ?? errorsDocsFor(code);
64
70
  this.exitCode = init.exitCode ?? 1;
65
71
  this.cliExit = init.exitCode ?? 1;
66
72
  this.guide = `promigence agent-guide --section errors --code ${(0, hint_js_1.hintArg)(code)}`;
@@ -111,6 +117,27 @@ class PreviewsUnavailableError extends InvalidArgumentError {
111
117
  }
112
118
  }
113
119
  exports.PreviewsUnavailableError = PreviewsUnavailableError;
120
+ const SANDBOX_ENDED = /^sandbox (\S+) has ended \(`([a-z_]+)`\)/;
121
+ const SANDBOX_EXPIRED = /^sandbox (\S+) reached its timeout and ended\b/;
122
+ function sandboxEndedIn(message) {
123
+ const text = message.trim();
124
+ const m = SANDBOX_ENDED.exec(text);
125
+ if (m)
126
+ return { sandboxId: m[1], state: m[2] };
127
+ const x = SANDBOX_EXPIRED.exec(text);
128
+ return x ? { sandboxId: x[1], state: "ended" } : undefined;
129
+ }
130
+ exports.SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
131
+ class SandboxEndedError extends InvalidArgumentError {
132
+ sandboxId;
133
+ state;
134
+ constructor(message, sandboxId, state, fix = exports.SANDBOX_ENDED_FIX) {
135
+ super(message, fix, "usage");
136
+ this.sandboxId = sandboxId;
137
+ this.state = state;
138
+ }
139
+ }
140
+ exports.SandboxEndedError = SandboxEndedError;
114
141
  class EnvFailedError extends PromigenceError {
115
142
  constructor(code, message, fix) {
116
143
  super({ code, message, fix, exitCode: 3 });
@@ -221,6 +248,21 @@ class ServiceRestartError extends PromigenceError {
221
248
  }
222
249
  }
223
250
  exports.ServiceRestartError = ServiceRestartError;
251
+ 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. " +
252
+ "Billed unless the drop was ours: then email support@promigence.ai with the sandbox id.";
253
+ class ExecLostError extends PromigenceError {
254
+ sandboxId;
255
+ lost;
256
+ constructor(message, ids = {}, lost = "gone") {
257
+ super({
258
+ code: "exec_lost", message, fix: EXEC_LOST_FIX, docs: `${exports.ERRORS_DOCS_URL}#exec_lost`,
259
+ exitCode: 3, category: "env", retryable: false,
260
+ });
261
+ this.sandboxId = ids.sandboxId;
262
+ this.lost = lost;
263
+ }
264
+ }
265
+ exports.ExecLostError = ExecLostError;
224
266
  class ApiError extends PromigenceError {
225
267
  }
226
268
  exports.ApiError = ApiError;
@@ -240,6 +282,8 @@ exports.EXIT_BY_CODE = {
240
282
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
241
283
  not_connected: 7,
242
284
  service_restart: 3,
285
+ digest_mismatch: 4,
286
+ exec_lost: 3,
243
287
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
244
288
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
245
289
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
@@ -114,9 +114,10 @@ class CommandHandle {
114
114
  stderr: this.stderr,
115
115
  truncated: Boolean(this.record.truncated),
116
116
  execId: this.pid,
117
+ timedOut: (0, command_errors_js_1.exitTimedOut)(this.record.exit, this.opts.timeoutMs),
117
118
  };
118
119
  if (result.exitCode !== 0) {
119
- if ((0, command_errors_js_1.exitTimedOut)(this.record.exit, this.opts.timeoutMs))
120
+ if (result.timedOut)
120
121
  throw new command_errors_js_1.CommandTimeoutError(result, this.opts.timeoutMs);
121
122
  throw new core_js_1.CommandExitError(result);
122
123
  }
@@ -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/cjs/http2.js CHANGED
@@ -41,6 +41,7 @@ const FETCH_AT_LOAD = globalThis.fetch;
41
41
  const PROXY_VARS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"];
42
42
  const OFF = new Set(["0", "false", "off", "no"]);
43
43
  const NOT_IN_H2 = new Set(["connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "host", "te"]);
44
+ const ACCEPT_ENCODING = "br, gzip";
44
45
  function http2Allowed(opt, env = process.env) {
45
46
  try {
46
47
  const envFlag = env["PROMIGENCE_HTTP2"]?.trim().toLowerCase();
@@ -1100,6 +1101,8 @@ function exchange(pool, entry, target, init, deadline) {
1100
1101
  if (!NOT_IN_H2.has(name))
1101
1102
  headers[name] = init.headers[k];
1102
1103
  }
1104
+ if (headers["accept-encoding"] === undefined)
1105
+ headers["accept-encoding"] = ACCEPT_ENCODING;
1103
1106
  headers[http2.sensitiveHeaders] = ["authorization"];
1104
1107
  const body = init.body === undefined ? undefined : Buffer.from(init.body, "utf8");
1105
1108
  if (body !== undefined)
@@ -1351,6 +1354,12 @@ const THREAD_GRACE_MS = 1_000;
1351
1354
  const THREAD_SWEEP_MS = 250;
1352
1355
  const THREAD_MARKS = 4096;
1353
1356
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1357
+ function clockOffset(hr = () => Number(process.hrtime.bigint()) / 1e6, now = () => performance.now(), n = 5) {
1358
+ let best = -Infinity;
1359
+ for (let i = 0; i < n; i++)
1360
+ best = Math.max(best, hr() - now());
1361
+ return best;
1362
+ }
1354
1363
  class TransportThread {
1355
1364
  origin;
1356
1365
  worker;
@@ -1376,7 +1385,7 @@ class TransportThread {
1376
1385
  posts = 0;
1377
1386
  answered = 0;
1378
1387
  pace = THREAD_DEFAULTS.pace;
1379
- hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1388
+ hrOffset = clockOffset();
1380
1389
  constructor(origin, worker, port, acks) {
1381
1390
  this.origin = origin;
1382
1391
  this.worker = worker;
@@ -1657,6 +1666,8 @@ class TransportThread {
1657
1666
  if (!NOT_IN_H2.has(name))
1658
1667
  headers[name] = init.headers[k];
1659
1668
  }
1669
+ if (headers["accept-encoding"] === undefined)
1670
+ headers["accept-encoding"] = ACCEPT_ENCODING;
1660
1671
  if (init.body !== undefined)
1661
1672
  headers["content-length"] = Buffer.byteLength(init.body, "utf8");
1662
1673
  const id = this.nextId++;
@@ -2066,6 +2077,9 @@ exports.__http2Internals = {
2066
2077
  threadKnobs(env) {
2067
2078
  return threadKnobs(env);
2068
2079
  },
2080
+ clockOffset(hr, now) {
2081
+ return clockOffset(hr, now);
2082
+ },
2069
2083
  wedgeThreadForTest() {
2070
2084
  try {
2071
2085
  transportThread?.port.postMessage({ t: "wedge-for-test" });
@@ -2078,6 +2092,18 @@ exports.__http2Internals = {
2078
2092
  }
2079
2093
  catch { }
2080
2094
  },
2095
+ holdTurnsForTest() {
2096
+ try {
2097
+ transportThread?.port.postMessage({ t: "hold-turns-for-test" });
2098
+ }
2099
+ catch { }
2100
+ },
2101
+ releaseTurnsForTest() {
2102
+ try {
2103
+ transportThread?.port.postMessage({ t: "release-turns-for-test" });
2104
+ }
2105
+ catch { }
2106
+ },
2081
2107
  early: {
2082
2108
  start(host) {
2083
2109
  return earlyLookupFor(host.toLowerCase()) !== undefined;