@promigence/sdk 0.1.0-beta.6 → 0.1.0-beta.8

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/README.md CHANGED
@@ -13,8 +13,9 @@ npm i @promigence/sdk
13
13
  ```
14
14
 
15
15
  You need a Promigence account (private beta, by invite). The quickest way to create one is the CLI:
16
- `npm i -g promigence`, then `promigence signup`. The SDK reads the key from `PROMIGENCE_API_KEY` or
17
- from where the CLI stored it: the system keychain, or `~/.config/promigence/credentials`.
16
+ `npm i -g promigence`, then `promigence signup`. The SDK uses the key you pass (`apiKey`) or
17
+ `PROMIGENCE_API_KEY`; with neither, the key the CLI stored: the system keychain, or
18
+ `~/.config/promigence/credentials`. A key you pass or set is used as is; the stored one is not read.
18
19
 
19
20
  ## Use it
20
21
 
@@ -253,6 +253,11 @@ export interface Totals {
253
253
  grader_failed?: number;
254
254
  snapshot_failed: number;
255
255
  aborted_by_client: number;
256
+ /**
257
+ * Sandboxes Promigence support stopped (outcome `stopped_by_support`), each with its reason in its
258
+ * failure's message. Never counted as your own kill (`aborted_by_client`). Absent from an older API.
259
+ */
260
+ stopped_by_support?: number;
256
261
  expired: number;
257
262
  cap_killed: number;
258
263
  not_started: number;
@@ -349,7 +354,9 @@ export interface SecretRecord {
349
354
  name: string;
350
355
  /** Length of the stored value, so a caller can sanity-check without reading it. */
351
356
  bytes: number;
357
+ /** On `secrets.set` only: `false` when an existing secret of that name was overwritten. */
352
358
  created?: boolean;
359
+ /** On `secrets.list` only. */
353
360
  created_at?: string;
354
361
  updated_at?: string;
355
362
  /**
@@ -370,7 +377,7 @@ export interface SecretRecord {
370
377
  * `api.anthropic.com`. Absent when there is none. A variable the caller sets itself wins.
371
378
  */
372
379
  env?: Record<string, string>;
373
- /** The request headers the secret authenticates (present when `hosts` is). */
380
+ /** The request headers the secret authenticates: on `secrets.list`, when `hosts` is present. */
374
381
  headers?: string[];
375
382
  /** What the variable in a sandbox that names it holds instead of the value (on `set` of a bound secret). */
376
383
  placeholder?: string;
@@ -467,13 +474,17 @@ export interface SnapshotRecord {
467
474
  source?: SnapshotSource;
468
475
  /** `false` for an image snapshot built without `verify` — stored, but nothing was verified. */
469
476
  verified?: boolean;
470
- start?: string | string[];
471
477
  created_at?: string;
472
478
  /**
473
479
  * Set when the snapshot can no longer be started, saying why (it was lost before it could be
474
480
  * kept); a fork of it is refused `snapshot_unavailable`. Absent while it can be started.
475
481
  */
476
482
  unavailable?: string;
483
+ /**
484
+ * The user commands, terminals and SSH sessions in its sandboxes run as when they name none:
485
+ * its image's own `USER`, or the `user` it was created with. Absent: root.
486
+ */
487
+ user?: string | null;
477
488
  [extra: string]: unknown;
478
489
  }
479
490
  /** `GET /snapshots/:id/manifest`: the snapshot's manifest, sealed (`buildBundle` turns it into a `.promigence` bundle). */
@@ -510,8 +521,6 @@ export interface ExecResponse {
510
521
  stdout: string;
511
522
  stderr: string;
512
523
  truncated?: boolean;
513
- stdout_ref?: string;
514
- stderr_ref?: string;
515
524
  /** True when the command was killed at its own timeout. */
516
525
  timed_out?: boolean;
517
526
  /** True when the command was killed for running out of memory. */
@@ -539,7 +548,13 @@ export interface SandboxRecord extends QueuedInfo {
539
548
  */
540
549
  pause_reason?: "spend_cap" | "credits_exhausted";
541
550
  run_id?: string;
551
+ /**
552
+ * The snapshot it was started from, as its run names it (`runs.get(runId).snapshot_id`): the
553
+ * snapshot's id, or `promigence/base` for the built-in base.
554
+ */
542
555
  snapshot_id?: string;
556
+ /** When the sandbox was created, RFC 3339. */
557
+ created_at?: string;
543
558
  outcome?: Outcome;
544
559
  /**
545
560
  * The labels this sandbox inherited from its run — the same map `runs.start({ labels })` took.
@@ -627,7 +642,7 @@ export interface Page<T> {
627
642
  /** What the server actually used, which is not always what was asked for (it caps at 500). */
628
643
  limit: number;
629
644
  }
630
- /** `GET /health` — unauthenticated: `ok`, `version`, `build_sha` and `serving`. */
645
+ /** `GET /health` — unauthenticated: `ok`, `version`, `serving` and `location`; `build_sha` to a caller with a key. */
631
646
  export interface HealthResponse {
632
647
  ok?: boolean;
633
648
  version?: string;
@@ -733,6 +748,8 @@ export interface CreditOffer {
733
748
  state: string;
734
749
  credit_usd: number;
735
750
  card_bonus_usd: number;
751
+ /** USD charged to confirm Apple Pay, Google Pay or Amazon Pay when one is added (added to the credit). */
752
+ method_check_usd?: number;
736
753
  expires_at?: string | null;
737
754
  campaign?: string | null;
738
755
  message: string;
@@ -891,6 +908,16 @@ export interface BillingUsage {
891
908
  brand?: string;
892
909
  last4?: string;
893
910
  } | null;
911
+ /** Set when the card on file does not unlock paid limits for this organization (it already pays for another one,
912
+ * it is being confirmed with a $1 charge, or that charge did not go through). */
913
+ card_note?: string | null;
914
+ /** The $1 that confirms Apple Pay, Google Pay or Amazon Pay on file: `confirming`, `paid` or `failed`. */
915
+ method_check?: {
916
+ method: string;
917
+ status: string;
918
+ amount_usd: number;
919
+ bonus_usd?: number | null;
920
+ } | null;
894
921
  suspended?: boolean;
895
922
  suspend_reason?: string | null;
896
923
  period?: {
@@ -914,6 +941,10 @@ export interface BillingUsage {
914
941
  upcoming_invoice_error?: string | null;
915
942
  [extra: string]: unknown;
916
943
  }
944
+ /** One row of `GET /billing/history`: a credit the organization received, or a payment. */
945
+ export type BillingHistoryEntry = M.BillingHistoryEntry;
946
+ /** What `billing.historyPage()` lists: everything, only credits, or only payments. */
947
+ export type BillingHistoryKind = "all" | "credits" | "payments";
917
948
  /** One metered row of `GET /me/ledger`. */
918
949
  export interface LedgerEntry {
919
950
  entry_id: string;
@@ -1184,9 +1215,15 @@ export interface SearchOpts {
1184
1215
  * `POST /sandboxes/:id/files/archive` — where the tar landed **inside the sandbox** and how big
1185
1216
  * it is. The bytes stay in the sandbox on purpose; see `files.downloadDir`.
1186
1217
  */
1187
- export interface ArchiveResult extends FileInfo {
1218
+ export interface ArchiveResult {
1219
+ sandbox_id: string;
1220
+ /** Where the tar landed inside the sandbox. */
1188
1221
  archive_path: string;
1222
+ /** Its size in bytes. */
1223
+ size: number;
1224
+ /** The directory that was packed. */
1189
1225
  source: string;
1226
+ served_by?: ServedBy;
1190
1227
  }
1191
1228
  /** `POST /sandboxes/:id/files/extract`. `entries` is `null` on the fallback path, which cannot
1192
1229
  * count them without a second pass — a number invented there would be worse than none. */
@@ -1312,8 +1349,6 @@ export interface MeResponse {
1312
1349
  email?: string;
1313
1350
  provider?: string;
1314
1351
  } | null;
1315
- /** @deprecated Never sent: the key is `key.key_id`. */
1316
- key_id?: string;
1317
1352
  /**
1318
1353
  * The plan the organisation's limits and prices come from: `hobby` for a trial organisation
1319
1354
  * with no plan assigned. The name `tier_over_plan` and the other refusals use.
@@ -1349,7 +1384,6 @@ export interface MeResponse {
1349
1384
  /** The cap a run gets when it names none, in USD; `0` names none (the run is capped at its quote). */
1350
1385
  default_spend_cap?: number;
1351
1386
  card_on_file?: boolean;
1352
- default_class?: string;
1353
1387
  /** The free trial: credits, data out and the limits that apply until a card is on file. */
1354
1388
  trial?: TrialStatus;
1355
1389
  /** Something about the account to act on (the card-required switch), or null. */
@@ -1661,6 +1695,18 @@ export interface SnapshotCreateOpts {
1661
1695
  tier?: Tier;
1662
1696
  /** Image forms: the name of a stored secret holding the registry credentials. */
1663
1697
  registrySecret?: string;
1698
+ /**
1699
+ * Image and Dockerfile forms: the user commands, terminals and SSH sessions in its sandboxes
1700
+ * run as when they name none. A user name or uid, with an optional `:group` or `:gid`, as an
1701
+ * image's `USER` takes it; `root` for root. Default: the image's own `USER`, as `docker run`
1702
+ * does (root when it sets none).
1703
+ */
1704
+ user?: string;
1705
+ /**
1706
+ * Image and Dockerfile forms: the user its `start`, `setup` and `verify` run as (for example
1707
+ * the snapshot's own `user`). Default: `root`.
1708
+ */
1709
+ setupUser?: string;
1664
1710
  alias?: string;
1665
1711
  /**
1666
1712
  * A DEFAULT grader for this snapshot: a run on it with no `grade` of its own uses this one.
@@ -1951,12 +1997,15 @@ export declare class PromigenceClient {
1951
1997
  timeoutMs?: number;
1952
1998
  }): Promise<StatusResponse>;
1953
1999
  /**
1954
- * `GET /pricing` — the price list, live: the per-second rates, every sandbox shape on sale and
1955
- * its price, the build price, the limits, the free credit and the plan they come from, read on
1956
- * every call from what billing charges. With a key: what this key's organization pays. Without
1957
- * one, or with `newAccount: true` (no key is sent): what a new account gets.
2000
+ * `GET /pricing` — your organization's price list, live: the per-second rates, every sandbox
2001
+ * shape on sale and its price, the build price, the limits, the free credit and the plan they
2002
+ * come from, read on every call from what billing charges. Needs a key: without one this throws
2003
+ * `AuthError` and sends nothing. The published list prices are on the pricing page.
1958
2004
  *
1959
2005
  * Prices change: read them when you need them rather than keeping a copy.
2006
+ *
2007
+ * @param opts.newAccount No longer supported: there is no key-less price list. Passing `true`
2008
+ * throws `InvalidArgumentError` and sends nothing.
1960
2009
  */
1961
2010
  pricing(opts?: {
1962
2011
  newAccount?: boolean;
@@ -2000,6 +2049,16 @@ export declare class PromigenceClient {
2000
2049
  limit?: number;
2001
2050
  cursor?: string;
2002
2051
  }) => Promise<Page<LedgerEntry>>;
2052
+ /**
2053
+ * `GET /billing/history` — the credits the organization received and its payments, newest
2054
+ * first (one page). `kind` narrows it to `credits` or `payments`; feed `nextCursor` back as
2055
+ * `cursor` until it is `null`. An owner's key.
2056
+ */
2057
+ historyPage: (opts?: {
2058
+ kind?: BillingHistoryKind;
2059
+ limit?: number;
2060
+ cursor?: string;
2061
+ }) => Promise<Page<BillingHistoryEntry>>;
2003
2062
  /** `POST /billing/checkout` — the hosted page that puts a card on file. */
2004
2063
  checkout: () => Promise<{
2005
2064
  url: string;
@@ -2400,6 +2459,18 @@ export declare class PromigenceClient {
2400
2459
  onData?: (data: Uint8Array) => void;
2401
2460
  onExit?: (exit: PtyExit) => void;
2402
2461
  timeoutMs?: number;
2462
+ /** Pick the same terminal up again when its connection drops or the service is updated;
2463
+ * see `PtyConnectOpts.resume`. Default: on. */
2464
+ resume?: boolean;
2465
+ /** Reconnect in a new shell when the service is updated (a WebSocket `Close` 1012) and the
2466
+ * terminal cannot be picked up again; see `PtyConnectOpts.reconnect`. Default: on without
2467
+ * `cmd`, off with one. */
2468
+ reconnect?: boolean;
2469
+ /** Called after each reconnect: `resumed` says whether it is the same terminal or a new
2470
+ * shell. */
2471
+ onReconnected?: (how: {
2472
+ resumed: boolean;
2473
+ }) => void;
2403
2474
  }) => Promise<PtySession>;
2404
2475
  /** `GET /sandboxes/:id`. */
2405
2476
  get: (sandboxId: string) => Promise<SandboxRecord>;
@@ -2475,6 +2546,12 @@ export declare class PromigenceClient {
2475
2546
  /**
2476
2547
  * `POST /sandboxes/:id/probe` — is this environment still the one that was verified?
2477
2548
  *
2549
+ * Compares the sandbox's files under the paths its snapshot's verification recorded with what
2550
+ * was recorded. It is the same check a failed run gets, which decides whether the failure was
2551
+ * the environment's (`integrity_mismatch`, not billed) or a path the snapshot's own `verify`
2552
+ * never covered (`verify_did_not_cover`, billed). Asking for it up front is cheaper than
2553
+ * spending a run to find out.
2554
+ *
2478
2555
  * `first_bad_path` names the **first differing path**, which is the only part of a failed
2479
2556
  * probe anyone can act on.
2480
2557
  */
@@ -2485,8 +2562,7 @@ export declare class PromigenceClient {
2485
2562
  ok: boolean;
2486
2563
  ms: number;
2487
2564
  checked: string | string[];
2488
- first_bad_path: string | null;
2489
- probe: Record<string, unknown>;
2565
+ first_bad_path?: string | null;
2490
2566
  }>;
2491
2567
  /** `DELETE /sandboxes/:id`. */
2492
2568
  kill: (sandboxId: string) => Promise<unknown>;
@@ -2623,9 +2699,11 @@ export declare class PromigenceClient {
2623
2699
  /**
2624
2700
  * Write a file of **any size**, in [`FILE_PUT_CHUNK_BYTES`] requests.
2625
2701
  *
2626
- * A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file, and the
2627
- * thrown error carries how far it got (`written` in the error's evidence).
2628
- * Re-running the same `put` is safe: chunk 0 truncates.
2702
+ * Chunk 0 goes without `?offset=`, which means "this is the file" (create, truncate);
2703
+ * every later chunk carries one, which appends, so a file has no size limit here.
2704
+ *
2705
+ * A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file. Re-running the
2706
+ * same `put` is safe: chunk 0 truncates.
2629
2707
  *
2630
2708
  * `mode` (`0o755`) sets the file's permission bits. Without it a file that already exists
2631
2709
  * keeps its mode, as a shell `>` does, and a new one is `0o644`.
@@ -2735,8 +2813,7 @@ export declare class PromigenceClient {
2735
2813
  ok: boolean;
2736
2814
  ms: number;
2737
2815
  checked: string | string[];
2738
- first_bad_path: string | null;
2739
- probe: Record<string, unknown>;
2816
+ first_bad_path?: string | null;
2740
2817
  }>;
2741
2818
  };
2742
2819
  /**
@@ -2883,15 +2960,14 @@ export declare class PromigenceClient {
2883
2960
  manifest: (idOrAlias: string) => Promise<SnapshotManifestResponse>;
2884
2961
  /**
2885
2962
  * The `.promigence` bundle for a snapshot: the sealed form, which names exactly one
2886
- * environment and verifies offline (`verifyBundle`). `served_by` says whether the API wrote it
2887
- * (`control`) or this client composed it from the snapshot's manifest (`client`); both are the
2888
- * same document.
2963
+ * environment and verifies offline (`verifyBundle`). This client composes it from the
2964
+ * snapshot's sealed manifest (`served_by: "client"`); the API serves no bundle of its own.
2889
2965
  */
2890
2966
  bundle: (idOrAlias: string, opts?: {
2891
2967
  alias?: string;
2892
2968
  }) => Promise<{
2893
2969
  bundle: Bundle;
2894
- served_by: "control" | "client";
2970
+ served_by: "client";
2895
2971
  record?: SnapshotRecord;
2896
2972
  }>;
2897
2973
  };
@@ -168,7 +168,7 @@ function decodeCredentials(buf) {
168
168
  return buf.subarray(3).toString("utf8");
169
169
  return buf.toString("utf8");
170
170
  }
171
- function storedCredentials(env = process.env) {
171
+ function storedCredentials(env = process.env, platform = process.platform) {
172
172
  const path = credentialsPath(env);
173
173
  let present = true;
174
174
  try {
@@ -178,7 +178,7 @@ function storedCredentials(env = process.env) {
178
178
  present = e.code !== "ENOENT";
179
179
  }
180
180
  if (!present) {
181
- const store = (0, credstore_js_1.keychainStore)(env);
181
+ const store = (0, credstore_js_1.keychainStore)(env, platform);
182
182
  if (store === undefined)
183
183
  return undefined;
184
184
  const r = (0, credstore_js_1.keychainReadCached)(store, path, env);
@@ -292,8 +292,6 @@ function keyOrigin(env = process.env) {
292
292
  const stored = readCredentialsWithStore(env);
293
293
  if (stored.creds.api_key)
294
294
  return stored.store === "macos-keychain" ? "macOS Keychain" : stored.store === "secret-service" ? "Secret Service keyring" : "credentials file";
295
- if (compatApiKey(env["E2B_API_KEY"]))
296
- return "E2B_API_KEY (compat)";
297
295
  return undefined;
298
296
  }
299
297
  function resolveCredentials(opts, env = process.env) {
@@ -302,17 +300,19 @@ function resolveCredentials(opts, env = process.env) {
302
300
  const t = u.replace(/\/+$/, "");
303
301
  return /^https?:\/\/[^/?#]+$/i.test(t) ? `${t}/v1` : t;
304
302
  };
305
- const { creds, store } = readCredentialsWithStore(env);
306
303
  const envKey = set(env["PROMIGENCE_API_KEY"]?.trim());
307
304
  const envUrl = set(env["PROMIGENCE_API_URL"]);
308
305
  const optKey = opts.apiKey?.trim();
306
+ const given = optKey !== undefined || envKey !== undefined;
307
+ const { creds, store } = given
308
+ ? { creds: {}, store: undefined }
309
+ : readCredentialsWithStore(env);
309
310
  const source = optKey !== undefined ? "opts" : envKey !== undefined ? "env" : creds.api_key !== undefined ? "file" : undefined;
310
311
  const ours = optKey ?? envKey ?? creds.api_key;
311
- const compat = ours === undefined ? compatApiKey(env["E2B_API_KEY"]) : undefined;
312
- const compatUrl = compat === undefined
313
- ? undefined
314
- : apiUrlFromCompatUrl(env["E2B_API_URL"]) ?? apiUrlFromCompatDomain(env["E2B_DOMAIN"]);
315
- const fromFile = trim(creds.api_url ?? compatUrl ?? core_js_1.DEFAULT_API_URL);
312
+ const fallback = ours === undefined ? opts.fallbackKey : undefined;
313
+ const fallbackKey = set(fallback?.apiKey.trim());
314
+ const fallbackUrl = fallbackKey === undefined ? undefined : set(fallback?.apiUrl);
315
+ const fromFile = trim(creds.api_url ?? fallbackUrl ?? core_js_1.DEFAULT_API_URL);
316
316
  const fromEnv = envUrl !== undefined ? trim(envUrl) : fromFile;
317
317
  const apiUrl = opts.apiUrl !== undefined ? trim(opts.apiUrl) : fromEnv;
318
318
  const paired = source === "file" ? fromFile : source === "env" ? fromEnv : apiUrl;
@@ -324,10 +324,10 @@ function resolveCredentials(opts, env = process.env) {
324
324
  : "Pass apiKey together with apiUrl, or set PROMIGENCE_API_URL to that URL instead of passing apiUrl.");
325
325
  return { apiKey: undefined, apiUrl, refusal };
326
326
  }
327
- const key = ours ?? compat;
327
+ const key = ours ?? fallbackKey;
328
328
  if (key) {
329
329
  const from = source === "opts" ? "the apiKey option" : source === "env" ? "PROMIGENCE_API_KEY"
330
- : source === "file" ? storedName(store, env) : "E2B_API_KEY";
330
+ : source === "file" ? storedName(store, env) : fallback.from;
331
331
  const refusal = keyRefusal(key, from);
332
332
  if (refusal)
333
333
  return { apiKey: undefined, apiUrl, refusal };
@@ -540,6 +540,12 @@ function snapshotCreateBody(opts) {
540
540
  if (o.env !== undefined && Object.keys(o.env).length && forms[0] === "repo") {
541
541
  throw new core_js_1.InvalidArgumentError("snapshots.create env is for an image or a Dockerfile", "A repository snapshot sets its environment in its setup steps, or pass env on each command.");
542
542
  }
543
+ if ((o.user !== undefined || o.setupUser !== undefined) && forms[0] === "repo") {
544
+ throw new core_js_1.InvalidArgumentError("snapshots.create user and setupUser are for an image or a Dockerfile", "A repository snapshot runs as root; name a user on each command instead ({ user: 'app' }).");
545
+ }
546
+ if (o.setupUser !== undefined && o.user === undefined) {
547
+ throw new core_js_1.InvalidArgumentError("snapshots.create setupUser needs user", "Pass { user: 'app', setupUser: 'app' }: start, setup and verify as app too (without setupUser they run as root), every later command as app.");
548
+ }
543
549
  if (!github && (o.ref !== undefined || o.from !== undefined || o.track)) {
544
550
  throw new core_js_1.InvalidArgumentError("snapshots.create ref, from and track are for a connected repository (repo: 'github:owner/name' or 'gitlab:group/project')", "Pass { repo: 'github:your-org/app', ref: 'main' }, or drop them for this form.");
545
551
  }
@@ -549,7 +555,7 @@ function snapshotCreateBody(opts) {
549
555
  setup: o.setup, verify, tier: o.tier, alias: o.alias,
550
556
  others: [
551
557
  ["sources", Boolean(o.sources?.length)], ["start", o.start !== undefined],
552
- ["registrySecret", Boolean(o.registrySecret)], ["cpuClass", Boolean(o.cpuClass)],
558
+ ["registrySecret", Boolean(o.registrySecret)],
553
559
  ],
554
560
  });
555
561
  }
@@ -597,11 +603,13 @@ function snapshotCreateBody(opts) {
597
603
  body["registry_secret"] = o.registrySecret;
598
604
  if (o.env !== undefined && Object.keys(o.env).length)
599
605
  body["env"] = { ...o.env };
606
+ if (o.user !== undefined)
607
+ body["user"] = o.user;
608
+ if (o.setupUser !== undefined)
609
+ body["setup_user"] = o.setupUser;
600
610
  }
601
611
  if (o.alias)
602
612
  body["alias"] = o.alias;
603
- if (o.cpuClass)
604
- body["cpu_class"] = o.cpuClass;
605
613
  if (o.grade) {
606
614
  if (!o.grade.cmd)
607
615
  throw new core_js_1.InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
@@ -677,6 +685,7 @@ function errorFor(code, message, fix, extra = {}) {
677
685
  case "invalid_argument":
678
686
  return attach(new core_js_1.InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
679
687
  case "not_released": return attach(new core_js_1.NotReleasedError(message));
688
+ case "service_restart": return attach(new core_js_1.ServiceRestartError(message, fix));
680
689
  default:
681
690
  if (ENV_CODES.has(code) || extra.category === "env")
682
691
  return attach(new core_js_1.EnvFailedError(code, message, fix));
@@ -1240,12 +1249,14 @@ class PromigenceClient {
1240
1249
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1241
1250
  });
1242
1251
  }
1243
- pricing(opts = {}) {
1244
- const anonymous = opts.newAccount === true || !this.authenticated;
1252
+ async pricing(opts = {}) {
1253
+ if (opts.newAccount === true) {
1254
+ throw new core_js_1.InvalidArgumentError("pricing({ newAccount: true }) is no longer supported: the price list needs a key", "Call pricing() with your API key for your organization's prices. The published list prices are on the pricing page, and `promigence pricing` prints them without a key.");
1255
+ }
1256
+ this.requireKey();
1245
1257
  return this.request({
1246
1258
  method: "GET",
1247
1259
  path: "/pricing",
1248
- ...(anonymous ? { anonymous: true } : {}),
1249
1260
  ...(opts.signal ? { signal: opts.signal } : {}),
1250
1261
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1251
1262
  });
@@ -1281,6 +1292,14 @@ class PromigenceClient {
1281
1292
  const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
1282
1293
  return pageOf(r, "entries", limit);
1283
1294
  },
1295
+ historyPage: async (opts = {}) => {
1296
+ const limit = opts.limit ?? 50;
1297
+ const q = new URLSearchParams(pageQuery(limit, 100, opts.cursor));
1298
+ if (opts.kind !== undefined && opts.kind !== "all")
1299
+ q.set("kind", opts.kind);
1300
+ const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
1301
+ return pageOf(r, "entries", limit);
1302
+ },
1284
1303
  checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1285
1304
  portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1286
1305
  referrals: () => this.request({ method: "GET", path: "/referrals" }),
@@ -1775,6 +1794,10 @@ class PromigenceClient {
1775
1794
  on.heartbeat?.();
1776
1795
  continue;
1777
1796
  }
1797
+ if (event === "reconnect") {
1798
+ reader.cancel().catch(() => { });
1799
+ throw new core_js_1.ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
1800
+ }
1778
1801
  let parsed = {};
1779
1802
  try {
1780
1803
  parsed = JSON.parse(data);
@@ -2010,6 +2033,9 @@ class PromigenceClient {
2010
2033
  ...(opts.onData ? { onData: opts.onData } : {}),
2011
2034
  ...(opts.onExit ? { onExit: opts.onExit } : {}),
2012
2035
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2036
+ ...(opts.resume === undefined ? {} : { resume: opts.resume }),
2037
+ ...(opts.reconnect === undefined ? {} : { reconnect: opts.reconnect }),
2038
+ ...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
2013
2039
  refuse: (status, text, retryAfter) => {
2014
2040
  let payload = {};
2015
2041
  try {
@@ -2447,19 +2473,6 @@ class PromigenceClient {
2447
2473
  },
2448
2474
  manifest: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}/manifest` }),
