@promigence/sdk 0.1.0-beta.16 → 0.1.0-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/client.d.ts +123 -10
- package/dist/cjs/client.js +374 -82
- package/dist/cjs/context.d.ts +2 -1
- package/dist/cjs/context.js +5 -0
- package/dist/cjs/core.d.ts +25 -2
- package/dist/cjs/core.js +25 -4
- package/dist/cjs/handles.js +2 -1
- package/dist/cjs/http2.js +5 -0
- package/dist/cjs/models.gen.d.ts +205 -23
- package/dist/cjs/proxy.js +32 -3
- package/dist/cjs/sandbox.d.ts +12 -5
- package/dist/cjs/sandbox.js +1 -1
- package/dist/client.d.ts +123 -10
- package/dist/client.js +375 -83
- package/dist/context.d.ts +2 -1
- package/dist/context.js +5 -0
- package/dist/core.d.ts +25 -2
- package/dist/core.js +23 -3
- package/dist/handles.js +2 -1
- package/dist/http2.js +5 -0
- package/dist/models.gen.d.ts +205 -23
- package/dist/proxy.js +33 -4
- package/dist/sandbox.d.ts +12 -5
- package/dist/sandbox.js +1 -1
- package/package.json +1 -1
package/dist/cjs/client.js
CHANGED
|
@@ -46,6 +46,7 @@ Object.defineProperty(exports, "isKeyedCreate", { enumerable: true, get: functio
|
|
|
46
46
|
const dispatcher_js_1 = require("./dispatcher.js");
|
|
47
47
|
const ca_guard_js_1 = require("./ca-guard.js");
|
|
48
48
|
const hint_js_1 = require("./hint.js");
|
|
49
|
+
const command_errors_js_1 = require("./command-errors.js");
|
|
49
50
|
const github_js_1 = require("./github.js");
|
|
50
51
|
const gitlab_js_1 = require("./gitlab.js");
|
|
51
52
|
exports.FILE_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -124,27 +125,46 @@ function pageQuery(limit, max, cursor) {
|
|
|
124
125
|
}
|
|
125
126
|
exports.OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
126
127
|
function projectUpdateBody(u) {
|
|
128
|
+
return settingsBody(u, "projects.update", "the organization's");
|
|
129
|
+
}
|
|
130
|
+
function orgUpdateBody(u) {
|
|
131
|
+
return settingsBody(u, "orgs.update", "the default");
|
|
132
|
+
}
|
|
133
|
+
function settingsBody(u, what, fallback) {
|
|
127
134
|
const body = {};
|
|
128
135
|
if (u.name !== undefined)
|
|
129
136
|
body["name"] = u.name;
|
|
130
137
|
if (u.outputRetentionDays !== undefined) {
|
|
131
138
|
const d = u.outputRetentionDays;
|
|
132
139
|
if (d !== null && (!Number.isInteger(d) || d < 1 || d > exports.OUTPUT_RETENTION_MAX_DAYS)) {
|
|
133
|
-
throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`,
|
|
140
|
+
throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, `Pass null for ${fallback} setting.`);
|
|
134
141
|
}
|
|
135
142
|
body["output_retention_days"] = d;
|
|
136
143
|
}
|
|
137
144
|
if (u.failureCaptures !== undefined) {
|
|
138
145
|
if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
|
|
139
|
-
throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null",
|
|
146
|
+
throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null", `Pass null for ${fallback} setting.`);
|
|
140
147
|
}
|
|
141
148
|
if (Object.keys(body).length) {
|
|
142
149
|
throw new core_js_1.InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
|
|
143
150
|
}
|
|
144
151
|
body["failure_captures"] = u.failureCaptures;
|
|
145
152
|
}
|
|
153
|
+
if (u.previewLinkLength !== undefined) {
|
|
154
|
+
const l = u.previewLinkLength;
|
|
155
|
+
if (l === "custom") {
|
|
156
|
+
throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", `Pass "short", "long" or null (${fallback}) for now.`);
|
|
157
|
+
}
|
|
158
|
+
if (l !== null && l !== "short" && l !== "long") {
|
|
159
|
+
throw new core_js_1.InvalidArgumentError(`previewLinkLength must be "short", "long" or null, not ${JSON.stringify(l)}`, `Pass null for ${fallback} setting.`);
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(body).length) {
|
|
162
|
+
throw new core_js_1.InvalidArgumentError("previewLinkLength is set on its own", "Change it in a call of its own, without the other settings.");
|
|
163
|
+
}
|
|
164
|
+
body["preview_link_length"] = l;
|
|
165
|
+
}
|
|
146
166
|
if (!Object.keys(body).length) {
|
|
147
|
-
throw new core_js_1.InvalidArgumentError(
|
|
167
|
+
throw new core_js_1.InvalidArgumentError(`${what} needs something to change`, "Pass name, outputRetentionDays, failureCaptures or previewLinkLength.");
|
|
148
168
|
}
|
|
149
169
|
return body;
|
|
150
170
|
}
|
|
@@ -156,6 +176,14 @@ function expiryFields(o) {
|
|
|
156
176
|
body["expires_at"] = o.expiresAt;
|
|
157
177
|
return body;
|
|
158
178
|
}
|
|
179
|
+
function checkPreviewLength(length) {
|
|
180
|
+
if (length === undefined || length === "short" || length === "long")
|
|
181
|
+
return;
|
|
182
|
+
if (length === "custom") {
|
|
183
|
+
throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", "Pass length: \"short\" (the default) or \"long\" for now.");
|
|
184
|
+
}
|
|
185
|
+
throw new core_js_1.InvalidArgumentError(`length must be "short" or "long", got ${JSON.stringify(length)}`, "Omit it for the length your project or organization chose.");
|
|
186
|
+
}
|
|
159
187
|
exports.NOTICE_HEADER = "promigence-notice";
|
|
160
188
|
exports.MAX_SOURCES = 8;
|
|
161
189
|
function configDir(env = process.env) {
|
|
@@ -798,6 +826,76 @@ function parseRetryAfter(header) {
|
|
|
798
826
|
const MAX_RETRY_SLEEP_MS = 10_000;
|
|
799
827
|
const STREAM_IDLE_MS = 90_000;
|
|
800
828
|
const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
|
|
829
|
+
const EXEC_KEY_HEADER = "promigence-exec-key";
|
|
830
|
+
const EXEC_HOLD_DEFAULT_S = 120;
|
|
831
|
+
const REATTACH_SLACK_MS = 30_000;
|
|
832
|
+
const REATTACH_BACKOFF_MS = 250;
|
|
833
|
+
const REATTACH_BACKOFF_MAX_MS = 2_000;
|
|
834
|
+
const REATTACH_RETRY_AFTER_MAX_MS = 5_000;
|
|
835
|
+
const REATTACH_TAKEN_MS = 10_000;
|
|
836
|
+
const EXEC_CANCEL_TIMEOUT_MS = 3_000;
|
|
837
|
+
const REATTACH_AGAIN = new Set([429, 502, 503, 504]);
|
|
838
|
+
function newExecKey() {
|
|
839
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
840
|
+
let s = "";
|
|
841
|
+
for (const b of bytes)
|
|
842
|
+
s += b.toString(16).padStart(2, "0");
|
|
843
|
+
return s;
|
|
844
|
+
}
|
|
845
|
+
const UNSENT_CODES = /^(?:ECONNREFUSED|ENOTFOUND|EAI_[A-Z]+|ENETUNREACH|EHOSTUNREACH|UND_ERR_CONNECT_TIMEOUT|ERR_TLS_[A-Z_]+|ERR_SSL_[A-Z_]+|CERT_[A-Z_]+|UNABLE_TO_[A-Z_]+|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|HOSTNAME_MISMATCH|EPROTO)$/;
|
|
846
|
+
function provedUnsent(err) {
|
|
847
|
+
if ((0, ca_guard_js_1.compromisedKeyCause)(err) !== undefined)
|
|
848
|
+
return true;
|
|
849
|
+
const top = err;
|
|
850
|
+
if (top?.name === "TypeError" && /header/i.test(top.message ?? ""))
|
|
851
|
+
return true;
|
|
852
|
+
const unsent = (x) => {
|
|
853
|
+
const code = x?.code;
|
|
854
|
+
return typeof code === "string" && UNSENT_CODES.test(code);
|
|
855
|
+
};
|
|
856
|
+
let c = err;
|
|
857
|
+
for (let depth = 0; c !== null && typeof c === "object" && depth < 6; depth++) {
|
|
858
|
+
if (unsent(c))
|
|
859
|
+
return true;
|
|
860
|
+
const each = c.errors;
|
|
861
|
+
if (Array.isArray(each) && each.length > 0 && each.every(unsent))
|
|
862
|
+
return true;
|
|
863
|
+
c = c.cause;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
class HeldDrop extends Error {
|
|
868
|
+
key;
|
|
869
|
+
constructor(key, why) {
|
|
870
|
+
super(why);
|
|
871
|
+
this.key = key;
|
|
872
|
+
this.name = "HeldDrop";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function sleepUnless(ms, signal) {
|
|
876
|
+
if (signal === undefined)
|
|
877
|
+
return sleep(ms);
|
|
878
|
+
return new Promise((resolve) => {
|
|
879
|
+
if (signal.aborted) {
|
|
880
|
+
resolve();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const done = () => { clearTimeout(t); signal.removeEventListener("abort", done); resolve(); };
|
|
884
|
+
const t = setTimeout(done, ms);
|
|
885
|
+
signal.addEventListener("abort", done, { once: true });
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function reattachWait(attempt, retryAfterMs) {
|
|
889
|
+
if (retryAfterMs !== undefined)
|
|
890
|
+
return Math.min(retryAfterMs, REATTACH_RETRY_AFTER_MAX_MS);
|
|
891
|
+
return Math.min(REATTACH_BACKOFF_MAX_MS, REATTACH_BACKOFF_MS * 2 ** Math.min(attempt, 8)) * (0.5 + Math.random() * 0.5);
|
|
892
|
+
}
|
|
893
|
+
function stoppedUnattended(answer) {
|
|
894
|
+
return answer?.reason === "not_reattached";
|
|
895
|
+
}
|
|
896
|
+
function abortedByCaller(method, path) {
|
|
897
|
+
return new core_js_1.TimeoutError(`request ${method} ${path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
|
|
898
|
+
}
|
|
801
899
|
const POLLING_TICK_MS = 5;
|
|
802
900
|
const PARKED_TICK_MS = 50;
|
|
803
901
|
async function keepingLoopAwake(everyMs, fn) {
|
|
@@ -1018,6 +1116,82 @@ class PromigenceClient {
|
|
|
1018
1116
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1019
1117
|
return true;
|
|
1020
1118
|
}
|
|
1119
|
+
async heldExec(sandboxId, req) {
|
|
1120
|
+
const signal = req.signal;
|
|
1121
|
+
const abortedBefore = signal?.aborted === true;
|
|
1122
|
+
let key;
|
|
1123
|
+
try {
|
|
1124
|
+
let res;
|
|
1125
|
+
try {
|
|
1126
|
+
res = await this.request({ ...req, held: { onKey: (k) => { key = k; } } });
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
if (!(err instanceof HeldDrop))
|
|
1130
|
+
throw err;
|
|
1131
|
+
res = await this.reattachBuffered(sandboxId, err.key, req);
|
|
1132
|
+
}
|
|
1133
|
+
if (stoppedUnattended(res)) {
|
|
1134
|
+
throw new core_js_1.ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it in time`, { sandboxId }, "not_reattached");
|
|
1135
|
+
}
|
|
1136
|
+
return res;
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
if (key !== undefined && signal?.aborted && !abortedBefore)
|
|
1140
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
1141
|
+
throw err;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async reattachBuffered(sandboxId, key, req) {
|
|
1145
|
+
const budgetMs = EXEC_HOLD_DEFAULT_S * 1000 + REATTACH_SLACK_MS;
|
|
1146
|
+
let droppedAt = Date.now();
|
|
1147
|
+
for (let attempt = 0;; attempt++) {
|
|
1148
|
+
if (req.signal?.aborted)
|
|
1149
|
+
throw abortedByCaller(req.method, req.path);
|
|
1150
|
+
this.stats.retries++;
|
|
1151
|
+
const sentAt = Date.now();
|
|
1152
|
+
try {
|
|
1153
|
+
return await this.request({
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/attach`,
|
|
1156
|
+
body: { key },
|
|
1157
|
+
once: true,
|
|
1158
|
+
...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
|
|
1159
|
+
...(req.signal === undefined ? {} : { signal: req.signal }),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
if (req.signal?.aborted)
|
|
1164
|
+
throw abortedByCaller(req.method, req.path);
|
|
1165
|
+
const status = err instanceof core_js_1.PromigenceError ? err.status : undefined;
|
|
1166
|
+
if (status === 404) {
|
|
1167
|
+
throw new core_js_1.ExecLostError(`the connection to the service dropped before the command on sandbox ${sandboxId} answered, and it could not be picked up again: nothing is kept for it (it ended, it never started, or this service cannot keep it)`, { sandboxId }, "gone");
|
|
1168
|
+
}
|
|
1169
|
+
const lostConnection = status === undefined && (err instanceof core_js_1.UnreachableError || err instanceof core_js_1.TimeoutError);
|
|
1170
|
+
if (!lostConnection && !(status !== undefined && REATTACH_AGAIN.has(status)))
|
|
1171
|
+
throw err;
|
|
1172
|
+
if (Date.now() - sentAt >= REATTACH_TAKEN_MS)
|
|
1173
|
+
droppedAt = Date.now();
|
|
1174
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
1175
|
+
if (left <= 0) {
|
|
1176
|
+
throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${EXEC_HOLD_DEFAULT_S} s after the drop`, { sandboxId }, "unreachable");
|
|
1177
|
+
}
|
|
1178
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, err instanceof core_js_1.PromigenceError ? err.retryAfterMs : undefined)), req.signal);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async cancelHeldExec(sandboxId, key) {
|
|
1183
|
+
try {
|
|
1184
|
+
await this.request({
|
|
1185
|
+
method: "POST",
|
|
1186
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/cancel`,
|
|
1187
|
+
body: { key },
|
|
1188
|
+
timeoutMs: EXEC_CANCEL_TIMEOUT_MS,
|
|
1189
|
+
once: true,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1021
1195
|
async request(opts) {
|
|
1022
1196
|
const tr = (0, trace_js_1.traceBegin)(opts.method, opts.path);
|
|
1023
1197
|
const url = `${this.apiUrl}${opts.path}`;
|
|
@@ -1048,9 +1222,16 @@ class PromigenceClient {
|
|
|
1048
1222
|
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
1049
1223
|
let dispatcher;
|
|
1050
1224
|
let resentDelete = false;
|
|
1225
|
+
const maxRetries = opts.once === true ? 0 : this.maxRetries;
|
|
1226
|
+
let execKey;
|
|
1051
1227
|
for (let attempt = 0;; attempt++) {
|
|
1052
1228
|
let res;
|
|
1053
1229
|
this.stats.requests++;
|
|
1230
|
+
if (opts.held !== undefined) {
|
|
1231
|
+
execKey = newExecKey();
|
|
1232
|
+
headers[EXEC_KEY_HEADER] = execKey;
|
|
1233
|
+
opts.held.onKey(execKey);
|
|
1234
|
+
}
|
|
1054
1235
|
const sentAt = performance.now();
|
|
1055
1236
|
try {
|
|
1056
1237
|
if (this.h2) {
|
|
@@ -1093,11 +1274,14 @@ class PromigenceClient {
|
|
|
1093
1274
|
}
|
|
1094
1275
|
catch (err) {
|
|
1095
1276
|
const e = err;
|
|
1277
|
+
if (execKey !== undefined && !opts.signal?.aborted && !provedUnsent(err)) {
|
|
1278
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1279
|
+
}
|
|
1096
1280
|
if (keyedCreate && idempotencyKey && await this.againKeyed(attempt, opts.signal))
|
|
1097
1281
|
continue;
|
|
1098
1282
|
const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
|
|
1099
1283
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
|
|
1100
|
-
if (!timedOut && replayable && attempt <
|
|
1284
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1101
1285
|
this.stats.retries++;
|
|
1102
1286
|
if (opts.method === "DELETE")
|
|
1103
1287
|
resentDelete = true;
|
|
@@ -1105,9 +1289,8 @@ class PromigenceClient {
|
|
|
1105
1289
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1106
1290
|
continue;
|
|
1107
1291
|
}
|
|
1108
|
-
if (opts.signal?.aborted)
|
|
1109
|
-
throw
|
|
1110
|
-
}
|
|
1292
|
+
if (opts.signal?.aborted)
|
|
1293
|
+
throw abortedByCaller(opts.method, opts.path);
|
|
1111
1294
|
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
1112
1295
|
throw new core_js_1.TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
|
|
1113
1296
|
"PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK. A --wait is bounded " +
|
|
@@ -1124,6 +1307,11 @@ class PromigenceClient {
|
|
|
1124
1307
|
const gateway = (res.status === 502 || res.status === 504) && replayable;
|
|
1125
1308
|
let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
|
|
1126
1309
|
let body;
|
|
1310
|
+
if (execKey !== undefined && (res.status === 502 || res.status === 504)) {
|
|
1311
|
+
body = await res.text().catch(() => "");
|
|
1312
|
+
if (errorCodeOf(body) === undefined)
|
|
1313
|
+
throw new HeldDrop(execKey, `HTTP ${res.status}`);
|
|
1314
|
+
}
|
|
1127
1315
|
if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
|
|
1128
1316
|
body = await res.text().catch(() => "");
|
|
1129
1317
|
retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
|
|
@@ -1132,7 +1320,7 @@ class PromigenceClient {
|
|
|
1132
1320
|
body = await res.text().catch(() => "");
|
|
1133
1321
|
retryable = errorCodeOf(body) === undefined;
|
|
1134
1322
|
}
|
|
1135
|
-
if (retryable && attempt <
|
|
1323
|
+
if (retryable && attempt < maxRetries) {
|
|
1136
1324
|
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1137
1325
|
const jitter = Math.random();
|
|
1138
1326
|
const waitMs = retryAfterMs !== undefined
|
|
@@ -1165,8 +1353,10 @@ class PromigenceClient {
|
|
|
1165
1353
|
text = "";
|
|
1166
1354
|
else {
|
|
1167
1355
|
const e = err;
|
|
1356
|
+
if (execKey !== undefined && !opts.signal?.aborted)
|
|
1357
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1168
1358
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError";
|
|
1169
|
-
if (!timedOut && replayable && attempt <
|
|
1359
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1170
1360
|
this.stats.retries++;
|
|
1171
1361
|
if (opts.method === "DELETE")
|
|
1172
1362
|
resentDelete = true;
|
|
@@ -1543,6 +1733,7 @@ class PromigenceClient {
|
|
|
1543
1733
|
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
|
|
1544
1734
|
}),
|
|
1545
1735
|
},
|
|
1736
|
+
update: async (org, update) => this.request({ method: "PATCH", path: `/orgs/${orgSeg(org)}`, body: orgUpdateBody(update) }),
|
|
1546
1737
|
};
|
|
1547
1738
|
}
|
|
1548
1739
|
group_credits() {
|
|
@@ -1622,6 +1813,8 @@ class PromigenceClient {
|
|
|
1622
1813
|
body["network"] = opts.network;
|
|
1623
1814
|
if (opts.optimizeFor !== undefined)
|
|
1624
1815
|
body["optimize_for"] = opts.optimizeFor;
|
|
1816
|
+
if (opts.capture !== undefined)
|
|
1817
|
+
body["capture"] = opts.capture;
|
|
1625
1818
|
if (opts.secrets?.length)
|
|
1626
1819
|
body["secrets"] = opts.secrets;
|
|
1627
1820
|
if (opts.labels && Object.keys(opts.labels).length)
|
|
@@ -1855,7 +2048,9 @@ class PromigenceClient {
|
|
|
1855
2048
|
req.idempotencyKey = opts.idempotencyKey;
|
|
1856
2049
|
if (opts.signal)
|
|
1857
2050
|
req.signal = opts.signal;
|
|
1858
|
-
|
|
2051
|
+
if (opts.terminal)
|
|
2052
|
+
return this.request(req);
|
|
2053
|
+
return this.heldExec(sandboxId, req);
|
|
1859
2054
|
},
|
|
1860
2055
|
execStream: async (sandboxId, opts, on = {}) => {
|
|
1861
2056
|
if (!opts.argv?.length && !opts.sh) {
|
|
@@ -1884,7 +2079,9 @@ class PromigenceClient {
|
|
|
1884
2079
|
body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
|
|
1885
2080
|
body["max_output_bytes"] = opts.maxOutputBytes ?? core_js_1.MAX_OUTPUT_BYTES_DEFAULT;
|
|
1886
2081
|
const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
|
|
1887
|
-
const
|
|
2082
|
+
const keyed = opts.terminal !== true;
|
|
2083
|
+
let key;
|
|
2084
|
+
let idle = new AbortController();
|
|
1888
2085
|
let silent = false;
|
|
1889
2086
|
let reader;
|
|
1890
2087
|
let timer;
|
|
@@ -1896,23 +2093,33 @@ class PromigenceClient {
|
|
|
1896
2093
|
reader?.cancel().catch(() => { });
|
|
1897
2094
|
}, STREAM_IDLE_MS);
|
|
1898
2095
|
};
|
|
2096
|
+
const fresh = () => { idle = new AbortController(); silent = false; reader = undefined; };
|
|
2097
|
+
const abortedBefore = opts.signal?.aborted === true;
|
|
2098
|
+
const abandon = async (err) => {
|
|
2099
|
+
if (key !== undefined && !abortedBefore)
|
|
2100
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
2101
|
+
throw err;
|
|
2102
|
+
};
|
|
1899
2103
|
let stream;
|
|
1900
2104
|
const bearer = `Bearer ${this.requireKey()}`;
|
|
1901
2105
|
const dispatcher = this.guardDispatcher();
|
|
2106
|
+
const headers = () => ({
|
|
2107
|
+
accept: "text/event-stream",
|
|
2108
|
+
"content-type": "application/json",
|
|
2109
|
+
"user-agent": this.userAgent,
|
|
2110
|
+
authorization: bearer,
|
|
2111
|
+
...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
|
|
2112
|
+
});
|
|
1902
2113
|
for (let attempt = 0; stream === undefined; attempt++) {
|
|
1903
2114
|
let r;
|
|
1904
2115
|
this.stats.requests++;
|
|
2116
|
+
if (keyed)
|
|
2117
|
+
key = newExecKey();
|
|
1905
2118
|
arm();
|
|
1906
2119
|
try {
|
|
1907
2120
|
r = await this.fetchImpl(url, {
|
|
1908
2121
|
method: "POST",
|
|
1909
|
-
headers: {
|
|
1910
|
-
accept: "text/event-stream",
|
|
1911
|
-
"content-type": "application/json",
|
|
1912
|
-
"user-agent": this.userAgent,
|
|
1913
|
-
authorization: bearer,
|
|
1914
|
-
...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
|
|
1915
|
-
},
|
|
2122
|
+
headers: { ...headers(), ...(key === undefined ? {} : { [EXEC_KEY_HEADER]: key }) },
|
|
1916
2123
|
body: JSON.stringify(body),
|
|
1917
2124
|
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
1918
2125
|
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
@@ -1922,7 +2129,10 @@ class PromigenceClient {
|
|
|
1922
2129
|
clearTimeout(timer);
|
|
1923
2130
|
const e = err;
|
|
1924
2131
|
const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
|
|
1925
|
-
|
|
2132
|
+
const failed = new core_js_1.UnreachableError(`cannot reach ${url}: ${why}`);
|
|
2133
|
+
if (opts.signal?.aborted)
|
|
2134
|
+
return abandon(failed);
|
|
2135
|
+
throw failed;
|
|
1926
2136
|
}
|
|
1927
2137
|
if (r.ok && r.body) {
|
|
1928
2138
|
stream = r.body;
|
|
@@ -1953,87 +2163,166 @@ class PromigenceClient {
|
|
|
1953
2163
|
const errOut = [];
|
|
1954
2164
|
let exit;
|
|
1955
2165
|
let failure;
|
|
1956
|
-
|
|
1957
|
-
|
|
2166
|
+
let holdS;
|
|
2167
|
+
const received = { stdout: 0, stderr: 0 };
|
|
1958
2168
|
const lost = (how) => new core_js_1.UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
|
|
1959
2169
|
`Read it by id (\`promigence exec output <exec_id> --sandbox ${(0, hint_js_1.hintArg)(sandboxId)}\`) or re-run.`);
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2170
|
+
const consume = async (from) => {
|
|
2171
|
+
const dec = new TextDecoder();
|
|
2172
|
+
let buf = "";
|
|
2173
|
+
const rd = from.getReader();
|
|
2174
|
+
reader = rd;
|
|
2175
|
+
try {
|
|
2176
|
+
for (;;) {
|
|
2177
|
+
arm();
|
|
2178
|
+
let chunk;
|
|
2179
|
+
try {
|
|
2180
|
+
chunk = await rd.read();
|
|
2181
|
+
}
|
|
2182
|
+
catch (err) {
|
|
2183
|
+
if (opts.signal?.aborted)
|
|
2184
|
+
return abandon(err);
|
|
2185
|
+
if (holdS !== undefined && (exit || failure))
|
|
2186
|
+
return { done: true };
|
|
2187
|
+
if (silent)
|
|
2188
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2189
|
+
const e = err;
|
|
2190
|
+
return { done: false, why: `was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`, restart: false };
|
|
2191
|
+
}
|
|
2192
|
+
if (chunk.done)
|
|
2193
|
+
break;
|
|
2194
|
+
const value = chunk.value;
|
|
2195
|
+
buf += dec.decode(value, { stream: true });
|
|
2196
|
+
let sep;
|
|
2197
|
+
while ((sep = buf.indexOf("\n\n")) >= 0) {
|
|
2198
|
+
const block = buf.slice(0, sep);
|
|
2199
|
+
buf = buf.slice(sep + 2);
|
|
2200
|
+
let event = "";
|
|
2201
|
+
let data = "";
|
|
2202
|
+
for (const line of block.split("\n")) {
|
|
2203
|
+
if (line.startsWith("event:"))
|
|
2204
|
+
event = line.slice(6).trim();
|
|
2205
|
+
else if (line.startsWith("data:"))
|
|
2206
|
+
data = line.slice(5).replace(/^ /, "");
|
|
2207
|
+
}
|
|
2208
|
+
if (!event)
|
|
2209
|
+
continue;
|
|
2210
|
+
if (event === "heartbeat") {
|
|
2211
|
+
on.heartbeat?.();
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (event === "reconnect") {
|
|
2215
|
+
rd.cancel().catch(() => { });
|
|
2216
|
+
return { done: false, why: "was ended by a service restart", restart: true };
|
|
2217
|
+
}
|
|
2218
|
+
let parsed = {};
|
|
2219
|
+
try {
|
|
2220
|
+
parsed = JSON.parse(data);
|
|
2221
|
+
}
|
|
2222
|
+
catch { }
|
|
2223
|
+
if (event === "held") {
|
|
2224
|
+
const h = parsed["hold_s"];
|
|
2225
|
+
holdS = typeof h === "number" && Number.isFinite(h) && h >= 0 ? h : EXEC_HOLD_DEFAULT_S;
|
|
2226
|
+
}
|
|
2227
|
+
else if (event === "stdout" || event === "stderr") {
|
|
2228
|
+
const bytes = b64ToBytes(String(parsed["data"] ?? ""));
|
|
2229
|
+
if (event === "stdout") {
|
|
2230
|
+
received.stdout += bytes.length;
|
|
2231
|
+
out.push(bytes);
|
|
2232
|
+
on.stdout?.(bytes);
|
|
2233
|
+
}
|
|
2234
|
+
else {
|
|
2235
|
+
received.stderr += bytes.length;
|
|
2236
|
+
errOut.push(bytes);
|
|
2237
|
+
on.stderr?.(bytes);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
else if (event === "exit") {
|
|
2241
|
+
exit = { ...(exit ?? {}), ...parsed };
|
|
2242
|
+
}
|
|
2243
|
+
else if (event === "error") {
|
|
2244
|
+
failure = parsed;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
finally {
|
|
2250
|
+
clearTimeout(timer);
|
|
2251
|
+
}
|
|
2252
|
+
if (exit || failure)
|
|
2253
|
+
return { done: true };
|
|
2254
|
+
if (silent)
|
|
2255
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2256
|
+
return { done: false, why: "ended without an exit event", restart: false };
|
|
2257
|
+
};
|
|
2258
|
+
const reattach = async () => {
|
|
2259
|
+
const droppedAt = Date.now();
|
|
2260
|
+
const hold = holdS ?? EXEC_HOLD_DEFAULT_S;
|
|
2261
|
+
const budgetMs = hold * 1000 + REATTACH_SLACK_MS;
|
|
2262
|
+
for (let attempt = 0;; attempt++) {
|
|
2263
|
+
if (opts.signal?.aborted)
|
|
2264
|
+
return abandon(opts.signal.reason);
|
|
2265
|
+
fresh();
|
|
2266
|
+
this.stats.requests++;
|
|
2267
|
+
this.stats.retries++;
|
|
1963
2268
|
arm();
|
|
1964
|
-
let
|
|
2269
|
+
let retryAfterMs;
|
|
2270
|
+
let r;
|
|
1965
2271
|
try {
|
|
1966
|
-
|
|
2272
|
+
r = await this.fetchImpl(`${url}/attach`, {
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
headers: headers(),
|
|
2275
|
+
body: JSON.stringify({ key, stdout_offset: received.stdout, stderr_offset: received.stderr }),
|
|
2276
|
+
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
2277
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
2278
|
+
});
|
|
1967
2279
|
}
|
|
1968
2280
|
catch (err) {
|
|
2281
|
+
clearTimeout(timer);
|
|
1969
2282
|
if (opts.signal?.aborted)
|
|
1970
|
-
|
|
1971
|
-
if (silent)
|
|
1972
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1973
|
-
const e = err;
|
|
1974
|
-
throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
|
|
2283
|
+
return abandon(err);
|
|
1975
2284
|
}
|
|
1976
|
-
if (
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const block = buf.slice(0, sep);
|
|
1983
|
-
buf = buf.slice(sep + 2);
|
|
1984
|
-
let event = "";
|
|
1985
|
-
let data = "";
|
|
1986
|
-
for (const line of block.split("\n")) {
|
|
1987
|
-
if (line.startsWith("event:"))
|
|
1988
|
-
event = line.slice(6).trim();
|
|
1989
|
-
else if (line.startsWith("data:"))
|
|
1990
|
-
data = line.slice(5).replace(/^ /, "");
|
|
1991
|
-
}
|
|
1992
|
-
if (!event)
|
|
1993
|
-
continue;
|
|
1994
|
-
if (event === "heartbeat") {
|
|
1995
|
-
on.heartbeat?.();
|
|
1996
|
-
continue;
|
|
1997
|
-
}
|
|
1998
|
-
if (event === "reconnect") {
|
|
1999
|
-
reader.cancel().catch(() => { });
|
|
2000
|
-
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.");
|
|
2001
|
-
}
|
|
2002
|
-
let parsed = {};
|
|
2285
|
+
if (r !== undefined) {
|
|
2286
|
+
if (r.ok && r.body)
|
|
2287
|
+
return r.body;
|
|
2288
|
+
clearTimeout(timer);
|
|
2289
|
+
const text = await r.text().catch(() => "");
|
|
2290
|
+
let payload = {};
|
|
2003
2291
|
try {
|
|
2004
|
-
|
|
2292
|
+
payload = text ? JSON.parse(text) : {};
|
|
2005
2293
|
}
|
|
2006
2294
|
catch { }
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
out.push(bytes);
|
|
2011
|
-
on.stdout?.(bytes);
|
|
2012
|
-
}
|
|
2013
|
-
else {
|
|
2014
|
-
errOut.push(bytes);
|
|
2015
|
-
on.stderr?.(bytes);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
else if (event === "exit") {
|
|
2019
|
-
exit = { ...(exit ?? {}), ...parsed };
|
|
2295
|
+
retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
|
|
2296
|
+
if (r.status === 404) {
|
|
2297
|
+
throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and it could not be picked up again: it is no longer kept (it ended, or this service cannot keep it)`, { sandboxId }, "gone");
|
|
2020
2298
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2299
|
+
if (!REATTACH_AGAIN.has(r.status)) {
|
|
2300
|
+
throw errorFor(payload.error?.code ?? codeForStatus(r.status), payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs/attach`, payload.error?.fix ?? "See the API response for details.", { status: r.status, ...bodyExtra(payload), ...(retryAfterMs === undefined ? {} : { retryAfterMs }) });
|
|
2023
2301
|
}
|
|
2024
2302
|
}
|
|
2303
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
2304
|
+
if (left <= 0) {
|
|
2305
|
+
throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${hold} s after the drop`, { sandboxId }, "unreachable");
|
|
2306
|
+
}
|
|
2307
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, retryAfterMs)), opts.signal);
|
|
2025
2308
|
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2309
|
+
};
|
|
2310
|
+
for (let ended = await consume(stream); !ended.done; ended = await consume(stream)) {
|
|
2311
|
+
if (holdS === undefined || key === undefined) {
|
|
2312
|
+
if (ended.restart) {
|
|
2313
|
+
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.");
|
|
2314
|
+
}
|
|
2315
|
+
throw lost(ended.why);
|
|
2316
|
+
}
|
|
2317
|
+
stream = await reattach();
|
|
2029
2318
|
}
|
|
2030
2319
|
if (failure) {
|
|
2031
2320
|
throw errorFor(failure.code ?? "internal", failure.message ?? "the exec stream failed", failure.fix ?? "Re-run; if it repeats, include the exec_id.", { status: 200, ...bodyExtra({ error: failure }) });
|
|
2032
2321
|
}
|
|
2033
|
-
if (!exit
|
|
2034
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
2035
|
-
if (!exit) {
|
|
2322
|
+
if (!exit)
|
|
2036
2323
|
throw lost("ended without an exit event");
|
|
2324
|
+
if (stoppedUnattended(exit)) {
|
|
2325
|
+
throw new core_js_1.ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it ${holdS === undefined ? "in time" : `within ${holdS} s`}`, { sandboxId }, "not_reattached");
|
|
2037
2326
|
}
|
|
2038
2327
|
const rawCode = exit["exit_code"];
|
|
2039
2328
|
const signal = exit["signal"];
|
|
@@ -2154,12 +2443,14 @@ class PromigenceClient {
|
|
|
2154
2443
|
}),
|
|
2155
2444
|
},
|
|
2156
2445
|
ports: {
|
|
2157
|
-
expose: (sandboxId, port) => {
|
|
2446
|
+
expose: (sandboxId, port, opts = {}) => {
|
|
2158
2447
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2159
2448
|
throw new core_js_1.InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
|
|
2160
2449
|
}
|
|
2450
|
+
checkPreviewLength(opts.length);
|
|
2161
2451
|
return (0, core_js_1.withPreviewsFix)(this.request({
|
|
2162
|
-
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2452
|
+
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2453
|
+
body: { port, ...(opts.length === undefined ? {} : { length: opts.length }) },
|
|
2163
2454
|
}), sandboxId, port);
|
|
2164
2455
|
},
|
|
2165
2456
|
list: async (sandboxId) => {
|
|
@@ -2786,5 +3077,6 @@ function toCommandResult(r) {
|
|
|
2786
3077
|
stderr: r.stderr ?? "",
|
|
2787
3078
|
...(r.truncated === undefined ? {} : { truncated: r.truncated }),
|
|
2788
3079
|
...(r.exec_id === undefined ? {} : { execId: r.exec_id }),
|
|
3080
|
+
timedOut: (0, command_errors_js_1.answerTimedOut)(r),
|
|
2789
3081
|
};
|
|
2790
3082
|
}
|
package/dist/cjs/context.d.ts
CHANGED
|
@@ -96,7 +96,8 @@ export interface ContextOptions {
|
|
|
96
96
|
allowSecrets?: boolean;
|
|
97
97
|
/**
|
|
98
98
|
* Send these files that look like credentials, and only these: paths inside the context,
|
|
99
|
-
* `/`-separated, as `listContext` names them. Every other one is still refused
|
|
99
|
+
* `/`-separated, as `listContext` names them. Every other one is still refused, and a name that
|
|
100
|
+
* is no file of the context is refused too, so a mistyped or stale name is seen.
|
|
100
101
|
*/
|
|
101
102
|
allowSecretFiles?: readonly string[];
|
|
102
103
|
/** Called with each note: what was left out by default, or changed on the way. */
|