@promigence/sdk 0.1.0-beta.14 → 0.1.0-beta.15
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/cjs/client.d.ts +84 -14
- package/dist/cjs/client.js +126 -34
- package/dist/cjs/core.d.ts +1 -1
- package/dist/cjs/core.js +3 -1
- package/dist/cjs/models.gen.d.ts +599 -36
- package/dist/cjs/pty.d.ts +11 -14
- package/dist/cjs/pty.js +15 -12
- package/dist/client.d.ts +84 -14
- package/dist/client.js +125 -33
- package/dist/core.d.ts +1 -1
- package/dist/core.js +3 -1
- package/dist/models.gen.d.ts +599 -36
- package/dist/pty.d.ts +11 -14
- package/dist/pty.js +13 -11
- package/package.json +1 -1
package/dist/cjs/pty.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive terminals: `GET /v1/sandboxes/:id/pty` over a WebSocket (RFC 6455).
|
|
3
3
|
*
|
|
4
|
-
* The protocol:
|
|
4
|
+
* The protocol (`promigence.pty.v2`):
|
|
5
5
|
*
|
|
6
|
-
* client → server
|
|
6
|
+
* client → server first, one text frame `{"start":{"cmd"?,"secrets"?}}`: what to run and the secret
|
|
7
|
+
* names it gets, inside the connection, never in a header or the URL; then
|
|
8
|
+
* binary = keystrokes; text = `{"resize":{cols,rows}}` | `{"signal":N}` | `{"eof":true}`
|
|
7
9
|
* server → client binary = terminal output; text = `{"event":"open",…}` first and
|
|
8
|
-
* `{"event":"exit"|"error"|"closed",…}` last, then a Close frame
|
|
10
|
+
* `{"event":"exit"|"error"|"closed",…}` last, then a Close frame. A terminal that
|
|
11
|
+
* cannot open says so in its first frame: `{"event":"error","status",…}`, then Close.
|
|
9
12
|
*
|
|
10
13
|
* Zero dependencies, so this is a small client over `node:http` rather than the global
|
|
11
14
|
* `WebSocket`: that one cannot send an `Authorization` header, and a key in the query string is
|
|
@@ -32,26 +35,20 @@ export interface PtyExit {
|
|
|
32
35
|
message: string;
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
/** The
|
|
36
|
-
export declare const
|
|
37
|
-
/** The header the terminal's secret **names** travel in, base64 JSON. */
|
|
38
|
-
export declare const PTY_SECRETS_HEADER = "promigence-pty-secrets";
|
|
38
|
+
/** The WebSocket subprotocol a terminal speaks: its first message is `{"start": {"cmd"?, "secrets"?}}`. */
|
|
39
|
+
export declare const PTY_PROTOCOL = "promigence.pty.v2";
|
|
39
40
|
export interface PtyConnectOpts {
|
|
40
41
|
/** `http(s)://host[:port]/v1/sandboxes/<id>/pty?cols=&rows=` — dimensions only. */
|
|
41
42
|
url: string;
|
|
42
43
|
apiKey: string;
|
|
43
44
|
userAgent: string;
|
|
44
45
|
/**
|
|
45
|
-
* The command to run
|
|
46
|
-
*
|
|
47
|
-
* It used to be `?cmd=`, with `envs` composed into it as `export K=v;` — so a terminal opened
|
|
48
|
-
* with an API key in its environment wrote that key into the query string of every request,
|
|
49
|
-
* and therefore into every access log along the path. Names and values both stay out of the
|
|
50
|
-
* URL now.
|
|
46
|
+
* The command to run. Sent in the terminal's first message, inside the connection: never in a
|
|
47
|
+
* header or the URL, where a log on the way could keep it.
|
|
51
48
|
*/
|
|
52
49
|
command?: string;
|
|
53
50
|
/** Stored secret names (or `{name, as}` bindings), resolved server-side into the terminal's
|
|
54
|
-
* environment.
|
|
51
|
+
* environment. Sent in the first message with the command. */
|
|
55
52
|
secrets?: unknown[];
|
|
56
53
|
/** The `promigence-caller` label, when one is set (`setCaller`). */
|
|
57
54
|
caller?: string;
|
package/dist/cjs/pty.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PTY_RESTART_CLOSED_EXIT = exports.PTY_RESTART_EXIT = exports.PTY_RECONNECT_MAX_WAIT_MS = exports.PTY_RECONNECT_FIRST_WAIT_MS = exports.PTY_RECONNECT_FOR_MS = exports.PTY_RECONNECT_CODE = exports.PtySession = exports.FrameReader = exports.
|
|
3
|
+
exports.PTY_RESTART_CLOSED_EXIT = exports.PTY_RESTART_EXIT = exports.PTY_RECONNECT_MAX_WAIT_MS = exports.PTY_RECONNECT_FIRST_WAIT_MS = exports.PTY_RECONNECT_FOR_MS = exports.PTY_RECONNECT_CODE = exports.PtySession = exports.FrameReader = exports.PTY_PROTOCOL = exports.PTY_RESUME_HEADER = exports.PTY_INPUT_KEEP = exports.PTY_INPUT_CHUNK = void 0;
|
|
4
|
+
exports.ptyStartMessage = ptyStartMessage;
|
|
4
5
|
exports.wsAccept = wsAccept;
|
|
5
6
|
exports.encodeFrame = encodeFrame;
|
|
6
7
|
exports.connectPty = connectPty;
|
|
@@ -11,8 +12,10 @@ exports.PTY_INPUT_CHUNK = 64 * 1024;
|
|
|
11
12
|
exports.PTY_INPUT_KEEP = 256 * 1024;
|
|
12
13
|
exports.PTY_RESUME_HEADER = "promigence-resume";
|
|
13
14
|
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
14
|
-
exports.
|
|
15
|
-
|
|
15
|
+
exports.PTY_PROTOCOL = "promigence.pty.v2";
|
|
16
|
+
function ptyStartMessage(command, secrets) {
|
|
17
|
+
return JSON.stringify({ start: { ...(command ? { cmd: command } : {}), ...(secrets?.length ? { secrets } : {}) } });
|
|
18
|
+
}
|
|
16
19
|
function wsAccept(key) {
|
|
17
20
|
return (0, node_crypto_1.createHash)("sha1").update(key + WS_GUID).digest("base64");
|
|
18
21
|
}
|
|
@@ -258,15 +261,7 @@ function openLeg(opts, url, resume) {
|
|
|
258
261
|
authorization: `Bearer ${opts.apiKey}`,
|
|
259
262
|
"user-agent": opts.userAgent,
|
|
260
263
|
...(opts.caller ? { [caller_js_1.CALLER_HEADER]: opts.caller } : {}),
|
|
261
|
-
...(resume ? { [exports.PTY_RESUME_HEADER]: resume } : {}),
|
|
262
|
-
...(opts.command && !resume
|
|
263
|
-
? { [exports.PTY_COMMAND_HEADER]: Buffer.from(opts.command, "utf8").toString("base64") }
|
|
264
|
-
: {}),
|
|
265
|
-
...(opts.secrets?.length && !resume
|
|
266
|
-
? {
|
|
267
|
-
[exports.PTY_SECRETS_HEADER]: Buffer.from(JSON.stringify(opts.secrets), "utf8").toString("base64"),
|
|
268
|
-
}
|
|
269
|
-
: {}),
|
|
264
|
+
...(resume ? { [exports.PTY_RESUME_HEADER]: resume } : { "sec-websocket-protocol": exports.PTY_PROTOCOL }),
|
|
270
265
|
},
|
|
271
266
|
});
|
|
272
267
|
let resolveEnded;
|
|
@@ -299,6 +294,8 @@ function openLeg(opts, url, resume) {
|
|
|
299
294
|
return;
|
|
300
295
|
}
|
|
301
296
|
socket.setNoDelay(true);
|
|
297
|
+
if (!resume)
|
|
298
|
+
socket.write(encodeFrame(0x1, Buffer.from(ptyStartMessage(opts.command, opts.secrets), "utf8")));
|
|
302
299
|
const reader = new FrameReader();
|
|
303
300
|
let exit;
|
|
304
301
|
let closeCode;
|
|
@@ -336,6 +333,12 @@ function openLeg(opts, url, resume) {
|
|
|
336
333
|
...(typeof v["wall_ms"] === "number" ? { wallMs: v["wall_ms"] } : {}),
|
|
337
334
|
};
|
|
338
335
|
}
|
|
336
|
+
else if (v["event"] === "error" && !wasOpen && typeof v["status"] === "number") {
|
|
337
|
+
const e = (v["error"] && typeof v["error"] === "object" ? v["error"] : { code: v["code"], message: v["message"] });
|
|
338
|
+
fail(v["status"], JSON.stringify({ error: e }), typeof v["retry_after"] === "string" ? v["retry_after"] : undefined);
|
|
339
|
+
socket.destroy();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
339
342
|
else if (v["event"] === "error" || v["event"] === "closed") {
|
|
340
343
|
exit = {
|
|
341
344
|
exitCode: null, signal: null,
|
package/dist/client.d.ts
CHANGED
|
@@ -727,6 +727,10 @@ export interface OrgRecord {
|
|
|
727
727
|
projects: number;
|
|
728
728
|
members: number;
|
|
729
729
|
created_at: string;
|
|
730
|
+
/** Days command output is kept after a sandbox ends; `null` = the default, 7. */
|
|
731
|
+
output_retention_days?: number | null;
|
|
732
|
+
/** Keep a failed sandbox to reopen where it broke, when a run does not say; absent or `null` = the default. */
|
|
733
|
+
failure_captures?: boolean | null;
|
|
730
734
|
}
|
|
731
735
|
export interface ProjectRecord {
|
|
732
736
|
project_id: string;
|
|
@@ -736,8 +740,14 @@ export interface ProjectRecord {
|
|
|
736
740
|
keys: number;
|
|
737
741
|
/** The most the project may bill in a calendar month (UTC); `null` = no cap. */
|
|
738
742
|
monthly_cap_usd?: number | null;
|
|
743
|
+
/** Days this project's command output is kept after a sandbox ends; `null` = the organization's. */
|
|
744
|
+
output_retention_days?: number | null;
|
|
745
|
+
/** Keep this project's failed sandboxes to reopen, when a run does not say; absent or `null` = the organization's. */
|
|
746
|
+
failure_captures?: boolean | null;
|
|
739
747
|
created_at: string;
|
|
740
748
|
}
|
|
749
|
+
/** The longest command output may be kept after a sandbox ends, in days. */
|
|
750
|
+
export declare const OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
741
751
|
export interface MemberRecord {
|
|
742
752
|
user_id: string;
|
|
743
753
|
email: string;
|
|
@@ -905,8 +915,10 @@ export interface PricingView {
|
|
|
905
915
|
invite_required: boolean;
|
|
906
916
|
};
|
|
907
917
|
monthly_fee_usd: number;
|
|
908
|
-
/**
|
|
918
|
+
/** Credits the plan adds each paid month, USD. */
|
|
909
919
|
included_credits_usd: number;
|
|
920
|
+
/** For how many paid months the plan adds `included_credits_usd`; `null` when it adds them every paid month. */
|
|
921
|
+
included_credits_promo_months?: number | null;
|
|
910
922
|
monthly_minimum_usd: number;
|
|
911
923
|
}
|
|
912
924
|
export interface BillingUsage {
|
|
@@ -957,6 +969,20 @@ export interface BillingUsage {
|
|
|
957
969
|
period_end?: string | null;
|
|
958
970
|
} | null;
|
|
959
971
|
upcoming_invoice_error?: string | null;
|
|
972
|
+
/** What the organization's credits can pay for: balance, held, available, the next to expire. */
|
|
973
|
+
credits?: M.BillingCredits;
|
|
974
|
+
/** Auto-reload: when available credits fall below `threshold_usd`, `amount_usd` is added from `card`. */
|
|
975
|
+
auto_reload?: M.AutoReload;
|
|
976
|
+
/** The plan in force, and a renewal payment in trouble. */
|
|
977
|
+
plan_status?: M.PlanStatus;
|
|
978
|
+
/** `credits` (paid from credits bought in advance) or `postpaid` (billed after use, until `switch.at`). */
|
|
979
|
+
model?: string;
|
|
980
|
+
/** For a `postpaid` organization: when it moves to credits, and the credits ready then. */
|
|
981
|
+
switch?: M.BillingSwitch | null;
|
|
982
|
+
/** What this organization can buy now. */
|
|
983
|
+
trust?: M.TrustLimits;
|
|
984
|
+
/** A hold that refuses new work, and the page that lifts it; `null` when there is none. */
|
|
985
|
+
hold?: M.BillingHold | null;
|
|
960
986
|
[extra: string]: unknown;
|
|
961
987
|
}
|
|
962
988
|
/** One row of `GET /billing/history`: a credit the organization received, or a payment. */
|
|
@@ -1150,7 +1176,7 @@ export interface SnapshotVerifyResponse {
|
|
|
1150
1176
|
state: string;
|
|
1151
1177
|
}>;
|
|
1152
1178
|
}
|
|
1153
|
-
/** `
|
|
1179
|
+
/** `POST /sandboxes/:id/files/stat`. */
|
|
1154
1180
|
export interface FileStat {
|
|
1155
1181
|
path: string;
|
|
1156
1182
|
size: number;
|
|
@@ -1178,7 +1204,7 @@ export interface DirEntry {
|
|
|
1178
1204
|
}
|
|
1179
1205
|
/** An opaque label of how a file request was answered, passed through for support. */
|
|
1180
1206
|
export type ServedBy = string;
|
|
1181
|
-
/** `
|
|
1207
|
+
/** `POST /sandboxes/:id/files/list`. */
|
|
1182
1208
|
export interface DirListing {
|
|
1183
1209
|
sandbox_id: string;
|
|
1184
1210
|
path: string;
|
|
@@ -1187,7 +1213,7 @@ export interface DirListing {
|
|
|
1187
1213
|
truncated: boolean;
|
|
1188
1214
|
served_by?: ServedBy;
|
|
1189
1215
|
}
|
|
1190
|
-
/**
|
|
1216
|
+
/** `recursive`, `depth`, `limit` and `hidden` on `files/list` (sent in the request body). */
|
|
1191
1217
|
export interface DirListOpts {
|
|
1192
1218
|
recursive?: boolean;
|
|
1193
1219
|
/** With `recursive`: how many levels below `path`. Unset = no bound. */
|
|
@@ -1207,7 +1233,7 @@ export interface FileInfo {
|
|
|
1207
1233
|
is_symlink: boolean;
|
|
1208
1234
|
served_by?: ServedBy;
|
|
1209
1235
|
}
|
|
1210
|
-
/** One hit of `
|
|
1236
|
+
/** One hit of `POST /sandboxes/:id/files/search`. `line`/`text` only for a `contains` search. */
|
|
1211
1237
|
export interface FileMatch {
|
|
1212
1238
|
path: string;
|
|
1213
1239
|
line?: number | null;
|
|
@@ -1906,6 +1932,8 @@ interface RequestOpts {
|
|
|
1906
1932
|
onHeaders?: (headers: Headers) => void;
|
|
1907
1933
|
/** Extra request headers (e.g. `promigence-replaced-by`). Never the authorization. */
|
|
1908
1934
|
headers?: Record<string, string>;
|
|
1935
|
+
/** A `POST` that changes nothing (a file read): safe to send twice, so it is retried as a `GET` is. */
|
|
1936
|
+
replayable?: boolean;
|
|
1909
1937
|
}
|
|
1910
1938
|
/** One answer's timing ([`PromigenceClient.lastTiming`]). */
|
|
1911
1939
|
export interface Timing {
|
|
@@ -2011,13 +2039,16 @@ export declare class PromigenceClient {
|
|
|
2011
2039
|
* artefact must not go through: it would corrupt binary and cost a second copy of a file that
|
|
2012
2040
|
* can be gigabytes. This reads the body as an `ArrayBuffer` and hands it back untouched.
|
|
2013
2041
|
* Errors still arrive as JSON, so a non-2xx answer is decoded and mapped the same way.
|
|
2042
|
+
*
|
|
2043
|
+
* `body` is sent as given with its content type (a JSON question, or a `files put` form).
|
|
2044
|
+
* `read`: the request changes nothing, so a refusal is sent again as a `GET` would be.
|
|
2014
2045
|
*/
|
|
2015
2046
|
private bytes;
|
|
2016
2047
|
/**
|
|
2017
|
-
*
|
|
2048
|
+
* Read a byte body **without holding it**: each chunk goes to `onChunk` as it arrives, and
|
|
2018
2049
|
* the next is not read until `onChunk` has finished with it, so a 10 GiB artefact costs one
|
|
2019
|
-
* chunk of memory.
|
|
2020
|
-
* as JSON and are mapped exactly as [`bytes`] maps them
|
|
2050
|
+
* chunk of memory. A read only (a `POST` carries its question as `body`), so a door refusal
|
|
2051
|
+
* is sent again. Errors arrive as JSON and are mapped exactly as [`bytes`] maps them.
|
|
2021
2052
|
*/
|
|
2022
2053
|
private stream;
|
|
2023
2054
|
/**
|
|
@@ -2075,7 +2106,7 @@ export declare class PromigenceClient {
|
|
|
2075
2106
|
signal?: AbortSignal;
|
|
2076
2107
|
}): Promise<Outcomes>;
|
|
2077
2108
|
readonly billing: {
|
|
2078
|
-
/** `GET /billing` — plan, card, period, usage, trial,
|
|
2109
|
+
/** `GET /billing` — plan, credits, auto-reload, card, period, usage, trial, the next plan invoice. */
|
|
2079
2110
|
usage: () => Promise<BillingUsage>;
|
|
2080
2111
|
/** `GET /me/ledger?limit=N` — the metered rows the month's total is the sum of (one page). */
|
|
2081
2112
|
ledger: (limit?: number) => Promise<LedgerEntry[]>;
|
|
@@ -2097,6 +2128,29 @@ export declare class PromigenceClient {
|
|
|
2097
2128
|
limit?: number;
|
|
2098
2129
|
cursor?: string;
|
|
2099
2130
|
}) => Promise<Page<BillingHistoryEntry>>;
|
|
2131
|
+
/** `GET /billing/holds` — what each running or paused sandbox holds of the organization's credits. */
|
|
2132
|
+
holds: () => Promise<M.CreditHolds>;
|
|
2133
|
+
/**
|
|
2134
|
+
* `POST /billing/credits` — a payment page to buy credits, from $5, in whole dollars. The credits are
|
|
2135
|
+
* added when the payment goes through: `settleCredits(session_id)` checks it. An owner's full-access key.
|
|
2136
|
+
*/
|
|
2137
|
+
buyCredits: (amountUsd: number) => Promise<M.PaymentPage>;
|
|
2138
|
+
/** `POST /billing/credits/:session_id/settle` — check a credits payment and add its credits if it went through. */
|
|
2139
|
+
settleCredits: (sessionId: string) => Promise<M.CreditsSettled>;
|
|
2140
|
+
/** `GET /billing/auto-reload` — the auto-reload settings, and the authorization turning it on asks for. */
|
|
2141
|
+
autoReload: () => Promise<M.AutoReload>;
|
|
2142
|
+
/**
|
|
2143
|
+
* `PUT /billing/auto-reload` — turn auto-reload on or off, or change it. Turning it on, or changing the level or
|
|
2144
|
+
* the amount, needs `authorizationVersion`: the version of the authorization the owner accepted.
|
|
2145
|
+
*/
|
|
2146
|
+
setAutoReload: (opts: {
|
|
2147
|
+
on: boolean;
|
|
2148
|
+
thresholdUsd?: number;
|
|
2149
|
+
amountUsd?: number;
|
|
2150
|
+
authorizationVersion?: string;
|
|
2151
|
+
}) => Promise<M.AutoReload>;
|
|
2152
|
+
/** `POST /billing/disputes/:dispute_id/repay` — a payment page to repay a disputed payment. */
|
|
2153
|
+
repayDispute: (disputeId: string) => Promise<M.PaymentPage>;
|
|
2100
2154
|
/** `POST /billing/checkout` — the hosted page that puts a card on file. */
|
|
2101
2155
|
checkout: () => Promise<{
|
|
2102
2156
|
url: string;
|
|
@@ -2109,7 +2163,7 @@ export declare class PromigenceClient {
|
|
|
2109
2163
|
}>;
|
|
2110
2164
|
/** `GET /referrals` — your code and its offer, your referrals by status, what they earned. */
|
|
2111
2165
|
referrals: () => Promise<ReferralStatus>;
|
|
2112
|
-
/** `POST /referrals/apply` — apply a referral code (an owner's key, before the first
|
|
2166
|
+
/** `POST /referrals/apply` — apply a referral code (an owner's key, before the first payment of at least the reward). */
|
|
2113
2167
|
applyReferral: (code: string) => Promise<ReferralApplied>;
|
|
2114
2168
|
};
|
|
2115
2169
|
private group_billing;
|
|
@@ -2173,6 +2227,15 @@ export declare class PromigenceClient {
|
|
|
2173
2227
|
create: (org: string, name: string) => Promise<ProjectRecord>;
|
|
2174
2228
|
/** `PATCH /orgs/:org/projects/:project {name}` — rename it (the `default` project keeps its name). */
|
|
2175
2229
|
rename: (org: string, project: string, name: string) => Promise<ProjectRecord>;
|
|
2230
|
+
/**
|
|
2231
|
+
* `PATCH /orgs/:org/projects/:project` — rename it, set how long its command output is kept, or
|
|
2232
|
+
* whether its failed sandboxes are kept to reopen (that one on its own). The settings need an owner signed in.
|
|
2233
|
+
*/
|
|
2234
|
+
update: (org: string, project: string, update: {
|
|
2235
|
+
name?: string;
|
|
2236
|
+
outputRetentionDays?: number | null;
|
|
2237
|
+
failureCaptures?: boolean | null;
|
|
2238
|
+
}) => Promise<ProjectRecord>;
|
|
2176
2239
|
};
|
|
2177
2240
|
};
|
|
2178
2241
|
private group_orgs;
|
|
@@ -2499,7 +2562,7 @@ export declare class PromigenceClient {
|
|
|
2499
2562
|
rows?: number;
|
|
2500
2563
|
cmd?: string;
|
|
2501
2564
|
/** Stored secret names, or `{name, as}` bindings, put into the terminal's environment
|
|
2502
|
-
* by the server. Names travel
|
|
2565
|
+
* by the server. Names travel inside the terminal's connection; values never leave the service. */
|
|
2503
2566
|
secrets?: SecretBinding[];
|
|
2504
2567
|
onData?: (data: Uint8Array) => void;
|
|
2505
2568
|
onExit?: (exit: PtyExit) => void;
|
|
@@ -2544,6 +2607,12 @@ export declare class PromigenceClient {
|
|
|
2544
2607
|
optimizeFor?: OptimizeFor;
|
|
2545
2608
|
spendCap?: number;
|
|
2546
2609
|
}) => Promise<ResumeResult>;
|
|
2610
|
+
/**
|
|
2611
|
+
* `POST /sandboxes/resume {paused_for: "credits"}` — resume every sandbox paused because credits
|
|
2612
|
+
* ran out. Each resume is checked like new work: the answer lists the sandboxes that resumed and
|
|
2613
|
+
* those that still need credits.
|
|
2614
|
+
*/
|
|
2615
|
+
resumePausedForCredits: () => Promise<M.SandboxesResumed>;
|
|
2547
2616
|
/**
|
|
2548
2617
|
* `POST /sandboxes/:id/snapshot` — photograph a running sandbox and keep it running.
|
|
2549
2618
|
*
|
|
@@ -2656,8 +2725,9 @@ export declare class PromigenceClient {
|
|
|
2656
2725
|
}) => Promise<SandboxPatchResponse>;
|
|
2657
2726
|
/**
|
|
2658
2727
|
* `get` streams: the body is handed back as bytes and never parsed, because an eval
|
|
2659
|
-
* artefact, a trace or a run log is exactly what this is for. `put` sends the file
|
|
2660
|
-
*
|
|
2728
|
+
* artefact, a trace or a run log is exactly what this is for. `put` sends the file's bytes
|
|
2729
|
+
* as they are rather than base64 inside JSON. A file's path, a name pattern and a search
|
|
2730
|
+
* text travel in the request body, never in the URL.
|
|
2661
2731
|
*/
|
|
2662
2732
|
files: {
|
|
2663
2733
|
/**
|
|
@@ -2744,7 +2814,7 @@ export declare class PromigenceClient {
|
|
|
2744
2814
|
/**
|
|
2745
2815
|
* Write a file of **any size**, in [`FILE_PUT_CHUNK_BYTES`] requests.
|
|
2746
2816
|
*
|
|
2747
|
-
* Chunk 0 goes without
|
|
2817
|
+
* Chunk 0 goes without an `offset`, which means "this is the file" (create, truncate);
|
|
2748
2818
|
* every later chunk carries one, which appends, so a file has no size limit here.
|
|
2749
2819
|
*
|
|
2750
2820
|
* A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file. Re-running the
|
package/dist/client.js
CHANGED
|
@@ -42,6 +42,26 @@ function bytesToB64(b) {
|
|
|
42
42
|
function modeParam(mode) {
|
|
43
43
|
return typeof mode === "number" ? mode.toString(8) : mode;
|
|
44
44
|
}
|
|
45
|
+
function jsonBody(value) {
|
|
46
|
+
return { data: new TextEncoder().encode(JSON.stringify(value)), contentType: "application/json" };
|
|
47
|
+
}
|
|
48
|
+
function fileForm(fields, data) {
|
|
49
|
+
const boundary = `promigence-${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
50
|
+
let head = "";
|
|
51
|
+
for (const [name, value] of fields) {
|
|
52
|
+
head += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
|
53
|
+
}
|
|
54
|
+
head += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\n`
|
|
55
|
+
+ "Content-Type: application/octet-stream\r\n\r\n";
|
|
56
|
+
const enc = new TextEncoder();
|
|
57
|
+
const pre = enc.encode(head);
|
|
58
|
+
const post = enc.encode(`\r\n--${boundary}--\r\n`);
|
|
59
|
+
const out = new Uint8Array(pre.length + data.length + post.length);
|
|
60
|
+
out.set(pre, 0);
|
|
61
|
+
out.set(data, pre.length);
|
|
62
|
+
out.set(post, pre.length + data.length);
|
|
63
|
+
return { data: out, contentType: `multipart/form-data; boundary=${boundary}` };
|
|
64
|
+
}
|
|
45
65
|
export function execOutputBytes(r, stream = "stdout") {
|
|
46
66
|
const b64 = r[`${stream}_b64`];
|
|
47
67
|
if (typeof b64 === "string")
|
|
@@ -71,6 +91,32 @@ function pageQuery(limit, max, cursor) {
|
|
|
71
91
|
q.set("cursor", cursor);
|
|
72
92
|
return q.toString();
|
|
73
93
|
}
|
|
94
|
+
export const OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
95
|
+
function projectUpdateBody(u) {
|
|
96
|
+
const body = {};
|
|
97
|
+
if (u.name !== undefined)
|
|
98
|
+
body["name"] = u.name;
|
|
99
|
+
if (u.outputRetentionDays !== undefined) {
|
|
100
|
+
const d = u.outputRetentionDays;
|
|
101
|
+
if (d !== null && (!Number.isInteger(d) || d < 1 || d > OUTPUT_RETENTION_MAX_DAYS)) {
|
|
102
|
+
throw new InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, "Pass null for the organization's setting.");
|
|
103
|
+
}
|
|
104
|
+
body["output_retention_days"] = d;
|
|
105
|
+
}
|
|
106
|
+
if (u.failureCaptures !== undefined) {
|
|
107
|
+
if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
|
|
108
|
+
throw new InvalidArgumentError("failureCaptures must be true, false or null", "Pass null for the organization's setting.");
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(body).length) {
|
|
111
|
+
throw new InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
|
|
112
|
+
}
|
|
113
|
+
body["failure_captures"] = u.failureCaptures;
|
|
114
|
+
}
|
|
115
|
+
if (!Object.keys(body).length) {
|
|
116
|
+
throw new InvalidArgumentError("projects.update needs something to change", "Pass name, outputRetentionDays or failureCaptures.");
|
|
117
|
+
}
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
74
120
|
function expiryFields(o) {
|
|
75
121
|
const body = {};
|
|
76
122
|
if (o.expiresInDays !== undefined)
|
|
@@ -944,7 +990,8 @@ export class PromigenceClient {
|
|
|
944
990
|
const idempotencyKey = opts.idempotencyKey ?? (keyedCreate ? newIdempotencyKey() : undefined);
|
|
945
991
|
if (idempotencyKey)
|
|
946
992
|
headers["idempotency-key"] = idempotencyKey;
|
|
947
|
-
const replayable = opts.method === "GET" || opts.method === "DELETE" ||
|
|
993
|
+
const replayable = opts.method === "GET" || opts.method === "DELETE" || opts.replayable === true
|
|
994
|
+
|| (keyedCreate && Boolean(idempotencyKey));
|
|
948
995
|
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
949
996
|
let dispatcher;
|
|
950
997
|
let resentDelete = false;
|
|
@@ -1141,7 +1188,7 @@ export class PromigenceClient {
|
|
|
1141
1188
|
}
|
|
1142
1189
|
}
|
|
1143
1190
|
}
|
|
1144
|
-
async bytes(method, path, body, opts = {}) {
|
|
1191
|
+
async bytes(method, path, body, opts = {}, read = method === "GET") {
|
|
1145
1192
|
const url = `${this.apiUrl}${path}`;
|
|
1146
1193
|
const headers = {
|
|
1147
1194
|
accept: "application/octet-stream, application/json",
|
|
@@ -1151,7 +1198,7 @@ export class PromigenceClient {
|
|
|
1151
1198
|
if (this.caller)
|
|
1152
1199
|
headers[CALLER_HEADER] = this.caller;
|
|
1153
1200
|
if (body !== undefined)
|
|
1154
|
-
headers["content-type"] =
|
|
1201
|
+
headers["content-type"] = body.contentType;
|
|
1155
1202
|
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1156
1203
|
if (opts.signal)
|
|
1157
1204
|
signals.push(opts.signal);
|
|
@@ -1165,7 +1212,7 @@ export class PromigenceClient {
|
|
|
1165
1212
|
method,
|
|
1166
1213
|
headers,
|
|
1167
1214
|
signal,
|
|
1168
|
-
...(body === undefined ? {} : { body: body }),
|
|
1215
|
+
...(body === undefined ? {} : { body: body.data }),
|
|
1169
1216
|
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1170
1217
|
});
|
|
1171
1218
|
}
|
|
@@ -1185,7 +1232,7 @@ export class PromigenceClient {
|
|
|
1185
1232
|
}
|
|
1186
1233
|
catch { }
|
|
1187
1234
|
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
|
|
1188
|
-
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt,
|
|
1235
|
+
const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, read, opts.signal);
|
|
1189
1236
|
if (wait !== undefined) {
|
|
1190
1237
|
this.stats.retries++;
|
|
1191
1238
|
await sleep(wait);
|
|
@@ -1199,7 +1246,7 @@ export class PromigenceClient {
|
|
|
1199
1246
|
});
|
|
1200
1247
|
}
|
|
1201
1248
|
}
|
|
1202
|
-
async stream(path, onChunk, opts = {}) {
|
|
1249
|
+
async stream(method, path, onChunk, opts = {}, body) {
|
|
1203
1250
|
const url = `${this.apiUrl}${path}`;
|
|
1204
1251
|
const headers = {
|
|
1205
1252
|
accept: "application/octet-stream, application/json",
|
|
@@ -1208,17 +1255,25 @@ export class PromigenceClient {
|
|
|
1208
1255
|
};
|
|
1209
1256
|
if (this.caller)
|
|
1210
1257
|
headers[CALLER_HEADER] = this.caller;
|
|
1258
|
+
if (body !== undefined)
|
|
1259
|
+
headers["content-type"] = body.contentType;
|
|
1211
1260
|
const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
|
|
1212
1261
|
if (opts.signal)
|
|
1213
1262
|
signals.push(opts.signal);
|
|
1214
1263
|
const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1215
|
-
const timedOut = () => new TimeoutError(
|
|
1264
|
+
const timedOut = () => new TimeoutError(`${method} ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs.");
|
|
1216
1265
|
const dispatcher = this.guardDispatcher();
|
|
1217
1266
|
for (let attempt = 0;; attempt++) {
|
|
1218
1267
|
let res;
|
|
1219
1268
|
this.stats.requests++;
|
|
1220
1269
|
try {
|
|
1221
|
-
res = await this.fetchImpl(url, {
|
|
1270
|
+
res = await this.fetchImpl(url, {
|
|
1271
|
+
method,
|
|
1272
|
+
headers,
|
|
1273
|
+
signal,
|
|
1274
|
+
...(body === undefined ? {} : { body: body.data }),
|
|
1275
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
1276
|
+
});
|
|
1222
1277
|
}
|
|
1223
1278
|
catch (err) {
|
|
1224
1279
|
const e = err;
|
|
@@ -1267,7 +1322,7 @@ export class PromigenceClient {
|
|
|
1267
1322
|
continue;
|
|
1268
1323
|
}
|
|
1269
1324
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1270
|
-
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from
|
|
1325
|
+
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1271
1326
|
status: res.status,
|
|
1272
1327
|
...bodyExtra(payload),
|
|
1273
1328
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
@@ -1346,6 +1401,29 @@ export class PromigenceClient {
|
|
|
1346
1401
|
const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
|
|
1347
1402
|
return pageOf(r, "entries", limit);
|
|
1348
1403
|
},
|
|
1404
|
+
holds: () => this.request({ method: "GET", path: "/billing/holds" }),
|
|
1405
|
+
buyCredits: (amountUsd) => this.request({ method: "POST", path: "/billing/credits", body: { amount_usd: amountUsd } }),
|
|
1406
|
+
settleCredits: (sessionId) => {
|
|
1407
|
+
if (!sessionId?.trim())
|
|
1408
|
+
throw new InvalidArgumentError("billing.settleCredits needs a session id", "Pass the session_id buyCredits() answered.");
|
|
1409
|
+
return this.request({ method: "POST", path: `/billing/credits/${encodeURIComponent(sessionId)}/settle`, body: {} });
|
|
1410
|
+
},
|
|
1411
|
+
autoReload: () => this.request({ method: "GET", path: "/billing/auto-reload" }),
|
|
1412
|
+
setAutoReload: (opts) => this.request({
|
|
1413
|
+
method: "PUT",
|
|
1414
|
+
path: "/billing/auto-reload",
|
|
1415
|
+
body: {
|
|
1416
|
+
on: opts.on,
|
|
1417
|
+
...(opts.thresholdUsd === undefined ? {} : { threshold_usd: opts.thresholdUsd }),
|
|
1418
|
+
...(opts.amountUsd === undefined ? {} : { amount_usd: opts.amountUsd }),
|
|
1419
|
+
...(opts.authorizationVersion === undefined ? {} : { authorization_version: opts.authorizationVersion }),
|
|
1420
|
+
},
|
|
1421
|
+
}),
|
|
1422
|
+
repayDispute: (disputeId) => {
|
|
1423
|
+
if (!disputeId?.trim())
|
|
1424
|
+
throw new InvalidArgumentError("billing.repayDispute needs a dispute id", "Pass the hold's dispute_id from billing.usage().");
|
|
1425
|
+
return this.request({ method: "POST", path: `/billing/disputes/${encodeURIComponent(disputeId)}/repay`, body: {} });
|
|
1426
|
+
},
|
|
1349
1427
|
checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
|
|
1350
1428
|
portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
|
|
1351
1429
|
referrals: () => this.request({ method: "GET", path: "/referrals" }),
|
|
@@ -1404,6 +1482,9 @@ export class PromigenceClient {
|
|
|
1404
1482
|
rename: (org, project, name) => this.request({
|
|
1405
1483
|
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
|
|
1406
1484
|
}),
|
|
1485
|
+
update: async (org, project, update) => this.request({
|
|
1486
|
+
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
|
|
1487
|
+
}),
|
|
1407
1488
|
},
|
|
1408
1489
|
};
|
|
1409
1490
|
}
|
|
@@ -2152,6 +2233,7 @@ export class PromigenceClient {
|
|
|
2152
2233
|
...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
|
|
2153
2234
|
},
|
|
2154
2235
|
}),
|
|
2236
|
+
resumePausedForCredits: () => this.request({ method: "POST", path: "/sandboxes/resume", body: { paused_for: "credits" } }),
|
|
2155
2237
|
snapshot: (sandboxId, opts = {}) => this.request({
|
|
2156
2238
|
method: "POST",
|
|
2157
2239
|
path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
|
|
@@ -2222,38 +2304,42 @@ export class PromigenceClient {
|
|
|
2222
2304
|
},
|
|
2223
2305
|
files: {
|
|
2224
2306
|
list: (sandboxId, path = "/", opts = {}) => {
|
|
2225
|
-
const
|
|
2307
|
+
const body = { path };
|
|
2226
2308
|
if (opts.recursive)
|
|
2227
|
-
|
|
2309
|
+
body.recursive = true;
|
|
2228
2310
|
if (opts.depth !== undefined)
|
|
2229
|
-
|
|
2311
|
+
body.depth = Math.max(0, Math.floor(opts.depth));
|
|
2230
2312
|
if (opts.limit !== undefined)
|
|
2231
|
-
|
|
2313
|
+
body.limit = Math.max(1, Math.floor(opts.limit));
|
|
2232
2314
|
if (opts.hidden === false)
|
|
2233
|
-
|
|
2315
|
+
body.hidden = false;
|
|
2234
2316
|
return this.request({
|
|
2235
|
-
method: "
|
|
2236
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list
|
|
2317
|
+
method: "POST",
|
|
2318
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list`,
|
|
2319
|
+
body,
|
|
2320
|
+
replayable: true,
|
|
2237
2321
|
});
|
|
2238
2322
|
},
|
|
2239
2323
|
search: (sandboxId, path, opts = {}) => {
|
|
2240
2324
|
if (!opts.pattern && !opts.contains) {
|
|
2241
2325
|
throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
|
|
2242
2326
|
}
|
|
2243
|
-
const
|
|
2327
|
+
const body = { path };
|
|
2244
2328
|
if (opts.pattern)
|
|
2245
|
-
|
|
2329
|
+
body.pattern = opts.pattern;
|
|
2246
2330
|
if (opts.contains)
|
|
2247
|
-
|
|
2331
|
+
body.contains = opts.contains;
|
|
2248
2332
|
if (opts.ignoreCase)
|
|
2249
|
-
|
|
2333
|
+
body.ignore_case = true;
|
|
2250
2334
|
if (opts.limit !== undefined)
|
|
2251
|
-
|
|
2335
|
+
body.limit = Math.max(1, Math.floor(opts.limit));
|
|
2252
2336
|
if (opts.hidden === false)
|
|
2253
|
-
|
|
2337
|
+
body.hidden = false;
|
|
2254
2338
|
return this.request({
|
|
2255
|
-
method: "
|
|
2256
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search
|
|
2339
|
+
method: "POST",
|
|
2340
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search`,
|
|
2341
|
+
body,
|
|
2342
|
+
replayable: true,
|
|
2257
2343
|
});
|
|
2258
2344
|
},
|
|
2259
2345
|
mkdir: (sandboxId, path, opts = {}) => this.request({
|
|
@@ -2310,11 +2396,13 @@ export class PromigenceClient {
|
|
|
2310
2396
|
body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
|
|
2311
2397
|
}),
|
|
2312
2398
|
stat: (sandboxId, path) => this.request({
|
|
2313
|
-
method: "
|
|
2314
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat
|
|
2399
|
+
method: "POST",
|
|
2400
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat`,
|
|
2401
|
+
body: { path },
|
|
2402
|
+
replayable: true,
|
|
2315
2403
|
}),
|
|
2316
|
-
get: (sandboxId, path, opts = {}) => this.bytes("
|
|
2317
|
-
download: (sandboxId, path, onChunk, opts = {}) => this.stream(`/sandboxes/${encodeURIComponent(sandboxId)}/files
|
|
2404
|
+
get: (sandboxId, path, opts = {}) => this.bytes("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, jsonBody({ path }), opts, true),
|
|
2405
|
+
download: (sandboxId, path, onChunk, opts = {}) => this.stream("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, onChunk, opts, jsonBody({ path })),
|
|
2318
2406
|
text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
|
|
2319
2407
|
put: async (sandboxId, path, data, opts = {}) => {
|
|
2320
2408
|
const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
@@ -2324,8 +2412,7 @@ export class PromigenceClient {
|
|
|
2324
2412
|
}, opts);
|
|
2325
2413
|
},
|
|
2326
2414
|
putFrom: async (sandboxId, path, source, opts = {}) => {
|
|
2327
|
-
const
|
|
2328
|
-
const base = `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}${mode}`;
|
|
2415
|
+
const url = `/sandboxes/${encodeURIComponent(sandboxId)}/files`;
|
|
2329
2416
|
let sent = 0;
|
|
2330
2417
|
let chunks = 0;
|
|
2331
2418
|
let last;
|
|
@@ -2334,7 +2421,12 @@ export class PromigenceClient {
|
|
|
2334
2421
|
if (slice.length === 0 && sent < source.size) {
|
|
2335
2422
|
throw new InvalidArgumentError(`the source of ${path} ended at ${sent} of ${source.size} bytes`, "The file shrank while it was being sent; send it again once nothing is writing it.");
|
|
2336
2423
|
}
|
|
2337
|
-
const
|
|
2424
|
+
const fields = [["path", path]];
|
|
2425
|
+
if (opts.mode !== undefined)
|
|
2426
|
+
fields.push(["mode", opts.mode.toString(8)]);
|
|
2427
|
+
if (sent > 0)
|
|
2428
|
+
fields.push(["offset", String(sent)]);
|
|
2429
|
+
const raw = await this.bytes("PUT", url, fileForm(fields, slice), opts);
|
|
2338
2430
|
last = JSON.parse(new TextDecoder().decode(raw));
|
|
2339
2431
|
sent += slice.length;
|
|
2340
2432
|
chunks += 1;
|
|
@@ -2343,8 +2435,8 @@ export class PromigenceClient {
|
|
|
2343
2435
|
},
|
|
2344
2436
|
delete: (sandboxId, path, opts = {}) => this.request({
|
|
2345
2437
|
method: "DELETE",
|
|
2346
|
-
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files
|
|
2347
|
-
|
|
2438
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/files`,
|
|
2439
|
+
body: { path, ...(opts.recursive ? { recursive: true } : {}) },
|
|
2348
2440
|
}),
|
|
2349
2441
|
},
|
|
2350
2442
|
output: async (execId, opts = {}) => {
|