2449
2475
  bundle: async (idOrAlias, opts = {}) => {
2450
- try {
2451
- const r = await this.request({
2452
- method: "GET", path: `/snapshots/${seg(idOrAlias)}/bundle`,
2453
- });
2454
- if (r?.bundle?.schema === bundle_js_1.BUNDLE_SCHEMA) {
2455
- return { bundle: r.bundle, served_by: "control" };
2456
- }
2457
- }
2458
- catch (e) {
2459
- const code = e?.code;
2460
- if (code !== "not_found" && code !== "enoent" && e?.status !== 405)
2461
- throw e;
2462
- }
2463
2476
  const [record, m] = await Promise.all([
2464
2477
  this.snapshots.get(idOrAlias).catch(() => undefined),
2465
2478
  this.snapshots.manifest(idOrAlias),
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.6";
2
+ export declare const VERSION = "0.1.0-beta.8";
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";
@@ -190,6 +190,15 @@ export declare class TimeoutError extends PromigenceError {
190
190
  runId?: string;
191
191
  });
192
192
  }
193
+ /**
194
+ * A service update may stop a command that was running while its output was streaming to you
195
+ * (`execStream`, `commands.run` with output callbacks); the time it ran is credited back, and the
196
+ * sandbox keeps running. `retryable` is `false`: the command may have done part of its work before
197
+ * it stopped, so run it again only if that is safe. exit 3
198
+ */
199
+ export declare class ServiceRestartError extends PromigenceError {
200
+ constructor(message: string, fix: string);
201
+ }
193
202
  /** An API error with no more specific class (5xx, unknown code). exit from the table. */
194
203
  export declare class ApiError extends PromigenceError {
195
204
  }
package/dist/cjs/core.js CHANGED
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EXIT_BY_CODE = exports.NotReleasedError = exports.ApiError = 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.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;
4
4
  exports.previewsFix = previewsFix;
5
5
  exports.withPreviewsFix = withPreviewsFix;
6
6
  exports.fixBeyondMessage = fixBeyondMessage;
7
7
  exports.previewsOff = previewsOff;
8
8
  exports.exitForCode = exitForCode;
9
9
  const hint_js_1 = require("./hint.js");
10
- exports.VERSION = "0.1.0-beta.6";
10
+ exports.VERSION = "0.1.0-beta.8";
11
11
  exports.DOCS_URL = "https://promigence.ai";
12
12
  exports.ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
13
13
  exports.DEFAULT_API_URL = "https://api.promigence.ai/v1";
@@ -215,6 +215,12 @@ class TimeoutError extends PromigenceError {
215
215
  }
216
216
  }
