xapi-to 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -1
- package/dist/chunk-TYY6JR6O.js +870 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +998 -670
- package/dist/openai-sandbox-client.d.ts +85 -0
- package/dist/openai-sandbox-client.js +285 -0
- package/examples/openai-agents-sandbox-local.ts +131 -0
- package/examples/sandbox-api-cli-openai.mjs +450 -0
- package/package.json +25 -3
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +18 -5
- package/skills/xapi/guides/linkedin.md +55 -0
- package/skills/xapi/guides/sandbox.md +466 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/src/client.ts +664 -0
- package/src/config.ts +160 -0
- package/src/openai-sandbox-client.ts +349 -0
- package/src/sandbox-client.ts +289 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
9
|
+
|
|
10
|
+
// src/format.ts
|
|
11
|
+
function getFormat() {
|
|
12
|
+
const f = process.env.XAPI_OUTPUT || "json";
|
|
13
|
+
if (f === "pretty" || f === "table") return f;
|
|
14
|
+
return "json";
|
|
15
|
+
}
|
|
16
|
+
function output(data, format) {
|
|
17
|
+
const fmt = format || getFormat();
|
|
18
|
+
if (fmt === "json") {
|
|
19
|
+
console.log(JSON.stringify(data));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (fmt === "pretty") {
|
|
23
|
+
console.log(JSON.stringify(data, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (fmt === "table") {
|
|
27
|
+
const rows = tableRows(data);
|
|
28
|
+
if (rows) {
|
|
29
|
+
printTable(rows);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
console.log(JSON.stringify(data, null, 2));
|
|
34
|
+
}
|
|
35
|
+
function tableRows(data) {
|
|
36
|
+
if (Array.isArray(data)) return normalizeRows(data, "value");
|
|
37
|
+
if (!data || typeof data !== "object") return null;
|
|
38
|
+
const obj = data;
|
|
39
|
+
const preferredKeys = ["items", "actions", "results", "services", "categories", "bindings", "providers"];
|
|
40
|
+
for (const key of preferredKeys) {
|
|
41
|
+
const value = obj[key];
|
|
42
|
+
if (Array.isArray(value)) return normalizeRows(value, singularKey(key));
|
|
43
|
+
}
|
|
44
|
+
const firstArray = Object.entries(obj).find(([, value]) => Array.isArray(value));
|
|
45
|
+
return firstArray ? normalizeRows(firstArray[1], singularKey(firstArray[0])) : null;
|
|
46
|
+
}
|
|
47
|
+
function normalizeRows(rows, primitiveKey) {
|
|
48
|
+
return rows.map((row) => {
|
|
49
|
+
if (row && typeof row === "object" && !Array.isArray(row)) {
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
return { [primitiveKey]: row };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function singularKey(key) {
|
|
56
|
+
if (key === "categories") return "category";
|
|
57
|
+
if (key.endsWith("ies")) return `${key.slice(0, -3)}y`;
|
|
58
|
+
if (key.endsWith("s")) return key.slice(0, -1);
|
|
59
|
+
return "value";
|
|
60
|
+
}
|
|
61
|
+
function formatCell(value) {
|
|
62
|
+
if (value === null || value === void 0) return "";
|
|
63
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
function fitCell(value, width) {
|
|
67
|
+
const rendered = formatCell(value);
|
|
68
|
+
if (rendered.length <= width) return rendered.padEnd(width);
|
|
69
|
+
if (width <= 1) return "\u2026".slice(0, width);
|
|
70
|
+
return `${rendered.slice(0, width - 1)}\u2026`;
|
|
71
|
+
}
|
|
72
|
+
function printTable(rows) {
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
console.log("(empty)");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const keys = Object.keys(rows[0]);
|
|
78
|
+
const widths = keys.map(
|
|
79
|
+
(k) => Math.min(40, Math.max(k.length, ...rows.map((r) => formatCell(r[k]).length)))
|
|
80
|
+
);
|
|
81
|
+
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
82
|
+
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
|
|
83
|
+
console.log(header);
|
|
84
|
+
console.log(sep);
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const line = keys.map((k, i) => fitCell(row[k], widths[i])).join(" ");
|
|
87
|
+
console.log(line);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function err(msg, detail) {
|
|
91
|
+
if (process.stderr.isTTY) {
|
|
92
|
+
console.error(`Error: ${msg}`);
|
|
93
|
+
if (detail !== void 0) {
|
|
94
|
+
const rendered = detail && typeof detail === "object" ? JSON.stringify(detail, null, 2) : String(detail);
|
|
95
|
+
console.error(rendered.split("\n").map((line) => ` ${line}`).join("\n"));
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const out = { error: msg };
|
|
99
|
+
if (detail !== void 0) out.detail = detail;
|
|
100
|
+
console.error(JSON.stringify(out));
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/config.ts
|
|
106
|
+
import { homedir } from "os";
|
|
107
|
+
import { join } from "path";
|
|
108
|
+
var XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || "action.xapi.to";
|
|
109
|
+
var XAPI_API_HOST = process.env.XAPI_API_HOST || "api.xapi.to";
|
|
110
|
+
var XAPI_SANDBOX_HOST = process.env.XAPI_SANDBOX_HOST || "sandbox.xapi.to";
|
|
111
|
+
function scheme(host) {
|
|
112
|
+
return isLoopbackHost(host) ? "http" : "https";
|
|
113
|
+
}
|
|
114
|
+
var ALLOWED_HOST_EXACT = ["xapi.to", "xapi.xyz"];
|
|
115
|
+
var ALLOWED_HOST_SUFFIXES = [".xapi.to", ".xapi.xyz"];
|
|
116
|
+
function hostnameOf(hostOrUrl) {
|
|
117
|
+
const raw = hostOrUrl.includes("://") ? hostOrUrl : `http://${hostOrUrl}`;
|
|
118
|
+
try {
|
|
119
|
+
return new URL(raw).hostname.toLowerCase();
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function isLoopbackIPv4(h) {
|
|
125
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
126
|
+
if (!m) return false;
|
|
127
|
+
const octets = m.slice(1).map(Number);
|
|
128
|
+
return octets.every((o) => o <= 255) && octets[0] === 127;
|
|
129
|
+
}
|
|
130
|
+
function isLoopbackHostname(h) {
|
|
131
|
+
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "[::1]" || isLoopbackIPv4(h);
|
|
132
|
+
}
|
|
133
|
+
function isLoopbackHost(hostOrUrl) {
|
|
134
|
+
return isLoopbackHostname(hostnameOf(hostOrUrl));
|
|
135
|
+
}
|
|
136
|
+
function isAllowedHost(hostOrUrl) {
|
|
137
|
+
const h = hostnameOf(hostOrUrl);
|
|
138
|
+
if (!h) return false;
|
|
139
|
+
if (isLoopbackHostname(h)) return true;
|
|
140
|
+
if (ALLOWED_HOST_EXACT.includes(h)) return true;
|
|
141
|
+
return ALLOWED_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix));
|
|
142
|
+
}
|
|
143
|
+
function assertAllowedHost(hostOrUrl) {
|
|
144
|
+
if (!isAllowedHost(hostOrUrl)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`refusing to contact untrusted host "${hostnameOf(hostOrUrl) || hostOrUrl}": the xapi API key may only be sent to *.xapi.to, *.xapi.xyz, or localhost`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var CONFIG_DIR = join(homedir(), ".xapi");
|
|
151
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
152
|
+
function loadFileConfig() {
|
|
153
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
156
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
157
|
+
return typeof parsed.apiKey === "string" && parsed.apiKey.trim() ? { apiKey: parsed.apiKey } : {};
|
|
158
|
+
} catch {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function getApiKeySource() {
|
|
163
|
+
if (process.env.XAPI_KEY) return "XAPI_KEY";
|
|
164
|
+
if (process.env.XAPI_API_KEY) return "XAPI_API_KEY";
|
|
165
|
+
return loadFileConfig().apiKey ? "file" : "none";
|
|
166
|
+
}
|
|
167
|
+
function getConfig() {
|
|
168
|
+
const file = loadFileConfig();
|
|
169
|
+
return {
|
|
170
|
+
actionHost: XAPI_ACTION_HOST,
|
|
171
|
+
sandboxHost: XAPI_SANDBOX_HOST,
|
|
172
|
+
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function requireApiKey(cfg) {
|
|
176
|
+
if (!cfg.apiKey) {
|
|
177
|
+
err("API key not configured", 'Run "npx xapi-to register" to create an account, or "npx xapi-to config set apiKey=<key>" to set an existing key.');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function saveConfig(updates) {
|
|
181
|
+
const current = loadFileConfig();
|
|
182
|
+
const merged = { ...current, ...updates };
|
|
183
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
184
|
+
if (process.platform !== "win32") chmodSync(CONFIG_DIR, 448);
|
|
185
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
186
|
+
if (process.platform !== "win32") chmodSync(CONFIG_FILE, 384);
|
|
187
|
+
}
|
|
188
|
+
function showConfig() {
|
|
189
|
+
const cfg = getConfig();
|
|
190
|
+
return {
|
|
191
|
+
actionHost: cfg.actionHost,
|
|
192
|
+
sandboxHost: cfg.sandboxHost,
|
|
193
|
+
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : void 0,
|
|
194
|
+
source: {
|
|
195
|
+
apiKey: getApiKeySource()
|
|
196
|
+
},
|
|
197
|
+
configFile: CONFIG_FILE
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/client.ts
|
|
202
|
+
import { open, rm } from "fs/promises";
|
|
203
|
+
import { once } from "events";
|
|
204
|
+
import { resolve } from "path";
|
|
205
|
+
import { Readable, Transform } from "stream";
|
|
206
|
+
import { pipeline } from "stream/promises";
|
|
207
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
208
|
+
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
209
|
+
var TRANSFER_IDLE_TIMEOUT_MS = 6e4;
|
|
210
|
+
var IDEMPOTENT_RETRIES = 2;
|
|
211
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
212
|
+
var RETRY_MAX_DELAY_MS = 8e3;
|
|
213
|
+
var HttpError = class extends Error {
|
|
214
|
+
constructor(status, detail, retryAfterMs) {
|
|
215
|
+
super(`HTTP ${status}: ${detail}`);
|
|
216
|
+
this.status = status;
|
|
217
|
+
this.retryAfterMs = retryAfterMs;
|
|
218
|
+
this.name = "HttpError";
|
|
219
|
+
}
|
|
220
|
+
status;
|
|
221
|
+
retryAfterMs;
|
|
222
|
+
};
|
|
223
|
+
var RequestTimeoutError = class extends Error {
|
|
224
|
+
constructor(timeoutMs) {
|
|
225
|
+
super(`request timed out after ${timeoutMs}ms`);
|
|
226
|
+
this.timeoutMs = timeoutMs;
|
|
227
|
+
this.name = "RequestTimeoutError";
|
|
228
|
+
}
|
|
229
|
+
timeoutMs;
|
|
230
|
+
};
|
|
231
|
+
function isRetryableStatus(status) {
|
|
232
|
+
return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
233
|
+
}
|
|
234
|
+
function isRetryableNetworkError(e) {
|
|
235
|
+
if (!(e instanceof Error)) return false;
|
|
236
|
+
if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
|
|
237
|
+
if (e.name === "AbortError") return false;
|
|
238
|
+
return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
|
|
239
|
+
}
|
|
240
|
+
function isRetryableRequestError(e) {
|
|
241
|
+
if (e instanceof HttpError) return isRetryableStatus(e.status);
|
|
242
|
+
if (e instanceof RequestTimeoutError) return true;
|
|
243
|
+
return isRetryableNetworkError(e);
|
|
244
|
+
}
|
|
245
|
+
function retryBaseDelayMs() {
|
|
246
|
+
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
247
|
+
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
248
|
+
}
|
|
249
|
+
function transferIdleTimeoutMs() {
|
|
250
|
+
const override = Number(process.env.XAPI_TRANSFER_IDLE_TIMEOUT_MS);
|
|
251
|
+
return Number.isFinite(override) && override > 0 ? override : TRANSFER_IDLE_TIMEOUT_MS;
|
|
252
|
+
}
|
|
253
|
+
function backoffDelayMs(attempt, retryAfterMs) {
|
|
254
|
+
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
255
|
+
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
256
|
+
}
|
|
257
|
+
const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
258
|
+
return capped / 2 + Math.random() * (capped / 2);
|
|
259
|
+
}
|
|
260
|
+
function parseRetryAfterMs(res) {
|
|
261
|
+
const header = res.headers.get("retry-after");
|
|
262
|
+
if (!header) return void 0;
|
|
263
|
+
const seconds = Number(header);
|
|
264
|
+
if (Number.isFinite(seconds)) return seconds * 1e3;
|
|
265
|
+
const at = Date.parse(header);
|
|
266
|
+
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
267
|
+
}
|
|
268
|
+
function sleep(ms) {
|
|
269
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
270
|
+
}
|
|
271
|
+
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
272
|
+
assertAllowedHost(url);
|
|
273
|
+
let attempt = 0;
|
|
274
|
+
while (true) {
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
const callerSignal = options.signal;
|
|
277
|
+
const abortFromCaller = () => controller.abort();
|
|
278
|
+
if (callerSignal?.aborted) controller.abort();
|
|
279
|
+
else callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
280
|
+
let timedOut = false;
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
timedOut = true;
|
|
283
|
+
controller.abort();
|
|
284
|
+
}, timeoutMs);
|
|
285
|
+
try {
|
|
286
|
+
const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
|
|
287
|
+
if (res.status >= 300 && res.status < 400) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
|
|
294
|
+
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
295
|
+
await res.text().catch(() => "");
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
298
|
+
attempt++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const text2 = await res.text();
|
|
302
|
+
throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
|
|
303
|
+
}
|
|
304
|
+
if (res.status === 204) {
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
const text = await res.text();
|
|
308
|
+
if (!text.trim()) {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
const body = JSON.parse(text);
|
|
312
|
+
if (body && typeof body === "object" && "success" in body && body.success === false) {
|
|
313
|
+
const data = body.data;
|
|
314
|
+
if (data?.statusCode === 401 || data?.error === "Unauthorized") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
(data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return body;
|
|
326
|
+
} catch (e) {
|
|
327
|
+
if (timedOut) {
|
|
328
|
+
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
329
|
+
if (attempt < retries) {
|
|
330
|
+
await sleep(backoffDelayMs(attempt));
|
|
331
|
+
attempt++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
throw timeoutError;
|
|
335
|
+
}
|
|
336
|
+
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
await sleep(backoffDelayMs(attempt));
|
|
339
|
+
attempt++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
throw e;
|
|
343
|
+
} finally {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function headers(apiKey) {
|
|
350
|
+
const h = { "Content-Type": "application/json" };
|
|
351
|
+
if (apiKey) h["XAPI-Key"] = apiKey;
|
|
352
|
+
return h;
|
|
353
|
+
}
|
|
354
|
+
function baseUrl(opts) {
|
|
355
|
+
return `${scheme(opts.actionHost)}://${opts.actionHost}`;
|
|
356
|
+
}
|
|
357
|
+
async function actionList(opts, params = {}) {
|
|
358
|
+
const url = new URL(`${baseUrl(opts)}/v1/actions`);
|
|
359
|
+
if (params.page) url.searchParams.set("page", String(params.page));
|
|
360
|
+
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
361
|
+
if (params.category) url.searchParams.set("category", params.category);
|
|
362
|
+
if (params.source) url.searchParams.set("source", params.source);
|
|
363
|
+
if (params.service_id) url.searchParams.set("service_id", params.service_id);
|
|
364
|
+
return request(
|
|
365
|
+
url.toString(),
|
|
366
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
367
|
+
DEFAULT_TIMEOUT_MS,
|
|
368
|
+
IDEMPOTENT_RETRIES
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async function actionSearch(query, opts, params = {}) {
|
|
372
|
+
const url = new URL(`${baseUrl(opts)}/v1/actions/search`);
|
|
373
|
+
url.searchParams.set("q", query);
|
|
374
|
+
if (params.category) url.searchParams.set("category", params.category);
|
|
375
|
+
if (params.source) url.searchParams.set("source", params.source);
|
|
376
|
+
if (params.page) url.searchParams.set("page", String(params.page));
|
|
377
|
+
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
378
|
+
if (params.include_all_versions) url.searchParams.set("include_all_versions", "true");
|
|
379
|
+
if (params.sort) url.searchParams.set("sort", params.sort);
|
|
380
|
+
return request(
|
|
381
|
+
url.toString(),
|
|
382
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
383
|
+
DEFAULT_TIMEOUT_MS,
|
|
384
|
+
IDEMPOTENT_RETRIES
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
async function actionCategories(opts, params = {}) {
|
|
388
|
+
const url = new URL(`${baseUrl(opts)}/v1/actions/categories`);
|
|
389
|
+
if (params.source) url.searchParams.set("source", params.source);
|
|
390
|
+
return request(
|
|
391
|
+
url.toString(),
|
|
392
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
393
|
+
DEFAULT_TIMEOUT_MS,
|
|
394
|
+
IDEMPOTENT_RETRIES
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
async function actionGet(id, opts) {
|
|
398
|
+
return request(
|
|
399
|
+
`${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
|
|
400
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
401
|
+
DEFAULT_TIMEOUT_MS,
|
|
402
|
+
IDEMPOTENT_RETRIES
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
async function actionBatch(ids, opts) {
|
|
406
|
+
return request(
|
|
407
|
+
`${baseUrl(opts)}/v1/actions/batch`,
|
|
408
|
+
{
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: headers(opts.apiKey),
|
|
411
|
+
body: JSON.stringify({ ids })
|
|
412
|
+
},
|
|
413
|
+
DEFAULT_TIMEOUT_MS,
|
|
414
|
+
IDEMPOTENT_RETRIES
|
|
415
|
+
// read-only metadata fetch — safe to retry
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
419
|
+
return request(
|
|
420
|
+
`${baseUrl(opts)}/v1/actions/execute`,
|
|
421
|
+
{
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: headers(opts.apiKey),
|
|
424
|
+
body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
|
|
425
|
+
},
|
|
426
|
+
Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
|
|
427
|
+
retries
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
async function actionStream(actionId, input, opts, httpMethod) {
|
|
431
|
+
const controller = new AbortController();
|
|
432
|
+
let timedOut = false;
|
|
433
|
+
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
434
|
+
let timer;
|
|
435
|
+
const resetTimeout = (timeoutMs) => {
|
|
436
|
+
if (timer) clearTimeout(timer);
|
|
437
|
+
activeTimeoutMs = timeoutMs;
|
|
438
|
+
timer = setTimeout(() => {
|
|
439
|
+
timedOut = true;
|
|
440
|
+
controller.abort();
|
|
441
|
+
}, timeoutMs);
|
|
442
|
+
};
|
|
443
|
+
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
444
|
+
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
445
|
+
assertAllowedHost(url);
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch(url, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: {
|
|
450
|
+
...headers(opts.apiKey),
|
|
451
|
+
Accept: "text/event-stream"
|
|
452
|
+
},
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
action_id: actionId,
|
|
455
|
+
...httpMethod ? { method: httpMethod } : {},
|
|
456
|
+
input,
|
|
457
|
+
stream: true
|
|
458
|
+
}),
|
|
459
|
+
redirect: "manual",
|
|
460
|
+
signal: controller.signal
|
|
461
|
+
});
|
|
462
|
+
if (res.status >= 300 && res.status < 400) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (!res.ok) {
|
|
468
|
+
const text = await res.text();
|
|
469
|
+
throw new HttpError(
|
|
470
|
+
res.status,
|
|
471
|
+
text.slice(0, 300),
|
|
472
|
+
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const contentType = res.headers.get("content-type") || "";
|
|
476
|
+
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
477
|
+
const text = await res.text();
|
|
478
|
+
throw new Error(
|
|
479
|
+
`expected an SSE response but received "${contentType || "unknown"}": ${text.slice(0, 300)}`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
if (!res.body) return;
|
|
483
|
+
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
484
|
+
resetTimeout(idleTimeoutMs);
|
|
485
|
+
const source = Readable.fromWeb(res.body);
|
|
486
|
+
for await (const chunk of source) {
|
|
487
|
+
resetTimeout(idleTimeoutMs);
|
|
488
|
+
if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
492
|
+
throw error;
|
|
493
|
+
} finally {
|
|
494
|
+
if (timer) clearTimeout(timer);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
498
|
+
const controller = new AbortController();
|
|
499
|
+
let timedOut = false;
|
|
500
|
+
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
501
|
+
let timer;
|
|
502
|
+
const resetTimeout = (timeoutMs) => {
|
|
503
|
+
if (timer) clearTimeout(timer);
|
|
504
|
+
activeTimeoutMs = timeoutMs;
|
|
505
|
+
timer = setTimeout(() => {
|
|
506
|
+
timedOut = true;
|
|
507
|
+
controller.abort();
|
|
508
|
+
}, timeoutMs);
|
|
509
|
+
};
|
|
510
|
+
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
511
|
+
const target = resolve(outputPath);
|
|
512
|
+
let file;
|
|
513
|
+
let complete = false;
|
|
514
|
+
try {
|
|
515
|
+
try {
|
|
516
|
+
file = await open(target, "wx");
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error?.code === "EEXIST") {
|
|
519
|
+
throw new Error(`Output file already exists: ${target}`);
|
|
520
|
+
}
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
524
|
+
assertAllowedHost(url);
|
|
525
|
+
const res = await fetch(url, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: headers(opts.apiKey),
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
action_id: actionId,
|
|
530
|
+
...httpMethod ? { method: httpMethod } : {},
|
|
531
|
+
input,
|
|
532
|
+
response_mode: "raw"
|
|
533
|
+
}),
|
|
534
|
+
redirect: "manual",
|
|
535
|
+
signal: controller.signal
|
|
536
|
+
});
|
|
537
|
+
if (res.status >= 300 && res.status < 400) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (!res.ok) {
|
|
543
|
+
const text = await res.text();
|
|
544
|
+
throw new HttpError(
|
|
545
|
+
res.status,
|
|
546
|
+
text.slice(0, 300),
|
|
547
|
+
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
let bytes = 0;
|
|
551
|
+
if (res.body) {
|
|
552
|
+
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
553
|
+
resetTimeout(idleTimeoutMs);
|
|
554
|
+
const source = Readable.fromWeb(res.body);
|
|
555
|
+
const counter = new Transform({
|
|
556
|
+
transform(chunk, _encoding, callback) {
|
|
557
|
+
resetTimeout(idleTimeoutMs);
|
|
558
|
+
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
559
|
+
callback(null, chunk);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
await pipeline(source, counter, file.createWriteStream());
|
|
563
|
+
} else {
|
|
564
|
+
await file.close();
|
|
565
|
+
}
|
|
566
|
+
complete = true;
|
|
567
|
+
return {
|
|
568
|
+
output: target,
|
|
569
|
+
bytes,
|
|
570
|
+
contentType: res.headers.get("content-type") || void 0,
|
|
571
|
+
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
572
|
+
status: res.status
|
|
573
|
+
};
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
576
|
+
throw error;
|
|
577
|
+
} finally {
|
|
578
|
+
if (timer) clearTimeout(timer);
|
|
579
|
+
if (!complete && file) {
|
|
580
|
+
await file.close().catch(() => void 0);
|
|
581
|
+
await rm(target, { force: true }).catch(() => void 0);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function actionServices(opts, params = {}) {
|
|
586
|
+
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
587
|
+
if (params.page) url.searchParams.set("page", String(params.page));
|
|
588
|
+
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
589
|
+
if (params.category) url.searchParams.set("category", params.category);
|
|
590
|
+
return request(
|
|
591
|
+
url.toString(),
|
|
592
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
593
|
+
DEFAULT_TIMEOUT_MS,
|
|
594
|
+
IDEMPOTENT_RETRIES
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
async function healthCheck(opts) {
|
|
598
|
+
return request(
|
|
599
|
+
`${baseUrl(opts)}/health`,
|
|
600
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
601
|
+
5e3,
|
|
602
|
+
0
|
|
603
|
+
// health is a quick connectivity probe — fail fast, don't retry
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
async function loginWithApiKey(apiKey, apiHost) {
|
|
607
|
+
return request(
|
|
608
|
+
`${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
|
|
609
|
+
{
|
|
610
|
+
method: "POST",
|
|
611
|
+
headers: { "Content-Type": "application/json" },
|
|
612
|
+
body: JSON.stringify({ apiKey })
|
|
613
|
+
},
|
|
614
|
+
DEFAULT_TIMEOUT_MS,
|
|
615
|
+
IDEMPOTENT_RETRIES
|
|
616
|
+
// auth exchange has no side effect — safe to retry
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
function jwtHeaders(jwtToken) {
|
|
620
|
+
return { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` };
|
|
621
|
+
}
|
|
622
|
+
async function listKeys(jwtToken, apiHost) {
|
|
623
|
+
return request(
|
|
624
|
+
`${scheme(apiHost)}://${apiHost}/api/keys`,
|
|
625
|
+
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
626
|
+
DEFAULT_TIMEOUT_MS,
|
|
627
|
+
IDEMPOTENT_RETRIES
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
631
|
+
return request(
|
|
632
|
+
`${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
|
|
633
|
+
{
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: jwtHeaders(jwtToken),
|
|
636
|
+
body: JSON.stringify({ plaintextKey })
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
async function listOAuthProviders(apiHost) {
|
|
641
|
+
return request(
|
|
642
|
+
`${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
|
|
643
|
+
{ method: "GET", headers: { "Content-Type": "application/json" } },
|
|
644
|
+
DEFAULT_TIMEOUT_MS,
|
|
645
|
+
IDEMPOTENT_RETRIES
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
649
|
+
const body = { apiKeyId, providerId };
|
|
650
|
+
if (scopes) body.scopes = scopes;
|
|
651
|
+
return request(
|
|
652
|
+
`${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
|
|
653
|
+
{
|
|
654
|
+
method: "POST",
|
|
655
|
+
headers: jwtHeaders(jwtToken),
|
|
656
|
+
body: JSON.stringify(body)
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
async function listOAuthBindings(jwtToken, apiHost) {
|
|
661
|
+
return request(
|
|
662
|
+
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
|
|
663
|
+
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
664
|
+
DEFAULT_TIMEOUT_MS,
|
|
665
|
+
IDEMPOTENT_RETRIES
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
|
|
669
|
+
const result = await request(
|
|
670
|
+
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
|
|
671
|
+
{ method: "DELETE", headers: jwtHeaders(jwtToken) }
|
|
672
|
+
);
|
|
673
|
+
return result ?? { success: true };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/sandbox-client.ts
|
|
677
|
+
var READ_RETRIES = 2;
|
|
678
|
+
var READ_TIMEOUT_MS = 3e4;
|
|
679
|
+
var MUTATION_TIMEOUT_MS = 18e4;
|
|
680
|
+
var PRODUCTION_PROVIDER_HOSTS = {
|
|
681
|
+
daytona: "daytona-sandbox",
|
|
682
|
+
e2b: "e2b-sandbox"
|
|
683
|
+
};
|
|
684
|
+
function parsedHost(raw) {
|
|
685
|
+
const value = raw.trim();
|
|
686
|
+
if (!value) throw new Error("sandbox host is empty");
|
|
687
|
+
const url = new URL(value.includes("://") ? value : `${scheme(value)}://${value}`);
|
|
688
|
+
if (url.username || url.password) throw new Error("sandbox host must not contain credentials");
|
|
689
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
690
|
+
throw new Error("sandbox host must not contain a path, query, or fragment");
|
|
691
|
+
}
|
|
692
|
+
const loopback = isLoopbackHost(url.toString());
|
|
693
|
+
if (loopback && url.protocol !== "http:" && url.protocol !== "https:") {
|
|
694
|
+
throw new Error("localhost sandbox hosts must use HTTP or HTTPS");
|
|
695
|
+
}
|
|
696
|
+
if (!loopback && url.protocol !== "https:") {
|
|
697
|
+
throw new Error("public sandbox hosts must use HTTPS");
|
|
698
|
+
}
|
|
699
|
+
return url;
|
|
700
|
+
}
|
|
701
|
+
function sandboxBaseUrl(host, provider) {
|
|
702
|
+
const url = parsedHost(host);
|
|
703
|
+
const pin = provider && provider !== "auto" ? provider : void 0;
|
|
704
|
+
if (pin) {
|
|
705
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(pin)) {
|
|
706
|
+
throw new Error(`invalid sandbox provider: ${pin}`);
|
|
707
|
+
}
|
|
708
|
+
if (isLoopbackHost(url.toString())) {
|
|
709
|
+
throw new Error("provider pinning is unavailable for a localhost sandbox gateway");
|
|
710
|
+
}
|
|
711
|
+
const labels = url.hostname.split(".");
|
|
712
|
+
const sandboxIndex = labels.indexOf("sandbox");
|
|
713
|
+
const isProduction = sandboxIndex >= 0 && labels.slice(sandboxIndex).join(".") === "sandbox.xapi.to";
|
|
714
|
+
const gatewayLabel = isProduction ? PRODUCTION_PROVIDER_HOSTS[pin] || pin : pin;
|
|
715
|
+
if (sandboxIndex === 0) labels.unshift(gatewayLabel);
|
|
716
|
+
else if (sandboxIndex === 1) labels[0] = gatewayLabel;
|
|
717
|
+
else throw new Error("provider pinning requires a sandbox.<xapi-domain> host");
|
|
718
|
+
url.hostname = labels.join(".");
|
|
719
|
+
}
|
|
720
|
+
assertAllowedHost(url.toString());
|
|
721
|
+
const hostname = url.hostname.toLowerCase();
|
|
722
|
+
if (!isLoopbackHost(url.toString()) && hostname !== "xapi.to" && !hostname.endsWith(".xapi.to")) {
|
|
723
|
+
throw new Error("sandbox API keys may only be sent to *.xapi.to or localhost");
|
|
724
|
+
}
|
|
725
|
+
return url.toString().replace(/\/$/, "");
|
|
726
|
+
}
|
|
727
|
+
function headers2(apiKey, body) {
|
|
728
|
+
return {
|
|
729
|
+
Accept: "application/json",
|
|
730
|
+
"XAPI-Key": apiKey,
|
|
731
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function sandboxRequest(opts, path, init = {}) {
|
|
735
|
+
const method = init.method || "GET";
|
|
736
|
+
const hasBody = init.body !== void 0;
|
|
737
|
+
return request(
|
|
738
|
+
`${sandboxBaseUrl(opts.sandboxHost, opts.provider)}${path}`,
|
|
739
|
+
{
|
|
740
|
+
method,
|
|
741
|
+
headers: headers2(opts.apiKey, hasBody),
|
|
742
|
+
...hasBody ? { body: JSON.stringify(init.body) } : {},
|
|
743
|
+
...init.signal ? { signal: init.signal } : {}
|
|
744
|
+
},
|
|
745
|
+
init.timeoutMs || (init.readOnly || method === "GET" ? READ_TIMEOUT_MS : MUTATION_TIMEOUT_MS),
|
|
746
|
+
init.readOnly || method === "GET" ? READ_RETRIES : 0
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
var sandboxOfferings = (opts) => sandboxRequest(opts, "/v1/offerings");
|
|
750
|
+
var sandboxQuote = (opts, body, signal) => sandboxRequest(opts, "/v1/quotes", { method: "POST", body, readOnly: true, signal });
|
|
751
|
+
var sandboxList = (opts) => sandboxRequest(opts, "/v1/sandboxes");
|
|
752
|
+
var sandboxHistory = (opts, filters = {}) => {
|
|
753
|
+
const query = new URLSearchParams();
|
|
754
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
755
|
+
if (value !== void 0 && value !== "") query.set(key, String(value));
|
|
756
|
+
}
|
|
757
|
+
return sandboxRequest(opts, `/v1/sandbox-history${query.size ? `?${query}` : ""}`);
|
|
758
|
+
};
|
|
759
|
+
var sandboxGet = (opts, id, signal) => sandboxRequest(opts, `/v1/sandboxes/${encodeURIComponent(id)}`, { signal });
|
|
760
|
+
var sandboxCreate = (opts, body) => sandboxRequest(opts, "/v1/sandboxes", { method: "POST", body });
|
|
761
|
+
var sandboxExec = (opts, id, body, signal) => sandboxRequest(
|
|
762
|
+
opts,
|
|
763
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/commands`,
|
|
764
|
+
{
|
|
765
|
+
method: "POST",
|
|
766
|
+
body,
|
|
767
|
+
signal,
|
|
768
|
+
timeoutMs: Math.max(MUTATION_TIMEOUT_MS, (body.timeoutSeconds || 60) * 1e3 + 3e4)
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
var sandboxFileWrite = (opts, id, body) => sandboxRequest(opts, `/v1/sandboxes/${encodeURIComponent(id)}/files`, { method: "POST", body });
|
|
772
|
+
var sandboxFileRead = (opts, id, path, encoding = "utf8") => sandboxRequest(
|
|
773
|
+
opts,
|
|
774
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}&encoding=${encoding}`
|
|
775
|
+
);
|
|
776
|
+
var sandboxFileList = (opts, id, path = ".", depth = 2) => sandboxRequest(
|
|
777
|
+
opts,
|
|
778
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/files/list?path=${encodeURIComponent(path)}&depth=${depth}`
|
|
779
|
+
);
|
|
780
|
+
var sandboxPort = (opts, id, port) => sandboxRequest(opts, `/v1/sandboxes/${encodeURIComponent(id)}/ports/${port}`);
|
|
781
|
+
var sandboxExtension = (opts, id, extensionId, body) => sandboxRequest(
|
|
782
|
+
opts,
|
|
783
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/extensions/${encodeURIComponent(extensionId)}`,
|
|
784
|
+
{ method: "POST", body }
|
|
785
|
+
);
|
|
786
|
+
var sandboxStateAction = (opts, id, action, body = {}) => sandboxRequest(
|
|
787
|
+
opts,
|
|
788
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/${action}`,
|
|
789
|
+
{ method: "POST", body }
|
|
790
|
+
);
|
|
791
|
+
var sandboxAudit = (opts, id, kind, page = 1, pageSize = 100) => sandboxRequest(
|
|
792
|
+
opts,
|
|
793
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/audit?kind=${encodeURIComponent(kind)}&page=${page}&pageSize=${pageSize}`
|
|
794
|
+
);
|
|
795
|
+
async function sandboxWait(opts, id, wanted, timeoutMs = 3e5, intervalMs = 2e3, signal) {
|
|
796
|
+
const deadline = Date.now() + timeoutMs;
|
|
797
|
+
let last;
|
|
798
|
+
while (Date.now() < deadline) {
|
|
799
|
+
if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
|
|
800
|
+
last = await sandboxGet(opts, id, signal);
|
|
801
|
+
const state = String(last.observedState || "");
|
|
802
|
+
if (wanted.includes(state)) return last;
|
|
803
|
+
if (["FAILED", "TERMINATED"].includes(state) && !wanted.includes(state)) {
|
|
804
|
+
throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(" or ")}`);
|
|
805
|
+
}
|
|
806
|
+
await new Promise((resolve2) => {
|
|
807
|
+
const done = () => {
|
|
808
|
+
clearTimeout(timer);
|
|
809
|
+
signal?.removeEventListener("abort", done);
|
|
810
|
+
resolve2();
|
|
811
|
+
};
|
|
812
|
+
const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
813
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
throw new Error(
|
|
817
|
+
`sandbox ${id} did not enter ${wanted.join(" or ")} within ${timeoutMs}ms (last state: ${last?.observedState || "unknown"})`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export {
|
|
822
|
+
__export,
|
|
823
|
+
getFormat,
|
|
824
|
+
output,
|
|
825
|
+
err,
|
|
826
|
+
XAPI_API_HOST,
|
|
827
|
+
XAPI_SANDBOX_HOST,
|
|
828
|
+
scheme,
|
|
829
|
+
assertAllowedHost,
|
|
830
|
+
getApiKeySource,
|
|
831
|
+
getConfig,
|
|
832
|
+
requireApiKey,
|
|
833
|
+
saveConfig,
|
|
834
|
+
showConfig,
|
|
835
|
+
HttpError,
|
|
836
|
+
isRetryableRequestError,
|
|
837
|
+
request,
|
|
838
|
+
actionList,
|
|
839
|
+
actionSearch,
|
|
840
|
+
actionCategories,
|
|
841
|
+
actionGet,
|
|
842
|
+
actionBatch,
|
|
843
|
+
actionCall,
|
|
844
|
+
actionStream,
|
|
845
|
+
actionDownload,
|
|
846
|
+
actionServices,
|
|
847
|
+
healthCheck,
|
|
848
|
+
loginWithApiKey,
|
|
849
|
+
listKeys,
|
|
850
|
+
enableOAuthForKey,
|
|
851
|
+
listOAuthProviders,
|
|
852
|
+
initiateOAuth,
|
|
853
|
+
listOAuthBindings,
|
|
854
|
+
deleteOAuthBinding,
|
|
855
|
+
sandboxOfferings,
|
|
856
|
+
sandboxQuote,
|
|
857
|
+
sandboxList,
|
|
858
|
+
sandboxHistory,
|
|
859
|
+
sandboxGet,
|
|
860
|
+
sandboxCreate,
|
|
861
|
+
sandboxExec,
|
|
862
|
+
sandboxFileWrite,
|
|
863
|
+
sandboxFileRead,
|
|
864
|
+
sandboxFileList,
|
|
865
|
+
sandboxPort,
|
|
866
|
+
sandboxExtension,
|
|
867
|
+
sandboxStateAction,
|
|
868
|
+
sandboxAudit,
|
|
869
|
+
sandboxWait
|
|
870
|
+
};
|