@kosa8/sdk 0.1.0 → 0.3.0
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 +82 -4
- package/dist/index.cjs +748 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -1
- package/dist/index.d.ts +546 -1
- package/dist/index.js +737 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,12 @@ __export(index_exports, {
|
|
|
33
33
|
ACTOR_HEADER: () => ACTOR_HEADER,
|
|
34
34
|
AGENT_TOKEN_HEADER: () => AGENT_TOKEN_HEADER,
|
|
35
35
|
APPROVAL_HEADER: () => APPROVAL_HEADER,
|
|
36
|
+
CLOUD_KEY_PREFIX: () => CLOUD_KEY_PREFIX,
|
|
36
37
|
CheckpointBook: () => CheckpointBook,
|
|
38
|
+
CloudFunctions: () => CloudFunctions,
|
|
39
|
+
CloudSandbox: () => CloudSandbox,
|
|
40
|
+
CloudSandboxes: () => CloudSandboxes,
|
|
41
|
+
DEFAULT_CLOUD_URL: () => DEFAULT_CLOUD_URL,
|
|
37
42
|
FileCheckpointStore: () => FileCheckpointStore,
|
|
38
43
|
Kosa8ApiError: () => Kosa8ApiError,
|
|
39
44
|
Kosa8ApprovalRequiredError: () => Kosa8ApprovalRequiredError,
|
|
@@ -41,20 +46,26 @@ __export(index_exports, {
|
|
|
41
46
|
Kosa8BadRequestError: () => Kosa8BadRequestError,
|
|
42
47
|
Kosa8CheckpointError: () => Kosa8CheckpointError,
|
|
43
48
|
Kosa8Client: () => Kosa8Client,
|
|
49
|
+
Kosa8Cloud: () => Kosa8Cloud,
|
|
44
50
|
Kosa8ConflictError: () => Kosa8ConflictError,
|
|
45
51
|
Kosa8ConnectionError: () => Kosa8ConnectionError,
|
|
46
52
|
Kosa8Error: () => Kosa8Error,
|
|
47
53
|
Kosa8NotFoundError: () => Kosa8NotFoundError,
|
|
54
|
+
Kosa8PaymentRequiredError: () => Kosa8PaymentRequiredError,
|
|
48
55
|
Kosa8PolicyError: () => Kosa8PolicyError,
|
|
56
|
+
Kosa8RateLimitError: () => Kosa8RateLimitError,
|
|
49
57
|
Kosa8StreamError: () => Kosa8StreamError,
|
|
58
|
+
Kosa8TimeoutError: () => Kosa8TimeoutError,
|
|
50
59
|
Kosa8UnsupportedError: () => Kosa8UnsupportedError,
|
|
51
60
|
MAX_CHECKPOINTS_PER_SANDBOX: () => MAX_CHECKPOINTS_PER_SANDBOX,
|
|
52
61
|
MemoryCheckpointStore: () => MemoryCheckpointStore,
|
|
53
62
|
SandboxHandle: () => SandboxHandle,
|
|
54
63
|
Transport: () => Transport,
|
|
55
64
|
defaultSocketPath: () => defaultSocketPath,
|
|
65
|
+
defaultWorkerFile: () => defaultWorkerFile,
|
|
56
66
|
errorFromResponse: () => errorFromResponse,
|
|
57
67
|
resolveEndpoint: () => resolveEndpoint,
|
|
68
|
+
tarContext: () => tarContext,
|
|
58
69
|
validateSandboxName: () => validateSandboxName
|
|
59
70
|
});
|
|
60
71
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -76,6 +87,8 @@ var Kosa8Error = class extends Error {
|
|
|
76
87
|
path;
|
|
77
88
|
/** The decoded response body, when there was one. */
|
|
78
89
|
body;
|
|
90
|
+
/** kosa8 Cloud's stable error code (payment_required, quota_exceeded, ...). */
|
|
91
|
+
code;
|
|
79
92
|
constructor(message, options = {}) {
|
|
80
93
|
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
81
94
|
this.name = new.target.name;
|
|
@@ -85,10 +98,13 @@ var Kosa8Error = class extends Error {
|
|
|
85
98
|
this.method = options.method;
|
|
86
99
|
this.path = options.path;
|
|
87
100
|
this.body = options.body;
|
|
101
|
+
this.code = options.code;
|
|
88
102
|
}
|
|
89
103
|
};
|
|
90
104
|
var Kosa8ConnectionError = class extends Kosa8Error {
|
|
91
105
|
};
|
|
106
|
+
var Kosa8TimeoutError = class extends Kosa8ConnectionError {
|
|
107
|
+
};
|
|
92
108
|
var Kosa8ApiError = class extends Kosa8Error {
|
|
93
109
|
};
|
|
94
110
|
var Kosa8BadRequestError = class extends Kosa8ApiError {
|
|
@@ -119,6 +135,10 @@ var Kosa8StreamError = class extends Kosa8Error {
|
|
|
119
135
|
this.stderr = out.stderr;
|
|
120
136
|
}
|
|
121
137
|
};
|
|
138
|
+
var Kosa8PaymentRequiredError = class extends Kosa8ApiError {
|
|
139
|
+
};
|
|
140
|
+
var Kosa8RateLimitError = class extends Kosa8ApiError {
|
|
141
|
+
};
|
|
122
142
|
var Kosa8CheckpointError = class extends Kosa8Error {
|
|
123
143
|
};
|
|
124
144
|
function isRefusal(v) {
|
|
@@ -152,8 +172,12 @@ function errorFromResponse(status, body, ctx = {}) {
|
|
|
152
172
|
return new Kosa8PolicyError(message, opts);
|
|
153
173
|
case 404:
|
|
154
174
|
return new Kosa8NotFoundError(message, opts);
|
|
175
|
+
case 402:
|
|
176
|
+
return new Kosa8PaymentRequiredError(message, opts);
|
|
155
177
|
case 409:
|
|
156
178
|
return new Kosa8ConflictError(message, opts);
|
|
179
|
+
case 429:
|
|
180
|
+
return new Kosa8RateLimitError(message, opts);
|
|
157
181
|
case 501:
|
|
158
182
|
return new Kosa8UnsupportedError(message, opts);
|
|
159
183
|
default:
|
|
@@ -331,16 +355,136 @@ var CheckpointBook = class {
|
|
|
331
355
|
}
|
|
332
356
|
};
|
|
333
357
|
|
|
358
|
+
// src/tar.ts
|
|
359
|
+
var import_node_fs2 = require("fs");
|
|
360
|
+
var path2 = __toESM(require("path"), 1);
|
|
361
|
+
var BLOCK = 512;
|
|
362
|
+
function octal(buf, off, len, n) {
|
|
363
|
+
buf.write(n.toString(8).padStart(len - 1, "0") + "\0", off, len, "ascii");
|
|
364
|
+
}
|
|
365
|
+
function header(name, type, size, mode, mtime, link = "") {
|
|
366
|
+
const h = Buffer.alloc(BLOCK);
|
|
367
|
+
h.write(name, 0, 100, "utf8");
|
|
368
|
+
octal(h, 100, 8, mode & 4095);
|
|
369
|
+
octal(h, 108, 8, 0);
|
|
370
|
+
octal(h, 116, 8, 0);
|
|
371
|
+
octal(h, 124, 12, size);
|
|
372
|
+
octal(h, 136, 12, mtime);
|
|
373
|
+
h.fill(" ", 148, 156);
|
|
374
|
+
h.write(type, 156, 1, "ascii");
|
|
375
|
+
h.write(link, 157, 100, "utf8");
|
|
376
|
+
h.write("ustar\0", 257, 6, "ascii");
|
|
377
|
+
h.write("00", 263, 2, "ascii");
|
|
378
|
+
let sum = 0;
|
|
379
|
+
for (const b of h) sum += b;
|
|
380
|
+
h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
|
|
381
|
+
return h;
|
|
382
|
+
}
|
|
383
|
+
function paxRecord(key, value) {
|
|
384
|
+
const body = ` ${key}=${value}
|
|
385
|
+
`;
|
|
386
|
+
let n = Buffer.byteLength(body);
|
|
387
|
+
let len = n + String(n).length;
|
|
388
|
+
if (String(len).length !== String(n).length) len = n + String(len).length;
|
|
389
|
+
return `${len}${body}`;
|
|
390
|
+
}
|
|
391
|
+
function pad(n) {
|
|
392
|
+
const r = n % BLOCK;
|
|
393
|
+
return Buffer.alloc(r === 0 ? 0 : BLOCK - r);
|
|
394
|
+
}
|
|
395
|
+
function entry(out, name, type, mode, mtime, data, link = "") {
|
|
396
|
+
let pax = "";
|
|
397
|
+
if (Buffer.byteLength(name) > 100) pax += paxRecord("path", name);
|
|
398
|
+
if (Buffer.byteLength(link) > 100) pax += paxRecord("linkpath", link);
|
|
399
|
+
if (pax !== "") {
|
|
400
|
+
const p = Buffer.from(pax, "utf8");
|
|
401
|
+
out.push(header("PaxHeader", "x", p.length, 420, mtime), p, pad(p.length));
|
|
402
|
+
}
|
|
403
|
+
const size = data?.length ?? 0;
|
|
404
|
+
out.push(header(name.slice(0, 100), type, size, mode, mtime, link.slice(0, 100)));
|
|
405
|
+
if (data) out.push(data, pad(size));
|
|
406
|
+
}
|
|
407
|
+
async function readIgnore(dir) {
|
|
408
|
+
let text;
|
|
409
|
+
try {
|
|
410
|
+
text = await import_node_fs2.promises.readFile(path2.join(dir, ".dockerignore"), "utf8");
|
|
411
|
+
} catch {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
const out = [];
|
|
415
|
+
for (const line of text.split(/\r?\n/)) {
|
|
416
|
+
const l = line.trim();
|
|
417
|
+
if (l === "" || line.startsWith("#")) continue;
|
|
418
|
+
out.push(path2.posix.normalize(l.replace(/\\/g, "/")).replace(/\/+$/, "") || ".");
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
async function tarContext(dir, dockerfile = "Dockerfile") {
|
|
423
|
+
try {
|
|
424
|
+
await import_node_fs2.promises.access(path2.join(dir, dockerfile));
|
|
425
|
+
} catch {
|
|
426
|
+
throw new Kosa8Error(`no ${dockerfile} in ${dir}`);
|
|
427
|
+
}
|
|
428
|
+
const ignore = await readIgnore(dir);
|
|
429
|
+
const skip = (rel) => ignore.some((p) => rel === p || rel.startsWith(p + "/"));
|
|
430
|
+
const out = [];
|
|
431
|
+
const walk = async (rel) => {
|
|
432
|
+
const ents = await import_node_fs2.promises.readdir(path2.join(dir, rel), { withFileTypes: true });
|
|
433
|
+
ents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
434
|
+
const subdirs = [];
|
|
435
|
+
for (const e of ents) {
|
|
436
|
+
const r = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
437
|
+
if (skip(r)) continue;
|
|
438
|
+
const full = path2.join(dir, r);
|
|
439
|
+
const st = await import_node_fs2.promises.lstat(full);
|
|
440
|
+
const mtime = Math.floor(st.mtimeMs / 1e3);
|
|
441
|
+
if (st.isSymbolicLink()) {
|
|
442
|
+
entry(out, r, "2", st.mode, mtime, void 0, await import_node_fs2.promises.readlink(full));
|
|
443
|
+
} else if (st.isDirectory()) {
|
|
444
|
+
entry(out, r + "/", "5", st.mode, mtime);
|
|
445
|
+
subdirs.push(r);
|
|
446
|
+
} else if (st.isFile()) {
|
|
447
|
+
entry(out, r, "0", st.mode, mtime, await import_node_fs2.promises.readFile(full));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const s of subdirs) await walk(s);
|
|
451
|
+
};
|
|
452
|
+
await walk("");
|
|
453
|
+
out.push(Buffer.alloc(BLOCK * 2));
|
|
454
|
+
return Buffer.concat(out);
|
|
455
|
+
}
|
|
456
|
+
|
|
334
457
|
// src/transport.ts
|
|
458
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
335
459
|
var http = __toESM(require("http"), 1);
|
|
336
460
|
var https = __toESM(require("https"), 1);
|
|
337
461
|
var os2 = __toESM(require("os"), 1);
|
|
338
|
-
var
|
|
462
|
+
var path3 = __toESM(require("path"), 1);
|
|
339
463
|
var APPROVAL_HEADER = "X-Kosa8-Approval";
|
|
340
464
|
var ACTOR_HEADER = "X-Kosa8-Actor";
|
|
341
465
|
var AGENT_TOKEN_HEADER = "X-Kosa8-Agent-Token";
|
|
342
466
|
function defaultSocketPath() {
|
|
343
|
-
return
|
|
467
|
+
return path3.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
|
|
468
|
+
}
|
|
469
|
+
function defaultWorkerFile() {
|
|
470
|
+
return path3.join(os2.homedir(), ".kosa8", "worker.json");
|
|
471
|
+
}
|
|
472
|
+
function loadSavedWorker(file) {
|
|
473
|
+
let w;
|
|
474
|
+
try {
|
|
475
|
+
w = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
476
|
+
} catch {
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
if (!w || typeof w !== "object") return void 0;
|
|
480
|
+
const o = w;
|
|
481
|
+
const host = typeof o.host === "string" ? o.host.trim() : "";
|
|
482
|
+
if (host === "") return void 0;
|
|
483
|
+
return {
|
|
484
|
+
host,
|
|
485
|
+
token: typeof o.token === "string" && o.token !== "" ? o.token : void 0,
|
|
486
|
+
insecure: o.insecure === true
|
|
487
|
+
};
|
|
344
488
|
}
|
|
345
489
|
function isLoopback(hostname) {
|
|
346
490
|
const h = hostname.replace(/^\[|\]$/g, "");
|
|
@@ -349,11 +493,20 @@ function isLoopback(hostname) {
|
|
|
349
493
|
function resolveEndpoint(opts) {
|
|
350
494
|
const env = opts.env ?? process.env;
|
|
351
495
|
const fromEnv = (env.KOSA8_HOST ?? "").trim();
|
|
352
|
-
const raw = (opts.host ?? fromEnv).trim()
|
|
353
|
-
|
|
354
|
-
|
|
496
|
+
const raw = (opts.host ?? fromEnv).trim();
|
|
497
|
+
const envInsecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
|
|
498
|
+
if (raw !== "") {
|
|
499
|
+
const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
|
|
500
|
+
return parseHost(raw, envInsecure, token);
|
|
501
|
+
}
|
|
502
|
+
if (opts.socketPath === void 0 && opts.workerFile !== null) {
|
|
503
|
+
const saved = loadSavedWorker(opts.workerFile ?? defaultWorkerFile());
|
|
504
|
+
if (saved) return parseHost(saved.host, envInsecure || saved.insecure, opts.token ?? saved.token);
|
|
355
505
|
}
|
|
356
|
-
|
|
506
|
+
return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
|
|
507
|
+
}
|
|
508
|
+
function parseHost(raw, insecure, token) {
|
|
509
|
+
let h = raw.replace(/\/+$/, "");
|
|
357
510
|
const i = h.indexOf("://");
|
|
358
511
|
if (i > 0) h = h.slice(0, i).toLowerCase() + h.slice(i);
|
|
359
512
|
if (h.startsWith("unix://")) {
|
|
@@ -369,13 +522,11 @@ function resolveEndpoint(opts) {
|
|
|
369
522
|
h = "http://" + h;
|
|
370
523
|
}
|
|
371
524
|
const url = new URL(h);
|
|
372
|
-
const insecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
|
|
373
525
|
if (url.protocol === "http:" && !insecure && !isLoopback(url.hostname)) {
|
|
374
526
|
throw new Kosa8Error(
|
|
375
527
|
`refusing to talk to ${url.host} over plain http: the token and every request would cross the network unencrypted. Use https://${url.host} (a worker behind a TLS-terminating tunnel or proxy), or set insecure: true / KOSA8_INSECURE=1 if this link is already private and trusted`
|
|
376
528
|
);
|
|
377
529
|
}
|
|
378
|
-
const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
|
|
379
530
|
return { kind: "tcp", url, token: token || void 0 };
|
|
380
531
|
}
|
|
381
532
|
var Transport = class {
|
|
@@ -467,7 +618,7 @@ var Transport = class {
|
|
|
467
618
|
}
|
|
468
619
|
reject(
|
|
469
620
|
new Kosa8ConnectionError(
|
|
470
|
-
`kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
|
|
621
|
+
`kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? this.endpoint.kind === "unix" && process.platform === "win32" ? " (on Windows the engine runs in WSL2: run `kosa8 wsl setup`, or set KOSA8_HOST)" : " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
|
|
471
622
|
{ method, path: p, cause: err }
|
|
472
623
|
)
|
|
473
624
|
);
|
|
@@ -580,6 +731,12 @@ function envList(env) {
|
|
|
580
731
|
return Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
581
732
|
}
|
|
582
733
|
var enc = encodeURIComponent;
|
|
734
|
+
function str(v) {
|
|
735
|
+
return typeof v === "string" ? v : "";
|
|
736
|
+
}
|
|
737
|
+
function num(v) {
|
|
738
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
739
|
+
}
|
|
583
740
|
var Kosa8Client = class {
|
|
584
741
|
transport;
|
|
585
742
|
checkpoints;
|
|
@@ -624,6 +781,11 @@ var Kosa8Client = class {
|
|
|
624
781
|
if (options.capture) body.capture = options.capture;
|
|
625
782
|
if (options.labels) body.labels = options.labels;
|
|
626
783
|
if (options.broker) body.broker = options.broker;
|
|
784
|
+
if (options.autoStopMin) body.auto_stop_min = options.autoStopMin;
|
|
785
|
+
if (options.autoDeleteMin) body.auto_delete_min = options.autoDeleteMin;
|
|
786
|
+
if (options.ttlMin) body.ttl_min = options.ttlMin;
|
|
787
|
+
if (options.ephemeral) body.ephemeral = true;
|
|
788
|
+
if (options.template) body.template = options.template;
|
|
627
789
|
return this.transport.json("POST", "/kosa8/v1/sandboxes", {
|
|
628
790
|
body,
|
|
629
791
|
approval: options.approval,
|
|
@@ -920,6 +1082,315 @@ var Kosa8Client = class {
|
|
|
920
1082
|
}
|
|
921
1083
|
return { sandbox: restored.sandbox, checkpoint: target, dropped, restore_ms: restored.restore_ms };
|
|
922
1084
|
}
|
|
1085
|
+
// --- toolbox: code -------------------------------------------------------------
|
|
1086
|
+
/**
|
|
1087
|
+
* POST /kosa8/v1/sandboxes/{id}/code: run a Python, JavaScript or
|
|
1088
|
+
* TypeScript snippet. It starts in `/workspace`. Charts a Python snippet
|
|
1089
|
+
* draws with matplotlib come back in `artifacts` as PNG bytes.
|
|
1090
|
+
*/
|
|
1091
|
+
async runCode(id, code, opts = {}) {
|
|
1092
|
+
const body = { language: opts.language ?? "python", code };
|
|
1093
|
+
if (opts.image) body.image = opts.image;
|
|
1094
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1095
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1096
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/code`, {
|
|
1097
|
+
body,
|
|
1098
|
+
approval: opts.approval,
|
|
1099
|
+
signal: opts.signal
|
|
1100
|
+
});
|
|
1101
|
+
const exitCode = num(d.exit);
|
|
1102
|
+
const timedOut = d.timed_out === true;
|
|
1103
|
+
const arts = Array.isArray(d.artifacts) ? d.artifacts : [];
|
|
1104
|
+
return {
|
|
1105
|
+
ok: exitCode === 0 && !timedOut,
|
|
1106
|
+
exitCode,
|
|
1107
|
+
stdout: str(d.stdout),
|
|
1108
|
+
stderr: str(d.stderr),
|
|
1109
|
+
language: str(d.language),
|
|
1110
|
+
image: str(d.image),
|
|
1111
|
+
artifacts: arts.map((a) => ({ type: str(a.type), name: str(a.name), data: Buffer.from(str(a.data), "base64") })),
|
|
1112
|
+
durationMs: num(d.duration_ms),
|
|
1113
|
+
timedOut,
|
|
1114
|
+
truncated: d.truncated === true
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
// --- toolbox: files ------------------------------------------------------------
|
|
1118
|
+
/** GET …/files: a folder in the workspace (default `/workspace`), or one entry. */
|
|
1119
|
+
async listFiles(id, path4 = "/workspace", opts = {}) {
|
|
1120
|
+
const d = await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1121
|
+
...opts,
|
|
1122
|
+
query: { path: path4 }
|
|
1123
|
+
});
|
|
1124
|
+
return d?.files ?? [];
|
|
1125
|
+
}
|
|
1126
|
+
/** GET …/files/stat: one entry, not following a symlink. */
|
|
1127
|
+
statFile(id, path4, opts = {}) {
|
|
1128
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files/stat`, { ...opts, query: { path: path4 } });
|
|
1129
|
+
}
|
|
1130
|
+
/** GET …/files/content: a file's bytes. A body cut short is a Kosa8StreamError. */
|
|
1131
|
+
async readFile(id, path4, opts = {}) {
|
|
1132
|
+
const p = `/kosa8/v1/sandboxes/${enc(id)}/files/content`;
|
|
1133
|
+
const res = await this.transport.raw("GET", p, { query: { path: path4 }, signal: opts.signal, approval: opts.approval });
|
|
1134
|
+
let data;
|
|
1135
|
+
try {
|
|
1136
|
+
data = await readAll(res.stream, opts.signal);
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (opts.signal?.aborted) throw err;
|
|
1139
|
+
throw new Kosa8StreamError(
|
|
1140
|
+
`${path4}: read cut short: ${err instanceof Error ? err.message : String(err)}`,
|
|
1141
|
+
{ stdout: "", stderr: "" },
|
|
1142
|
+
{ method: "GET", path: p, cause: err }
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
if (res.status >= 400) {
|
|
1146
|
+
const text = data.toString("utf8");
|
|
1147
|
+
let body = text;
|
|
1148
|
+
try {
|
|
1149
|
+
body = JSON.parse(text);
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
throw errorFromResponse(res.status, body, { method: "GET", path: p });
|
|
1153
|
+
}
|
|
1154
|
+
const want = res.headers["content-length"];
|
|
1155
|
+
if (want !== void 0 && Number(want) !== data.length) {
|
|
1156
|
+
throw new Kosa8StreamError(`${path4}: got ${data.length} of ${want} bytes`, { stdout: "", stderr: "" }, { method: "GET", path: p });
|
|
1157
|
+
}
|
|
1158
|
+
return data;
|
|
1159
|
+
}
|
|
1160
|
+
/** A file's contents as text (UTF-8 by default). */
|
|
1161
|
+
async readText(id, path4, opts = {}) {
|
|
1162
|
+
return (await this.readFile(id, path4, opts)).toString(opts.encoding ?? "utf8");
|
|
1163
|
+
}
|
|
1164
|
+
/** PUT …/files/content: replace a file, making its folders. Answers the new entry. */
|
|
1165
|
+
writeFile(id, path4, data, opts = {}) {
|
|
1166
|
+
return this.transport.json("PUT", `/kosa8/v1/sandboxes/${enc(id)}/files/content`, {
|
|
1167
|
+
query: { path: path4, mode: opts.mode },
|
|
1168
|
+
rawBody: data,
|
|
1169
|
+
approval: opts.approval,
|
|
1170
|
+
signal: opts.signal
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
/** DELETE …/files: a file, or with `recursive` a folder and everything in it. */
|
|
1174
|
+
deleteFile(id, path4, opts = {}) {
|
|
1175
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1176
|
+
query: { path: path4, recursive: opts.recursive },
|
|
1177
|
+
approval: opts.approval,
|
|
1178
|
+
signal: opts.signal
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
/** POST …/files/mkdir: a folder, with its parents. */
|
|
1182
|
+
makeDir(id, path4, opts = {}) {
|
|
1183
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/mkdir`, { ...opts, body: { path: path4 } });
|
|
1184
|
+
}
|
|
1185
|
+
/** POST …/files/move: rename or move a file or folder. Answers the entry at `to`. */
|
|
1186
|
+
moveFile(id, from, to, opts = {}) {
|
|
1187
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/move`, { ...opts, body: { from, to } });
|
|
1188
|
+
}
|
|
1189
|
+
// --- toolbox: git ----------------------------------------------------------------
|
|
1190
|
+
/**
|
|
1191
|
+
* POST …/git: one git operation in the workspace. A git failure resolves
|
|
1192
|
+
* with `ok: false`; only a refusal (bad op, no sandbox, policy) throws.
|
|
1193
|
+
*/
|
|
1194
|
+
async git(id, op, opts = {}) {
|
|
1195
|
+
const { approval, signal, authorName, authorEmail, ...fields } = opts;
|
|
1196
|
+
const body = { op };
|
|
1197
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) body[k] = v;
|
|
1198
|
+
if (authorName !== void 0) body.author_name = authorName;
|
|
1199
|
+
if (authorEmail !== void 0) body.author_email = authorEmail;
|
|
1200
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/git`, {
|
|
1201
|
+
body,
|
|
1202
|
+
approval,
|
|
1203
|
+
signal
|
|
1204
|
+
});
|
|
1205
|
+
const out = {
|
|
1206
|
+
op: str(d.op) || op,
|
|
1207
|
+
ok: d.ok === true,
|
|
1208
|
+
exitCode: num(d.exit),
|
|
1209
|
+
stdout: str(d.stdout),
|
|
1210
|
+
stderr: str(d.stderr),
|
|
1211
|
+
path: str(d.path)
|
|
1212
|
+
};
|
|
1213
|
+
if (d.status) out.status = d.status;
|
|
1214
|
+
if (d.log) out.log = d.log;
|
|
1215
|
+
if (d.branches) out.branches = d.branches;
|
|
1216
|
+
return out;
|
|
1217
|
+
}
|
|
1218
|
+
/** Clone `url` into the workspace (into `path`, else a folder named after the repo). */
|
|
1219
|
+
gitClone(id, url, opts = {}) {
|
|
1220
|
+
return this.git(id, "clone", { ...opts, url });
|
|
1221
|
+
}
|
|
1222
|
+
/** `git status` of the repository at `path`, parsed into `status`. */
|
|
1223
|
+
gitStatus(id, path4, opts = {}) {
|
|
1224
|
+
return this.git(id, "status", { ...opts, path: path4 });
|
|
1225
|
+
}
|
|
1226
|
+
/** Stage everything (unless `addAll: false`) and commit. A failed add is returned as is. */
|
|
1227
|
+
async gitCommit(id, path4, message, opts = {}) {
|
|
1228
|
+
const { addAll = true, ...rest } = opts;
|
|
1229
|
+
if (addAll) {
|
|
1230
|
+
const r = await this.git(id, "add", { path: path4, image: rest.image, approval: rest.approval, signal: rest.signal });
|
|
1231
|
+
if (!r.ok) return r;
|
|
1232
|
+
}
|
|
1233
|
+
return this.git(id, "commit", { ...rest, path: path4, message });
|
|
1234
|
+
}
|
|
1235
|
+
// --- toolbox: shell sessions -------------------------------------------------------
|
|
1236
|
+
/** POST …/sessions: a shell that keeps `cd` and `export` between commands. */
|
|
1237
|
+
createSession(id, name, opts = {}) {
|
|
1238
|
+
const body = {};
|
|
1239
|
+
if (name) body.name = name;
|
|
1240
|
+
if (opts.image) body.image = opts.image;
|
|
1241
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1242
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, {
|
|
1243
|
+
body,
|
|
1244
|
+
approval: opts.approval,
|
|
1245
|
+
signal: opts.signal
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
/** GET …/sessions */
|
|
1249
|
+
async listSessions(id, opts = {}) {
|
|
1250
|
+
return await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, opts) ?? [];
|
|
1251
|
+
}
|
|
1252
|
+
/** POST …/sessions/{name}/exec: run one command line in the session. */
|
|
1253
|
+
async sessionExec(id, session, command, opts = {}) {
|
|
1254
|
+
const body = { command };
|
|
1255
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1256
|
+
const d = await this.transport.json(
|
|
1257
|
+
"POST",
|
|
1258
|
+
`/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}/exec`,
|
|
1259
|
+
{ body, approval: opts.approval, signal: opts.signal }
|
|
1260
|
+
);
|
|
1261
|
+
const exitCode = num(d.exit);
|
|
1262
|
+
const timedOut = d.timed_out === true;
|
|
1263
|
+
return {
|
|
1264
|
+
ok: exitCode === 0 && !timedOut,
|
|
1265
|
+
exitCode,
|
|
1266
|
+
stdout: str(d.stdout),
|
|
1267
|
+
stderr: str(d.stderr),
|
|
1268
|
+
cwd: str(d.cwd),
|
|
1269
|
+
durationMs: num(d.duration_ms),
|
|
1270
|
+
timedOut,
|
|
1271
|
+
truncated: d.truncated === true
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/** Run a command in a session (default `main`), making the session on first use. */
|
|
1275
|
+
async shell(id, command, opts = {}) {
|
|
1276
|
+
const { session = "main", ...rest } = opts;
|
|
1277
|
+
try {
|
|
1278
|
+
return await this.sessionExec(id, session, command, rest);
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
if (!(err instanceof Kosa8NotFoundError) || !err.message.includes("no such session")) throw err;
|
|
1281
|
+
}
|
|
1282
|
+
await this.createSession(id, session, { approval: rest.approval, signal: rest.signal });
|
|
1283
|
+
return this.sessionExec(id, session, command, rest);
|
|
1284
|
+
}
|
|
1285
|
+
/** DELETE …/sessions/{name} */
|
|
1286
|
+
deleteSession(id, session, opts = {}) {
|
|
1287
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}`, opts);
|
|
1288
|
+
}
|
|
1289
|
+
// --- toolbox: terminals ----------------------------------------------------------
|
|
1290
|
+
/**
|
|
1291
|
+
* POST …/pty: a container with a terminal; resolves with its id. Connect
|
|
1292
|
+
* to `/kosa8/v1/sandboxes/{id}/containers/{cid}/tty` with any WebSocket
|
|
1293
|
+
* client: binary messages are the terminal, text messages JSON control.
|
|
1294
|
+
*/
|
|
1295
|
+
async startPty(id, opts = {}) {
|
|
1296
|
+
const body = {};
|
|
1297
|
+
if (opts.image) body.image = opts.image;
|
|
1298
|
+
if (opts.command && opts.command.length > 0) body.command = opts.command;
|
|
1299
|
+
if (opts.cols && opts.rows) {
|
|
1300
|
+
body.cols = opts.cols;
|
|
1301
|
+
body.rows = opts.rows;
|
|
1302
|
+
}
|
|
1303
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1304
|
+
const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pty`, {
|
|
1305
|
+
body,
|
|
1306
|
+
approval: opts.approval,
|
|
1307
|
+
signal: opts.signal
|
|
1308
|
+
});
|
|
1309
|
+
return out.id;
|
|
1310
|
+
}
|
|
1311
|
+
/** POST …/containers/{cid}/resize: set a terminal's size. */
|
|
1312
|
+
resizePty(id, container, cols, rows, opts = {}) {
|
|
1313
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/resize`, {
|
|
1314
|
+
...opts,
|
|
1315
|
+
body: { cols, rows }
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
// --- toolbox: previews -----------------------------------------------------------
|
|
1319
|
+
/** POST …/previews: a signed link, which expires, to a web server in the sandbox. */
|
|
1320
|
+
createPreview(id, port, opts = {}) {
|
|
1321
|
+
const body = { port };
|
|
1322
|
+
if (opts.session) body.session = opts.session;
|
|
1323
|
+
if (opts.container) body.container = opts.container;
|
|
1324
|
+
if (opts.expiresInS) body.expires_in_s = opts.expiresInS;
|
|
1325
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/previews`, {
|
|
1326
|
+
body,
|
|
1327
|
+
approval: opts.approval,
|
|
1328
|
+
signal: opts.signal
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
/** POST /kosa8/v1/previews/rotate: end every preview link at once. */
|
|
1332
|
+
revokePreviews(opts = {}) {
|
|
1333
|
+
return this.transport.json("POST", "/kosa8/v1/previews/rotate", opts);
|
|
1334
|
+
}
|
|
1335
|
+
// --- toolbox: lifecycle ----------------------------------------------------------
|
|
1336
|
+
/**
|
|
1337
|
+
* POST …/lifecycle: change auto-stop, auto-delete, TTL or ephemeral.
|
|
1338
|
+
* Options left out keep their value; 0 (or `false`) turns one off.
|
|
1339
|
+
*/
|
|
1340
|
+
setLifecycle(id, opts = {}) {
|
|
1341
|
+
const body = {};
|
|
1342
|
+
if (opts.autoStopMin !== void 0) body.auto_stop_min = opts.autoStopMin;
|
|
1343
|
+
if (opts.autoDeleteMin !== void 0) body.auto_delete_min = opts.autoDeleteMin;
|
|
1344
|
+
if (opts.ttlMin !== void 0) body.ttl_min = opts.ttlMin;
|
|
1345
|
+
if (opts.ephemeral !== void 0) body.ephemeral = opts.ephemeral;
|
|
1346
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/lifecycle`, {
|
|
1347
|
+
body,
|
|
1348
|
+
approval: opts.approval,
|
|
1349
|
+
signal: opts.signal
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
// --- toolbox: templates ----------------------------------------------------------
|
|
1353
|
+
/** GET /kosa8/v1/templates */
|
|
1354
|
+
async listTemplates(opts = {}) {
|
|
1355
|
+
return await this.transport.json("GET", "/kosa8/v1/templates", opts) ?? [];
|
|
1356
|
+
}
|
|
1357
|
+
/** DELETE /kosa8/v1/templates/{name}. Sandboxes made from it keep their copy. */
|
|
1358
|
+
deleteTemplate(name, opts = {}) {
|
|
1359
|
+
return this.transport.json("DELETE", `/kosa8/v1/templates/${enc(name)}`, opts);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* POST /kosa8/v1/templates: build `contextDir`'s Dockerfile in a throwaway
|
|
1363
|
+
* sandbox and keep the image as a template, for `createSandbox({ template })`.
|
|
1364
|
+
* The context is packed honouring `.dockerignore`. Resolves with the final
|
|
1365
|
+
* frame (`{ done, ok, image }`); a failed build throws Kosa8StreamError.
|
|
1366
|
+
*/
|
|
1367
|
+
async buildTemplate(name, contextDir = ".", opts = {}) {
|
|
1368
|
+
const dockerfile = opts.dockerfile ?? "Dockerfile";
|
|
1369
|
+
const q = new URLSearchParams();
|
|
1370
|
+
q.append("name", name);
|
|
1371
|
+
q.append("dockerfile", dockerfile);
|
|
1372
|
+
for (const [k, v] of Object.entries(opts.buildArgs ?? {})) q.append("buildarg", `${k}=${v}`);
|
|
1373
|
+
const egress = egressString(opts.egress);
|
|
1374
|
+
if (egress) q.append("egress", egress);
|
|
1375
|
+
if (opts.replace) q.append("replace", "1");
|
|
1376
|
+
const ctx = await tarContext(contextDir, dockerfile);
|
|
1377
|
+
let out = "";
|
|
1378
|
+
for await (const f of this.transport.frames("POST", `/kosa8/v1/templates?${q.toString()}`, {
|
|
1379
|
+
rawBody: ctx,
|
|
1380
|
+
approval: opts.approval,
|
|
1381
|
+
signal: opts.signal
|
|
1382
|
+
})) {
|
|
1383
|
+
if (f.done) {
|
|
1384
|
+
if (f.error) throw new Kosa8StreamError(f.error, { stdout: out, stderr: "" }, { daemonMessage: f.error });
|
|
1385
|
+
return f;
|
|
1386
|
+
}
|
|
1387
|
+
if (f.data) {
|
|
1388
|
+
out += f.data.toString("utf8");
|
|
1389
|
+
opts.onOutput?.(f.data, f.stream ?? "stdout");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
throw new Kosa8StreamError("template build stream ended without a final frame", { stdout: out, stderr: "" });
|
|
1393
|
+
}
|
|
923
1394
|
// --- helpers -------------------------------------------------------------------
|
|
924
1395
|
/** A handle bound to one sandbox. */
|
|
925
1396
|
sandbox(id) {
|
|
@@ -1019,13 +1490,275 @@ var SandboxHandle = class {
|
|
|
1019
1490
|
rewind(opts) {
|
|
1020
1491
|
return this.client.rewind(this.id, opts);
|
|
1021
1492
|
}
|
|
1493
|
+
runCode(code, opts) {
|
|
1494
|
+
return this.client.runCode(this.id, code, opts);
|
|
1495
|
+
}
|
|
1496
|
+
listFiles(path4, opts) {
|
|
1497
|
+
return this.client.listFiles(this.id, path4, opts);
|
|
1498
|
+
}
|
|
1499
|
+
statFile(path4, opts) {
|
|
1500
|
+
return this.client.statFile(this.id, path4, opts);
|
|
1501
|
+
}
|
|
1502
|
+
readFile(path4, opts) {
|
|
1503
|
+
return this.client.readFile(this.id, path4, opts);
|
|
1504
|
+
}
|
|
1505
|
+
readText(path4, opts) {
|
|
1506
|
+
return this.client.readText(this.id, path4, opts);
|
|
1507
|
+
}
|
|
1508
|
+
writeFile(path4, data, opts) {
|
|
1509
|
+
return this.client.writeFile(this.id, path4, data, opts);
|
|
1510
|
+
}
|
|
1511
|
+
deleteFile(path4, opts) {
|
|
1512
|
+
return this.client.deleteFile(this.id, path4, opts);
|
|
1513
|
+
}
|
|
1514
|
+
makeDir(path4, opts) {
|
|
1515
|
+
return this.client.makeDir(this.id, path4, opts);
|
|
1516
|
+
}
|
|
1517
|
+
moveFile(from, to, opts) {
|
|
1518
|
+
return this.client.moveFile(this.id, from, to, opts);
|
|
1519
|
+
}
|
|
1520
|
+
git(op, opts) {
|
|
1521
|
+
return this.client.git(this.id, op, opts);
|
|
1522
|
+
}
|
|
1523
|
+
gitClone(url, opts) {
|
|
1524
|
+
return this.client.gitClone(this.id, url, opts);
|
|
1525
|
+
}
|
|
1526
|
+
gitStatus(path4, opts) {
|
|
1527
|
+
return this.client.gitStatus(this.id, path4, opts);
|
|
1528
|
+
}
|
|
1529
|
+
gitCommit(path4, message, opts) {
|
|
1530
|
+
return this.client.gitCommit(this.id, path4, message, opts);
|
|
1531
|
+
}
|
|
1532
|
+
createSession(name, opts) {
|
|
1533
|
+
return this.client.createSession(this.id, name, opts);
|
|
1534
|
+
}
|
|
1535
|
+
sessions(opts) {
|
|
1536
|
+
return this.client.listSessions(this.id, opts);
|
|
1537
|
+
}
|
|
1538
|
+
sessionExec(session, command, opts) {
|
|
1539
|
+
return this.client.sessionExec(this.id, session, command, opts);
|
|
1540
|
+
}
|
|
1541
|
+
shell(command, opts) {
|
|
1542
|
+
return this.client.shell(this.id, command, opts);
|
|
1543
|
+
}
|
|
1544
|
+
deleteSession(session, opts) {
|
|
1545
|
+
return this.client.deleteSession(this.id, session, opts);
|
|
1546
|
+
}
|
|
1547
|
+
startPty(opts) {
|
|
1548
|
+
return this.client.startPty(this.id, opts);
|
|
1549
|
+
}
|
|
1550
|
+
resizePty(container, cols, rows, opts) {
|
|
1551
|
+
return this.client.resizePty(this.id, container, cols, rows, opts);
|
|
1552
|
+
}
|
|
1553
|
+
createPreview(port, opts) {
|
|
1554
|
+
return this.client.createPreview(this.id, port, opts);
|
|
1555
|
+
}
|
|
1556
|
+
setLifecycle(opts) {
|
|
1557
|
+
return this.client.setLifecycle(this.id, opts);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/cloud.ts
|
|
1562
|
+
var DEFAULT_CLOUD_URL = "https://api.kosa8.cloud";
|
|
1563
|
+
var CLOUD_KEY_PREFIX = "k8c_live_";
|
|
1564
|
+
function trimSlash(s) {
|
|
1565
|
+
return s.replace(/\/+$/, "");
|
|
1566
|
+
}
|
|
1567
|
+
function checkBase(url) {
|
|
1568
|
+
let u;
|
|
1569
|
+
try {
|
|
1570
|
+
u = new URL(url);
|
|
1571
|
+
} catch {
|
|
1572
|
+
throw new Kosa8Error(`kosa8 Cloud address is not a URL: ${url}`);
|
|
1573
|
+
}
|
|
1574
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
1575
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
1576
|
+
throw new Kosa8Error(`kosa8 Cloud address must use https: ${url}`);
|
|
1577
|
+
}
|
|
1578
|
+
return trimSlash(u.toString());
|
|
1579
|
+
}
|
|
1580
|
+
var Kosa8Cloud = class {
|
|
1581
|
+
baseUrl;
|
|
1582
|
+
sandboxes;
|
|
1583
|
+
functions;
|
|
1584
|
+
apiKey;
|
|
1585
|
+
fetchImpl;
|
|
1586
|
+
timeoutMs;
|
|
1587
|
+
constructor(options = {}) {
|
|
1588
|
+
const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
1589
|
+
const key = (options.apiKey ?? env.KOSA8_API_KEY ?? "").trim();
|
|
1590
|
+
if (!key) throw new Kosa8Error("no kosa8 Cloud API key: pass apiKey or set KOSA8_API_KEY");
|
|
1591
|
+
if (!key.startsWith(CLOUD_KEY_PREFIX)) throw new Kosa8Error(`a kosa8 Cloud API key starts with ${CLOUD_KEY_PREFIX}`);
|
|
1592
|
+
this.apiKey = key;
|
|
1593
|
+
this.baseUrl = checkBase(options.baseUrl ?? env.KOSA8_CLOUD_URL ?? DEFAULT_CLOUD_URL);
|
|
1594
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
1595
|
+
if (typeof f !== "function") throw new Kosa8Error("no fetch available; pass options.fetch");
|
|
1596
|
+
this.fetchImpl = f;
|
|
1597
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
1598
|
+
this.sandboxes = new CloudSandboxes(this);
|
|
1599
|
+
this.functions = new CloudFunctions(this);
|
|
1600
|
+
}
|
|
1601
|
+
/** The org, the key in use and, for a member's key, the member. */
|
|
1602
|
+
me() {
|
|
1603
|
+
return this.request("GET", "/v1/me");
|
|
1604
|
+
}
|
|
1605
|
+
/** This month's usage, bill estimate and whether work is paused for payment. */
|
|
1606
|
+
usage() {
|
|
1607
|
+
return this.request("GET", "/v1/usage");
|
|
1608
|
+
}
|
|
1609
|
+
/** @internal */
|
|
1610
|
+
async request(method, path4, body) {
|
|
1611
|
+
const ctl = new AbortController();
|
|
1612
|
+
const timer = setTimeout(() => ctl.abort(), this.timeoutMs);
|
|
1613
|
+
let resp;
|
|
1614
|
+
try {
|
|
1615
|
+
resp = await this.fetchImpl(`${this.baseUrl}${path4}`, {
|
|
1616
|
+
method,
|
|
1617
|
+
headers: {
|
|
1618
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1619
|
+
Accept: "application/json",
|
|
1620
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1621
|
+
},
|
|
1622
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
1623
|
+
signal: ctl.signal
|
|
1624
|
+
});
|
|
1625
|
+
} catch (e) {
|
|
1626
|
+
if (ctl.signal.aborted) {
|
|
1627
|
+
throw new Kosa8TimeoutError(`kosa8 Cloud did not answer within ${this.timeoutMs} ms`, { method, path: path4, cause: e });
|
|
1628
|
+
}
|
|
1629
|
+
throw new Kosa8ConnectionError(`could not reach kosa8 Cloud at ${this.baseUrl}: ${String(e?.message ?? e)}`, { method, path: path4, cause: e });
|
|
1630
|
+
} finally {
|
|
1631
|
+
clearTimeout(timer);
|
|
1632
|
+
}
|
|
1633
|
+
const text = await resp.text();
|
|
1634
|
+
let data = text;
|
|
1635
|
+
try {
|
|
1636
|
+
data = text === "" ? null : JSON.parse(text);
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
if (!resp.ok) throw cloudError(resp.status, data, { method, path: path4 });
|
|
1640
|
+
return data;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
function cloudError(status, body, ctx) {
|
|
1644
|
+
let code;
|
|
1645
|
+
let msgBody = body;
|
|
1646
|
+
if (typeof body === "object" && body !== null) {
|
|
1647
|
+
const b = body;
|
|
1648
|
+
if (typeof b.error === "string") code = b.error;
|
|
1649
|
+
msgBody = { error: typeof b.message === "string" && b.message ? b.message : b.error };
|
|
1650
|
+
}
|
|
1651
|
+
return errorFromResponse(status, msgBody, { ...ctx, code });
|
|
1652
|
+
}
|
|
1653
|
+
var CloudSandboxes = class {
|
|
1654
|
+
constructor(cloud) {
|
|
1655
|
+
this.cloud = cloud;
|
|
1656
|
+
}
|
|
1657
|
+
/** Start a sandbox; resolves once it is running. */
|
|
1658
|
+
async create(opts = {}) {
|
|
1659
|
+
const body = {};
|
|
1660
|
+
if (opts.template !== void 0) body.template = opts.template;
|
|
1661
|
+
if (opts.region !== void 0) body.region = opts.region;
|
|
1662
|
+
if (opts.sleepAfterS !== void 0) body.sleep_after_s = opts.sleepAfterS;
|
|
1663
|
+
const r = await this.cloud.request("POST", "/v1/sandboxes", body);
|
|
1664
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1665
|
+
}
|
|
1666
|
+
/** The org's sandboxes; with all, deleted ones too. */
|
|
1667
|
+
async list(opts = {}) {
|
|
1668
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes${opts.all ? "?all=1" : ""}`);
|
|
1669
|
+
return r.sandboxes.map((s) => new CloudSandbox(this.cloud, s));
|
|
1670
|
+
}
|
|
1671
|
+
async get(id) {
|
|
1672
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
1673
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var CloudSandbox = class {
|
|
1677
|
+
constructor(cloud, info) {
|
|
1678
|
+
this.cloud = cloud;
|
|
1679
|
+
this.info = info;
|
|
1680
|
+
}
|
|
1681
|
+
get id() {
|
|
1682
|
+
return this.info.id;
|
|
1683
|
+
}
|
|
1684
|
+
path(suffix = "") {
|
|
1685
|
+
return `/v1/sandboxes/${encodeURIComponent(this.info.id)}${suffix}`;
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Run a command and wait for it. A string runs through `sh -c`. A
|
|
1689
|
+
* sleeping sandbox wakes for it.
|
|
1690
|
+
*/
|
|
1691
|
+
async exec(cmd, opts = {}) {
|
|
1692
|
+
const argv = typeof cmd === "string" ? ["sh", "-c", cmd] : cmd;
|
|
1693
|
+
const body = { cmd: argv };
|
|
1694
|
+
if (opts.image !== void 0) body.image = opts.image;
|
|
1695
|
+
if (opts.env) body.env = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
|
|
1696
|
+
const r = await this.cloud.request(
|
|
1697
|
+
"POST",
|
|
1698
|
+
this.path("/exec"),
|
|
1699
|
+
body
|
|
1700
|
+
);
|
|
1701
|
+
const out = { exitCode: r.exit, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1702
|
+
if (typeof r.woke_ms === "number") out.wokeMs = r.woke_ms;
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
1705
|
+
/** Snapshot it and free its VM; it then costs only snapshot storage. */
|
|
1706
|
+
async sleep() {
|
|
1707
|
+
this.info = (await this.cloud.request("POST", this.path("/sleep"))).sandbox;
|
|
1708
|
+
return this;
|
|
1709
|
+
}
|
|
1710
|
+
/** Restore a sleeping sandbox, files and all. */
|
|
1711
|
+
async wake() {
|
|
1712
|
+
this.info = (await this.cloud.request("POST", this.path("/wake"))).sandbox;
|
|
1713
|
+
return this;
|
|
1714
|
+
}
|
|
1715
|
+
async refresh() {
|
|
1716
|
+
this.info = (await this.cloud.request("GET", this.path())).sandbox;
|
|
1717
|
+
return this;
|
|
1718
|
+
}
|
|
1719
|
+
async delete() {
|
|
1720
|
+
await this.cloud.request("DELETE", this.path());
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
var CloudFunctions = class {
|
|
1724
|
+
constructor(cloud) {
|
|
1725
|
+
this.cloud = cloud;
|
|
1726
|
+
}
|
|
1727
|
+
/** Deploy (create or replace) a function; every deploy bumps its version. */
|
|
1728
|
+
deploy(name, spec) {
|
|
1729
|
+
return this.cloud.request("PUT", `/v1/functions/${encodeURIComponent(name)}`, spec);
|
|
1730
|
+
}
|
|
1731
|
+
async list() {
|
|
1732
|
+
return (await this.cloud.request("GET", "/v1/functions")).functions;
|
|
1733
|
+
}
|
|
1734
|
+
get(name) {
|
|
1735
|
+
return this.cloud.request("GET", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1736
|
+
}
|
|
1737
|
+
async delete(name) {
|
|
1738
|
+
await this.cloud.request("DELETE", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1739
|
+
}
|
|
1740
|
+
/** Call it once. A handler that raises resolves with ok: false, not a throw. */
|
|
1741
|
+
invoke(name, event = null) {
|
|
1742
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/invoke`, { event });
|
|
1743
|
+
}
|
|
1744
|
+
/** One call per event, results in input order. */
|
|
1745
|
+
map(name, events, opts = {}) {
|
|
1746
|
+
const body = { events };
|
|
1747
|
+
if (opts.concurrency !== void 0) body.concurrency = opts.concurrency;
|
|
1748
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/map`, body);
|
|
1749
|
+
}
|
|
1022
1750
|
};
|
|
1023
1751
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1024
1752
|
0 && (module.exports = {
|
|
1025
1753
|
ACTOR_HEADER,
|
|
1026
1754
|
AGENT_TOKEN_HEADER,
|
|
1027
1755
|
APPROVAL_HEADER,
|
|
1756
|
+
CLOUD_KEY_PREFIX,
|
|
1028
1757
|
CheckpointBook,
|
|
1758
|
+
CloudFunctions,
|
|
1759
|
+
CloudSandbox,
|
|
1760
|
+
CloudSandboxes,
|
|
1761
|
+
DEFAULT_CLOUD_URL,
|
|
1029
1762
|
FileCheckpointStore,
|
|
1030
1763
|
Kosa8ApiError,
|
|
1031
1764
|
Kosa8ApprovalRequiredError,
|
|
@@ -1033,20 +1766,26 @@ var SandboxHandle = class {
|
|
|
1033
1766
|
Kosa8BadRequestError,
|
|
1034
1767
|
Kosa8CheckpointError,
|
|
1035
1768
|
Kosa8Client,
|
|
1769
|
+
Kosa8Cloud,
|
|
1036
1770
|
Kosa8ConflictError,
|
|
1037
1771
|
Kosa8ConnectionError,
|
|
1038
1772
|
Kosa8Error,
|
|
1039
1773
|
Kosa8NotFoundError,
|
|
1774
|
+
Kosa8PaymentRequiredError,
|
|
1040
1775
|
Kosa8PolicyError,
|
|
1776
|
+
Kosa8RateLimitError,
|
|
1041
1777
|
Kosa8StreamError,
|
|
1778
|
+
Kosa8TimeoutError,
|
|
1042
1779
|
Kosa8UnsupportedError,
|
|
1043
1780
|
MAX_CHECKPOINTS_PER_SANDBOX,
|
|
1044
1781
|
MemoryCheckpointStore,
|
|
1045
1782
|
SandboxHandle,
|
|
1046
1783
|
Transport,
|
|
1047
1784
|
defaultSocketPath,
|
|
1785
|
+
defaultWorkerFile,
|
|
1048
1786
|
errorFromResponse,
|
|
1049
1787
|
resolveEndpoint,
|
|
1788
|
+
tarContext,
|
|
1050
1789
|
validateSandboxName
|
|
1051
1790
|
});
|
|
1052
1791
|
//# sourceMappingURL=index.cjs.map
|