217
217
  exports.TimeoutError = TimeoutError;
218
+ class ServiceRestartError extends PromigenceError {
219
+ constructor(message, fix) {
220
+ super({ code: "service_restart", message, fix, exitCode: 3, category: "env", retryable: false });
221
+ }
222
+ }
223
+ exports.ServiceRestartError = ServiceRestartError;
218
224
  class ApiError extends PromigenceError {
219
225
  }
220
226
  exports.ApiError = ApiError;
@@ -233,6 +239,7 @@ exports.EXIT_BY_CODE = {
233
239
  invalid_argument: 2, unsupported: 2, not_released: 1, task_failed: 4,
234
240
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
235
241
  not_connected: 7,
242
+ service_restart: 3,
236
243
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
237
244
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
238
245
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
@@ -249,7 +256,7 @@ exports.EXIT_BY_CODE = {
249
256
  nonzero_exit: 4, grade_not_passed: 4, task_timeout: 4, oom_over_tier: 4, disk_over_tier: 4,
250
257
  shutdown_by_task: 4, modified_by_task: 4,
251
258
  cap_exceeded: 5, cap_killed: 12, cap_paused: 5, credits_paused: 5,
252
- aborted_by_client: 130, expired: 12, not_started: 12, superseded: 0,
259
+ aborted_by_client: 130, stopped_by_support: 12, expired: 12, not_started: 12, superseded: 0,
253
260
  usage: 2, internal: 1, auth: 7, not_found: 8, unreachable: 9, rate_limited: 10,
254
261
  timeout: 11, not_verified: 6, enoent: 8,
255
262
  snapshot_incompatible: 6, storage_limit_reached: 5,
@@ -259,10 +266,11 @@ exports.EXIT_BY_CODE = {
259
266
  build_timeout: 4,
260
267
  image_unresolvable: 2,
261
268
  registry_unavailable: 3,
262
- trial_credits_exhausted: 5, card_required: 5, credit_unclaimed: 5, concurrency_limit: 5, egress_cap_reached: 10,
269
+ trial_credits_exhausted: 5, card_required: 5, credit_unclaimed: 5, concurrency_limit: 5, egress_cap_reached: 5,
263
270
  billing_unavailable: 5,
264
271
  project_cap_reached: 5,
265
272
  spend_limit_reached: 5,
273
+ billing_hold: 5,
266
274
  tier_over_plan: 2,
267
275
  network_host_refused: 9, network_policy_unsupported: 3,
268
276
  secrets_in_snapshot: 2, secrets_unavailable: 3,
@@ -274,6 +282,8 @@ exports.EXIT_BY_CODE = {
274
282
  invite_invalid: 7,
275
283
  invite_required: 7,
276
284
  key_expired: 7,
285
+ account_blocked: 7,
286
+ setup_required: 7,
277
287
  };
278
288
  function exitForCode(code) {
279
289
  return Object.prototype.hasOwnProperty.call(exports.EXIT_BY_CODE, code) ? exports.EXIT_BY_CODE[code] : 1;
@@ -3,7 +3,8 @@
3
3
  * the credentials file (`~/.config/promigence/credentials`, 0600). The CLI writes it
4
4
  * (`promigence login`, `promigence auth login`, `promigence logout`); the CLI and both SDKs read it.
5
5
  *
6
- * macOS the login Keychain, through `security` (when HOME holds one)
6
+ * macOS the login Keychain, through `security` (when the user's home holds one: HOME,
7
+ * or the account's own home when the environment has no HOME)
7
8
  * Linux and BSD the Secret Service (GNOME Keyring, KWallet), through `secret-tool`, when a
8
9
  * desktop session bus is there (a CI job or an SSH shell has none: the file)
9
10
  * Windows the file: no tool that ships with Windows can read a stored secret back
@@ -34,7 +35,7 @@ export declare function storeLabel(store: CredentialStore, path?: string): strin
34
35
  * The keychain this process stores a sign-in in, or undefined for the file. `auto` is decided by
35
36
  * the platform and by whether its tool (and, for the Secret Service, a session bus) is there.
36
37
  */
37
- export declare function keychainStore(env?: NodeJS.ProcessEnv, platform?: string): KeychainStore | undefined;
38
+ export declare function keychainStore(env?: NodeJS.ProcessEnv, platform?: string, home?: () => string): KeychainStore | undefined;
38
39
  /**
39
40
  * The keychains a sign-out (or a sign-in that lands in the file) must clear for this path: the one
40
41
  * in use, and the platform's own even when `PROMIGENCE_CREDENTIAL_STORE=file` (an item a previous
@@ -13,6 +13,7 @@ exports.keychainCacheSet = keychainCacheSet;
13
13
  exports.forgetKeychainReads = forgetKeychainReads;
14
14
  const node_child_process_1 = require("node:child_process");
15
15
  const node_fs_1 = require("node:fs");
16
+ const node_os_1 = require("node:os");
16
17
  const node_path_1 = require("node:path");
17
18
  exports.KEYCHAIN_SERVICE = "promigence-cli";
18
19
  const KEYCHAIN_LABEL = "Promigence CLI credentials";
@@ -43,8 +44,19 @@ function sessionBus(env) {
43
44
  const run = env["XDG_RUNTIME_DIR"];
44
45
  return Boolean(run) && (0, node_fs_1.existsSync)((0, node_path_1.join)(run, "bus"));
45
46
  }
47
+ function userHome(env, home = node_os_1.homedir) {
48
+ const h = env["HOME"];
49
+ if (h)
50
+ return h;
51
+ try {
52
+ return home() || undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
46
58
  const warnedSetting = new Set();
47
- function keychainStore(env = process.env, platform = process.platform) {
59
+ function keychainStore(env = process.env, platform = process.platform, home = node_os_1.homedir) {
48
60
  const raw = (env["PROMIGENCE_CREDENTIAL_STORE"] ?? "").trim().toLowerCase();
49
61
  switch (raw) {
50
62
  case "file": return undefined;
@@ -59,8 +71,8 @@ function keychainStore(env = process.env, platform = process.platform) {
59
71
  }
60
72
  }
61
73
  if (platform === "darwin") {
62
- const home = env["HOME"];
63
- return home && (0, node_fs_1.existsSync)((0, node_path_1.join)(home, "Library", "Keychains")) && onPath("security", env) ? "macos-keychain" : undefined;
74
+ const h = userHome(env, home);
75
+ return h && (0, node_fs_1.existsSync)((0, node_path_1.join)(h, "Library", "Keychains")) && onPath("security", env) ? "macos-keychain" : undefined;
64
76
  }
65
77
  if (platform === "win32")
66
78
  return undefined;
@@ -107,8 +119,9 @@ function runTool(store, args, env, input, timeout) {
107
119
  const bin = onPath(toolName(store), env);
108
120
  if (bin === undefined)
109
121
  throw Object.assign(new Error(`${toolName(store)} not found`), { code: "ENOENT" });
122
+ const h = store === "macos-keychain" && !env["HOME"] ? userHome(env) : undefined;
110
123
  return (0, node_child_process_1.execFileSync)(bin, args, {
111
- encoding: "utf8", timeout, env,
124
+ encoding: "utf8", timeout, env: h ? { ...env, HOME: h } : env,
112
125
  ...(input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input, stdio: ["pipe", "pipe", "pipe"] }),
113
126
  });
114
127
  }