@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 +3 -2
- package/dist/cjs/client.d.ts +100 -24
- package/dist/cjs/client.js +44 -31
- package/dist/cjs/core.d.ts +10 -1
- package/dist/cjs/core.js +14 -4
- package/dist/cjs/credstore.d.ts +3 -2
- package/dist/cjs/credstore.js +17 -4
- package/dist/cjs/models.gen.d.ts +218 -22
- package/dist/cjs/proxy.js +2 -1
- package/dist/cjs/pty.d.ts +65 -3
- package/dist/cjs/pty.js +270 -34
- package/dist/cjs/sandbox.d.ts +34 -4
- package/dist/cjs/sandbox.js +3 -2
- package/dist/cjs/ssh.js +1 -0
- package/dist/client.d.ts +100 -24
- package/dist/client.js +46 -33
- package/dist/core.d.ts +10 -1
- package/dist/core.js +12 -3
- package/dist/credstore.d.ts +3 -2
- package/dist/credstore.js +17 -4
- package/dist/models.gen.d.ts +218 -22
- package/dist/proxy.js +2 -1
- package/dist/pty.d.ts +65 -3
- package/dist/pty.js +269 -33
- package/dist/sandbox.d.ts +34 -4
- package/dist/sandbox.js +3 -2
- package/dist/ssh.js +1 -0
- package/package.json +1 -1
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
|
|
17
|
-
|
|
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
|
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -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
|
|
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`, `
|
|
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
|
|
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` —
|
|
1955
|
-
* its price, the build price, the limits, the free credit and the plan they
|
|
1956
|
-
* every call from what billing charges.
|
|
1957
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2627
|
-
*
|
|
2628
|
-
*
|
|
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
|
|
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`).
|
|
2887
|
-
*
|
|
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: "
|
|
2970
|
+
served_by: "client";
|
|
2895
2971
|
record?: SnapshotRecord;
|
|
2896
2972
|
}>;
|
|
2897
2973
|
};
|
package/dist/cjs/client.js
CHANGED
|
@@ -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
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
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 ??
|
|
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) :
|
|
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)],
|
|
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
|
-
|
|
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),
|
package/dist/cjs/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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.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.
|
|
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:
|
|
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;
|
package/dist/cjs/credstore.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cjs/credstore.js
CHANGED
|
@@ -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
|
|
63
|
-
return
|
|
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
|
}
|