@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.
@@ -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)}`, "Pass null for the organization's setting.");
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", "Pass null for the organization's setting.");
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("projects.update needs something to change", "Pass name, outputRetentionDays or failureCaptures.");
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 < this.maxRetries && !opts.signal?.aborted) {
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 new core_js_1.TimeoutError(`request ${opts.method} ${opts.path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
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 < this.maxRetries) {
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 < this.maxRetries && !opts.signal?.aborted) {
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
- return this.request(req);
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 idle = new AbortController();
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
- throw new core_js_1.UnreachableError(`cannot reach ${url}: ${why}`);
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
- const dec = new TextDecoder();
1957
- let buf = "";
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
- reader = stream.getReader();
1961
- try {
1962
- for (;;) {
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 chunk;
2269
+ let retryAfterMs;
2270
+ let r;
1965
2271
  try {
1966
- chunk = await reader.read();
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
- throw err;
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 (chunk.done)
1977
- break;
1978
- const value = chunk.value;
1979
- buf += dec.decode(value, { stream: true });
1980
- let sep;
1981
- while ((sep = buf.indexOf("\n\n")) >= 0) {
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
- parsed = JSON.parse(data);
2292
+ payload = text ? JSON.parse(text) : {};
2005
2293
  }
2006
2294
  catch { }
2007
- if (event === "stdout" || event === "stderr") {
2008
- const bytes = b64ToBytes(String(parsed["data"] ?? ""));
2009
- if (event === "stdout") {
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
- else if (event === "error") {
2022
- failure = parsed;
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
- finally {
2028
- clearTimeout(timer);
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 && silent)
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`, body: { port },
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
  }
@@ -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. */