@promigence/sdk 0.1.0-beta.14 → 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/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 cannot see what a file holds, so look over what you send.";
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" && secretLike(e.rel)).map((e) => e.rel);
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
- if (c.secrets.length > 0 && !opts.allowSecrets) {
522
- throw new 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. ${CREDENTIAL_CHECK_NOTE}`);
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.14";
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 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);
package/dist/core.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.14";
2
+ export const VERSION = "0.1.0-beta.16";
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 available in this beta yet";
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 ?? DOCS_URL;
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,21 @@ 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
+ export function sandboxEndedIn(message) {
108
+ const m = SANDBOX_ENDED.exec(message.trim());
109
+ return m ? { sandboxId: m[1], state: m[2] } : undefined;
110
+ }
111
+ export const SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
112
+ export class SandboxEndedError extends InvalidArgumentError {
113
+ sandboxId;
114
+ state;
115
+ constructor(message, sandboxId, state, fix = SANDBOX_ENDED_FIX) {
116
+ super(message, fix, "usage");
117
+ this.sandboxId = sandboxId;
118
+ this.state = state;
119
+ }
120
+ }
102
121
  export class EnvFailedError extends PromigenceError {
103
122
  constructor(code, message, fix) {
104
123
  super({ code, message, fix, exitCode: 3 });
@@ -216,6 +235,7 @@ export const EXIT_BY_CODE = {
216
235
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
217
236
  not_connected: 7,
218
237
  service_restart: 3,
238
+ digest_mismatch: 4,
219
239
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
220
240
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
221
241
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
@@ -247,6 +267,8 @@ export const EXIT_BY_CODE = {
247
267
  project_cap_reached: 5,
248
268
  spend_limit_reached: 5,
249
269
  billing_hold: 5,
270
+ credits_exhausted: 5, purchase_limit: 5, purchases_blocked: 5,
271
+ card_needed: 5, auto_reload_unavailable: 5,
250
272
  tier_over_plan: 2,
251
273
  network_host_refused: 9, network_policy_unsupported: 3,
252
274
  secrets_in_snapshot: 2, secrets_unavailable: 3,
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
@@ -1340,6 +1340,12 @@ const THREAD_GRACE_MS = 1_000;
1340
1340
  const THREAD_SWEEP_MS = 250;
1341
1341
  const THREAD_MARKS = 4096;
1342
1342
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1343
+ function clockOffset(hr = () => Number(process.hrtime.bigint()) / 1e6, now = () => performance.now(), n = 5) {
1344
+ let best = -Infinity;
1345
+ for (let i = 0; i < n; i++)
1346
+ best = Math.max(best, hr() - now());
1347
+ return best;
1348
+ }
1343
1349
  class TransportThread {
1344
1350
  origin;
1345
1351
  worker;
@@ -1365,7 +1371,7 @@ class TransportThread {
1365
1371
  posts = 0;
1366
1372
  answered = 0;
1367
1373
  pace = THREAD_DEFAULTS.pace;
1368
- hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1374
+ hrOffset = clockOffset();
1369
1375
  constructor(origin, worker, port, acks) {
1370
1376
  this.origin = origin;
1371
1377
  this.worker = worker;
@@ -2054,6 +2060,9 @@ export const __http2Internals = {
2054
2060
  threadKnobs(env) {
2055
2061
  return threadKnobs(env);
2056
2062
  },
2063
+ clockOffset(hr, now) {
2064
+ return clockOffset(hr, now);
2065
+ },
2057
2066
  wedgeThreadForTest() {
2058
2067
  try {
2059
2068
  transportThread?.port.postMessage({ t: "wedge-for-test" });
@@ -2066,6 +2075,18 @@ export const __http2Internals = {
2066
2075
  }
2067
2076
  catch { }
2068
2077
  },
2078
+ holdTurnsForTest() {
2079
+ try {
2080
+ transportThread?.port.postMessage({ t: "hold-turns-for-test" });
2081
+ }
2082
+ catch { }
2083
+ },
2084
+ releaseTurnsForTest() {
2085
+ try {
2086
+ transportThread?.port.postMessage({ t: "release-turns-for-test" });
2087
+ }
2088
+ catch { }
2089
+ },
2069
2090
  early: {
2070
2091
  start(host) {
2071
2092
  return earlyLookupFor(host.toLowerCase()) !== undefined;