@veris-ai/daytona 0.2.1 → 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 +254 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2099 -0
- package/dist/cli.js.map +1 -0
- package/dist/control-plane.d.ts +13 -5
- package/dist/daytona-key.d.ts +28 -0
- package/dist/daytona.d.ts +86 -10
- package/dist/exec.d.ts +15 -0
- package/dist/gateway.d.ts +35 -1
- package/dist/index.cjs +579 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.js +554 -108
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +50 -38
- package/dist/profile.d.ts +70 -0
- package/dist/provision.d.ts +100 -0
- package/dist/push.d.ts +11 -0
- package/dist/receipt.d.ts +25 -3
- package/dist/run-receipt.d.ts +13 -0
- package/dist/run.d.ts +87 -0
- package/dist/service-control.d.ts +8 -0
- package/dist/teardown.d.ts +52 -0
- package/dist/trust.d.ts +100 -0
- package/dist/veris-api.d.ts +58 -0
- package/package.json +7 -1
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
BUNDLED_CA_FILES: () => BUNDLED_CA_FILES,
|
|
25
|
+
BUNDLED_CA_PATCH_SCRIPT: () => BUNDLED_CA_PATCH_SCRIPT,
|
|
24
26
|
CA_CERT_PATH: () => CA_CERT_PATH,
|
|
25
27
|
ControlPlane: () => ControlPlane,
|
|
26
|
-
|
|
28
|
+
DAYTONA_API_URL: () => DAYTONA_API_URL,
|
|
29
|
+
DEFAULT_API_BASE: () => DEFAULT_API_BASE,
|
|
30
|
+
DELETE_SANDBOXES: () => DELETE_SANDBOXES,
|
|
27
31
|
Daytona: () => Daytona,
|
|
28
32
|
MissingCredentialsError: () => MissingCredentialsError,
|
|
33
|
+
NODE_TRUST_FLAG: () => NODE_TRUST_FLAG,
|
|
29
34
|
ReceiptIntegrityError: () => ReceiptIntegrityError,
|
|
30
35
|
SDK_VERSION: () => SDK_VERSION,
|
|
31
36
|
SYSTEM_BUNDLE: () => SYSTEM_BUNDLE,
|
|
@@ -39,14 +44,33 @@ __export(index_exports, {
|
|
|
39
44
|
VerisGatewayUnreachableError: () => VerisGatewayUnreachableError,
|
|
40
45
|
VerisUntouchedError: () => VerisUntouchedError,
|
|
41
46
|
assertHttpControlPlane: () => assertHttpControlPlane,
|
|
47
|
+
buildNetwork: () => buildNetwork,
|
|
48
|
+
canDeleteSandboxes: () => canDeleteSandboxes,
|
|
49
|
+
cannotTeardownWarning: () => cannotTeardownWarning,
|
|
42
50
|
dataPlaneHosts: () => dataPlaneHosts,
|
|
51
|
+
daytonaApiUrl: () => daytonaApiUrl,
|
|
43
52
|
default: () => daytona_default,
|
|
53
|
+
directTwinHosts: () => directTwinHosts,
|
|
54
|
+
fetchDaytonaKey: () => fetchDaytonaKey,
|
|
44
55
|
fetchManual: () => fetchManual,
|
|
56
|
+
gatewayIps: () => gatewayIps,
|
|
45
57
|
gatewayProxyUrl: () => gatewayProxyUrl,
|
|
58
|
+
isPermissionDenied: () => isPermissionDenied,
|
|
46
59
|
isVerisSandbox: () => isVerisSandbox,
|
|
60
|
+
missingKeyMessage: () => missingKeyMessage,
|
|
61
|
+
nodeOptionsWithTrust: () => nodeOptionsWithTrust,
|
|
62
|
+
patchBundledCas: () => patchBundledCas,
|
|
63
|
+
profilePath: () => profilePath,
|
|
64
|
+
requireVerisCredentials: () => requireVerisCredentials,
|
|
65
|
+
resolveVerisCredentials: () => resolveVerisCredentials,
|
|
66
|
+
sandboxBrakes: () => sandboxBrakes,
|
|
67
|
+
teardownRefusedMessage: () => teardownRefusedMessage,
|
|
68
|
+
trustPrelude: () => trustPrelude,
|
|
47
69
|
twinHosts: () => twinHosts,
|
|
48
70
|
vendorHosts: () => vendorHosts,
|
|
49
|
-
vendoredTrustEnv: () => vendoredTrustEnv
|
|
71
|
+
vendoredTrustEnv: () => vendoredTrustEnv,
|
|
72
|
+
verisOwnsTwin: () => verisOwnsTwin,
|
|
73
|
+
verisTwinId: () => verisTwinId
|
|
50
74
|
});
|
|
51
75
|
module.exports = __toCommonJS(index_exports);
|
|
52
76
|
__reExport(index_exports, require("@daytona/sdk"), module.exports);
|
|
@@ -224,14 +248,19 @@ var ControlPlane = class {
|
|
|
224
248
|
}
|
|
225
249
|
if (!res.ok) await this.json(res, `update sandbox ${sandboxId}`);
|
|
226
250
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
// There was an extendTtl() here, and it did nothing.
|
|
252
|
+
//
|
|
253
|
+
// It PATCHed { ttl_minutes } at the sandbox resource, and the server's
|
|
254
|
+
// UpdateSandboxRequest declares only client_base_url. Pydantic ignores
|
|
255
|
+
// unknown fields, so the route answered 200 having changed no TTL at all —
|
|
256
|
+
// a call that reports success while doing nothing, which is worse than no
|
|
257
|
+
// call, because the caller then believes the twin will outlive the run.
|
|
258
|
+
// Nothing here called it; it was API surface promising a thing that could
|
|
259
|
+
// not happen.
|
|
260
|
+
//
|
|
261
|
+
// Bring it back when a route exists to carry it. Until then a twin's TTL is
|
|
262
|
+
// decided once, by ttl_minutes at create — where this SDK already keeps it
|
|
263
|
+
// in step with the Daytona sandbox's own ttlMinutes.
|
|
235
264
|
/** Create-time preflight: is the gateway infrastructure up, per the control plane? */
|
|
236
265
|
async gatewayHealth() {
|
|
237
266
|
const res = await this.request("GET", "/v1/gateway/health");
|
|
@@ -245,40 +274,162 @@ var ControlPlane = class {
|
|
|
245
274
|
}
|
|
246
275
|
};
|
|
247
276
|
|
|
277
|
+
// src/run-receipt.ts
|
|
278
|
+
var import_node_crypto = require("crypto");
|
|
279
|
+
|
|
248
280
|
// src/receipt.ts
|
|
281
|
+
var PAGE_LIMIT = 1e3;
|
|
282
|
+
var MAX_PAGES = 20;
|
|
283
|
+
function rowsOf(body) {
|
|
284
|
+
if (!body || !Array.isArray(body.requests)) {
|
|
285
|
+
throw new VerisError("invalid request log: expected requests array", { phase: "receipt" });
|
|
286
|
+
}
|
|
287
|
+
const rows = body.requests;
|
|
288
|
+
for (const r of rows) {
|
|
289
|
+
if (!r || !Number.isSafeInteger(r.id) || r.id <= 0 || typeof r.method !== "string" || typeof r.path !== "string" || typeof r.tier !== "string" || !(r.status === null || typeof r.status === "number")) {
|
|
290
|
+
throw new VerisError("invalid request log row: stable id, method, path, tier and status required", { phase: "receipt" });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return rows;
|
|
294
|
+
}
|
|
249
295
|
function parseRequestsBody(body) {
|
|
250
|
-
const rows =
|
|
251
|
-
const entries = rows.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
path: String(row.path ?? ""),
|
|
256
|
-
status: typeof row.status === "number" ? row.status : null
|
|
257
|
-
};
|
|
258
|
-
});
|
|
259
|
-
return { count: entries.length, entries };
|
|
296
|
+
const rows = rowsOf(body);
|
|
297
|
+
const entries = rows.filter(
|
|
298
|
+
(r) => ["handler", "fault", "fallback", "fallback-llm", "fallback-replay"].includes(r.tier) && !/^\/veris(?:\/|$)/.test(r.path)
|
|
299
|
+
).map(({ id, method, path, status, tier }) => ({ id, method, path, status, tier }));
|
|
300
|
+
return { count: entries.length, entries, total: rows.length };
|
|
260
301
|
}
|
|
261
|
-
async function
|
|
262
|
-
const
|
|
263
|
-
|
|
302
|
+
async function readPage(svc, query) {
|
|
303
|
+
const res = await fetch(`${svc.control_url.replace(/\/$/, "")}/veris/requests?${query}`, {
|
|
304
|
+
signal: AbortSignal.timeout(3e4),
|
|
305
|
+
redirect: "error"
|
|
306
|
+
});
|
|
264
307
|
const text = await res.text();
|
|
265
|
-
if (!res.ok) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
if (!res.ok) throw new VerisError(`could not read receipt for service '${svc.name}' (${res.status})`, { phase: "receipt" });
|
|
309
|
+
let body;
|
|
310
|
+
try {
|
|
311
|
+
body = JSON.parse(text);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new VerisError(`service '${svc.name}' returned a non-JSON receipt body`, { phase: "receipt" });
|
|
270
314
|
}
|
|
271
|
-
|
|
315
|
+
return rowsOf(body);
|
|
316
|
+
}
|
|
317
|
+
async function fetchWatermark(svc) {
|
|
318
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1", order: "desc" }));
|
|
319
|
+
if (rows.length > 1) throw new VerisError("request log ignored watermark limit", { phase: "receipt" });
|
|
320
|
+
return rows[0]?.id ?? 0;
|
|
321
|
+
}
|
|
322
|
+
async function fetchReceiptEntry(svc, sinceId = 0) {
|
|
323
|
+
if (!Number.isSafeInteger(sinceId) || sinceId < 0) throw new VerisError("invalid receipt watermark", { phase: "receipt" });
|
|
324
|
+
const end = await fetchWatermark(svc);
|
|
325
|
+
if (end < sinceId) throw new VerisError("receipt baseline invalid: log moved backwards; take a new baseline", { phase: "receipt" });
|
|
326
|
+
const entries = [];
|
|
327
|
+
const raw = [];
|
|
328
|
+
let mark = sinceId;
|
|
329
|
+
let incompleteReason;
|
|
330
|
+
for (let page = 0; mark < end; page++) {
|
|
331
|
+
if (page >= MAX_PAGES) {
|
|
332
|
+
incompleteReason = "page-limit";
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
let rows;
|
|
336
|
+
try {
|
|
337
|
+
rows = await readPage(svc, new URLSearchParams({ limit: String(PAGE_LIMIT), order: "asc", since_id: String(mark) }));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (!raw.length) throw error;
|
|
340
|
+
incompleteReason = "read-failed";
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
let previous = mark;
|
|
344
|
+
if (!rows.length || rows.some((r) => {
|
|
345
|
+
const bad = r.id <= previous;
|
|
346
|
+
previous = r.id;
|
|
347
|
+
return bad;
|
|
348
|
+
})) {
|
|
349
|
+
incompleteReason = "pagination-not-progressing";
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
const window = rows.filter((r) => r.id <= end);
|
|
353
|
+
raw.push(...window);
|
|
354
|
+
entries.push(...parseRequestsBody({ requests: window }).entries);
|
|
355
|
+
mark = rows[rows.length - 1].id;
|
|
356
|
+
}
|
|
357
|
+
entries.sort((a, b) => b.id - a.id);
|
|
358
|
+
return {
|
|
359
|
+
requests: entries.length,
|
|
360
|
+
controlUrl: svc.control_url,
|
|
361
|
+
entries,
|
|
362
|
+
sinceId,
|
|
363
|
+
untilId: end,
|
|
364
|
+
capped: Boolean(incompleteReason),
|
|
365
|
+
...incompleteReason ? { incompleteReason } : {},
|
|
366
|
+
raw: { requests: raw }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/run-receipt.ts
|
|
371
|
+
var HEADER = "x-veris-receipt-baseline";
|
|
372
|
+
function markerOf(row) {
|
|
272
373
|
try {
|
|
273
|
-
|
|
374
|
+
const headers = typeof row.request_headers === "string" ? JSON.parse(row.request_headers) : row.request_headers;
|
|
375
|
+
return headers?.[HEADER];
|
|
274
376
|
} catch {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function captureBaseline(twinId, sandboxId, services) {
|
|
381
|
+
const marks = await Promise.all(services.map(async (svc) => {
|
|
382
|
+
const marker = (0, import_node_crypto.randomUUID)();
|
|
383
|
+
const res = await fetch(`${svc.control_url.replace(/\/$/, "")}/veris/schema`, {
|
|
384
|
+
headers: { [HEADER]: marker },
|
|
385
|
+
signal: AbortSignal.timeout(3e4),
|
|
386
|
+
redirect: "error"
|
|
278
387
|
});
|
|
388
|
+
await res.text();
|
|
389
|
+
if (!res.ok) throw new VerisError(`could not establish baseline for '${svc.name}' (${res.status})`, { phase: "receipt" });
|
|
390
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
391
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1000", order: "desc" }));
|
|
392
|
+
const anchor = rows.find((r) => markerOf(r) === marker && r.tier === "control" && r.path === "/veris/schema");
|
|
393
|
+
if (anchor) return [svc.name, { controlUrl: svc.control_url, id: anchor.id, marker }];
|
|
394
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
395
|
+
}
|
|
396
|
+
throw new VerisError(`baseline unavailable for '${svc.name}': trace must retain control request headers`, { phase: "receipt" });
|
|
397
|
+
}));
|
|
398
|
+
return { version: 1, twinId, sandboxId, services: Object.fromEntries(marks) };
|
|
399
|
+
}
|
|
400
|
+
async function validateBaseline(baseline, twinId, sandboxId, services) {
|
|
401
|
+
const invalid = () => new VerisError("receipt baseline invalid: session, services or history changed; take a new baseline before execution", { phase: "receipt" });
|
|
402
|
+
if (baseline.version !== 1 || baseline.twinId !== twinId || baseline.sandboxId !== sandboxId || Object.keys(baseline.services).sort().join("\0") !== services.map((s) => s.name).sort().join("\0")) throw invalid();
|
|
403
|
+
await Promise.all(services.map(async (svc) => {
|
|
404
|
+
const mark = baseline.services[svc.name];
|
|
405
|
+
if (!mark || mark.controlUrl !== svc.control_url || !Number.isSafeInteger(mark.id) || mark.id <= 0) throw invalid();
|
|
406
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1", order: "asc", since_id: String(mark.id - 1) }));
|
|
407
|
+
if (rows.length !== 1 || rows[0].id !== mark.id || markerOf(rows[0]) !== mark.marker) throw invalid();
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/service-control.ts
|
|
412
|
+
async function serviceControl(svc, resource, options = {}) {
|
|
413
|
+
const method = options.method ?? "GET";
|
|
414
|
+
if (!["manual", "schema", "operations", "data", "requests"].includes(resource) || !["GET", "POST", "PATCH"].includes(method) || method !== "GET" && resource !== "data" || method === "GET" && options.body !== void 0 || !/^https?:\/\//.test(svc.control_url)) {
|
|
415
|
+
throw new VerisError("unsupported service control operation");
|
|
416
|
+
}
|
|
417
|
+
const url = new URL(`${svc.control_url.replace(/\/$/, "")}/veris/${resource}`);
|
|
418
|
+
for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
|
|
419
|
+
const res = await fetch(url, {
|
|
420
|
+
method,
|
|
421
|
+
redirect: "error",
|
|
422
|
+
signal: AbortSignal.timeout(3e4),
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
425
|
+
});
|
|
426
|
+
const text = await res.text();
|
|
427
|
+
if (!res.ok) throw new VerisError(`service '${svc.name}' ${method} /veris/${resource} failed (${res.status})`);
|
|
428
|
+
try {
|
|
429
|
+
return JSON.parse(text);
|
|
430
|
+
} catch {
|
|
431
|
+
throw new VerisError(`service '${svc.name}' returned invalid control JSON`);
|
|
279
432
|
}
|
|
280
|
-
const { count, entries } = parseRequestsBody(raw);
|
|
281
|
-
return { requests: count, controlUrl: svc.control_url, entries, raw };
|
|
282
433
|
}
|
|
283
434
|
|
|
284
435
|
// src/network.ts
|
|
@@ -303,6 +454,9 @@ function twinHosts(services) {
|
|
|
303
454
|
}
|
|
304
455
|
return [...hosts].sort();
|
|
305
456
|
}
|
|
457
|
+
function directTwinHosts(services) {
|
|
458
|
+
return twinHosts(services.filter((s) => isHttpUrl(s.url) && !(s.routes ?? []).length));
|
|
459
|
+
}
|
|
306
460
|
function dataPlaneHosts(services) {
|
|
307
461
|
const hosts = /* @__PURE__ */ new Set();
|
|
308
462
|
for (const svc of services) {
|
|
@@ -352,51 +506,30 @@ var PROCESS_CONTROLLING = /* @__PURE__ */ new Set([
|
|
|
352
506
|
function isSafeEnvName(name) {
|
|
353
507
|
return /^[A-Z][A-Z0-9_]{0,63}$/.test(name) && !PROCESS_CONTROLLING.has(name);
|
|
354
508
|
}
|
|
355
|
-
var
|
|
356
|
-
// JS
|
|
357
|
-
"registry.npmjs.org",
|
|
358
|
-
"registry.yarnpkg.com",
|
|
359
|
-
// Python
|
|
360
|
-
"pypi.org",
|
|
361
|
-
"files.pythonhosted.org",
|
|
362
|
-
// Go
|
|
363
|
-
"proxy.golang.org",
|
|
364
|
-
"sum.golang.org",
|
|
365
|
-
// Rust
|
|
366
|
-
"crates.io",
|
|
367
|
-
"static.crates.io",
|
|
368
|
-
"index.crates.io",
|
|
369
|
-
// Debian/Ubuntu
|
|
370
|
-
"deb.debian.org",
|
|
371
|
-
"security.debian.org",
|
|
372
|
-
"archive.ubuntu.com",
|
|
373
|
-
"security.ubuntu.com",
|
|
374
|
-
// Source + container hosts the above routinely redirect to
|
|
375
|
-
"github.com",
|
|
376
|
-
"codeload.github.com",
|
|
377
|
-
"objects.githubusercontent.com",
|
|
378
|
-
"raw.githubusercontent.com",
|
|
379
|
-
"ghcr.io"
|
|
380
|
-
];
|
|
509
|
+
var IPV4_RE = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
|
|
381
510
|
function buildNetwork(args) {
|
|
382
|
-
const {
|
|
383
|
-
if (mode === "open") return {};
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
511
|
+
const { mode, gatewayIps: gatewayIps2 } = args;
|
|
512
|
+
if (mode === "open") return { params: {} };
|
|
513
|
+
const ips = [...new Set(gatewayIps2)];
|
|
514
|
+
if (!ips.length) {
|
|
515
|
+
throw new VerisError(
|
|
516
|
+
"the control plane did not name the gateway's IP addresses (egress credential `gateway_ips`) and the gateway hostname could not be resolved, so the sandbox cannot be pinned to it. Upgrade the control plane, or run with veris.egress: 'open' (Daytona still blocks anything that bypasses the proxy).",
|
|
517
|
+
{ phase: "credential-mint" }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const bad = ips.filter((ip) => !IPV4_RE.test(ip));
|
|
521
|
+
if (bad.length) {
|
|
522
|
+
throw new VerisError(
|
|
523
|
+
`the control plane returned gateway addresses that are not IPv4: ${JSON.stringify(bad)} (Daytona's networkAllowList takes IPv4 CIDRs only)`,
|
|
524
|
+
{ phase: "credential-mint" }
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return { params: { networkAllowList: ips.map((ip) => `${ip}/32`).join(",") } };
|
|
398
528
|
}
|
|
399
529
|
|
|
530
|
+
// src/gateway.ts
|
|
531
|
+
var import_node_dns = require("dns");
|
|
532
|
+
|
|
400
533
|
// src/trust.ts
|
|
401
534
|
var CA_CERT_PATH = "/usr/local/share/ca-certificates/veris-ca.crt";
|
|
402
535
|
var SYSTEM_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
@@ -424,6 +557,51 @@ function vendoredTrustEnv() {
|
|
|
424
557
|
NODE_EXTRA_CA_CERTS: VERIS_BUNDLE
|
|
425
558
|
};
|
|
426
559
|
}
|
|
560
|
+
function trustPrelude(env = vendoredTrustEnv()) {
|
|
561
|
+
return Object.entries(env).map(([k, v]) => `export ${k}='${v.replace(/'/g, `'\\''`)}';`).join(" ");
|
|
562
|
+
}
|
|
563
|
+
var BUNDLED_CA_FILES = [
|
|
564
|
+
{ sdk: "pip (vendored certifi)", suffix: "pip/_vendor/certifi/cacert.pem" },
|
|
565
|
+
{ sdk: "certifi", suffix: "certifi/cacert.pem" },
|
|
566
|
+
{ sdk: "botocore", suffix: "botocore/cacert.pem" },
|
|
567
|
+
// Matches both stripe-python's site-packages layout and the stripe-ruby gem.
|
|
568
|
+
{ sdk: "stripe", suffix: "stripe/data/ca-certificates.crt" },
|
|
569
|
+
{ sdk: "httplib2", suffix: "httplib2/cacerts.txt" }
|
|
570
|
+
];
|
|
571
|
+
var BUNDLED_CA_PATCH_SCRIPT = "/tmp/veris-patch-bundled-cas.sh";
|
|
572
|
+
var BUNDLED_CA_PATCHED_MARKER = "the Veris CA was appended to ";
|
|
573
|
+
function bundledCaPatchScript() {
|
|
574
|
+
const find = BUNDLED_CA_FILES.map((f) => `find / -xdev \\( -path /proc -o -path /sys -o -path /dev \\) -prune -o -path '*/${f.suffix}' -type f -print 2>/dev/null`).join("; ");
|
|
575
|
+
return [
|
|
576
|
+
`marker=$(sed -n 2p ${VERIS_CA_FILE} 2>/dev/null)`,
|
|
577
|
+
// No CA on disk means nothing to append, and appending nothing to every
|
|
578
|
+
// bundle in the image would be the worst available no-op.
|
|
579
|
+
`[ -n "$marker" ] || { echo "no ${VERIS_CA_FILE} to append" >&2; exit 1; }`,
|
|
580
|
+
// The whole loop is one brace group at the end of the pipe so `n` survives
|
|
581
|
+
// to the summary: a pipeline's last stage is a subshell, and a counter
|
|
582
|
+
// incremented inside a bare `while` would be gone by the time we report it.
|
|
583
|
+
`{ ${find}; } | sort -u | { n=0`,
|
|
584
|
+
`while IFS= read -r f; do`,
|
|
585
|
+
` [ -w "$f" ] || continue`,
|
|
586
|
+
` grep -qF "$marker" "$f" && continue`,
|
|
587
|
+
` { printf '\\n'; cat ${VERIS_CA_FILE}; } >> "$f" || continue`,
|
|
588
|
+
` n=$((n+1))`,
|
|
589
|
+
` echo "${BUNDLED_CA_PATCHED_MARKER}$f"`,
|
|
590
|
+
`done`,
|
|
591
|
+
// Silence is the common case and it reads as "did that even run?", so say
|
|
592
|
+
// so. patchBundledCas() keeps only the lines above, so this costs it
|
|
593
|
+
// nothing.
|
|
594
|
+
`if [ "$n" -gt 0 ]; then echo "$n bundled CA file(s) patched"`,
|
|
595
|
+
`else echo "no bundled CA file needed patching \u2014 no SDK that ships its own is installed here, or they already trust the Veris CA"; fi`,
|
|
596
|
+
`}`
|
|
597
|
+
].join("\n");
|
|
598
|
+
}
|
|
599
|
+
var NODE_TRUST_FLAG = "--use-openssl-ca";
|
|
600
|
+
function nodeOptionsWithTrust(existing) {
|
|
601
|
+
const base = (existing ?? "").trim();
|
|
602
|
+
if (base.split(/\s+/).includes(NODE_TRUST_FLAG)) return base;
|
|
603
|
+
return base ? `${base} ${NODE_TRUST_FLAG}` : NODE_TRUST_FLAG;
|
|
604
|
+
}
|
|
427
605
|
var CA_INSTALL_CMD = [
|
|
428
606
|
"update-ca-certificates",
|
|
429
607
|
`(keytool -importcert -noprompt -cacerts -storepass changeit -alias veris -file ${CA_CERT_PATH} 2>/dev/null || true)`,
|
|
@@ -458,6 +636,16 @@ function gatewayProxyUrl(credential) {
|
|
|
458
636
|
}
|
|
459
637
|
return `http://${encodeURIComponent(credential.username)}:x@${credential.connect_address}`;
|
|
460
638
|
}
|
|
639
|
+
async function gatewayIps(credential, proxyUrl) {
|
|
640
|
+
if (credential.gateway_ips?.length) return credential.gateway_ips;
|
|
641
|
+
const host = new URL(proxyUrl).hostname;
|
|
642
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return [host];
|
|
643
|
+
try {
|
|
644
|
+
return await import_node_dns.promises.resolve4(host);
|
|
645
|
+
} catch {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
}
|
|
461
649
|
function assertProxyUrl(raw) {
|
|
462
650
|
let u;
|
|
463
651
|
try {
|
|
@@ -484,6 +672,7 @@ function assertProxyUrl(raw) {
|
|
|
484
672
|
}
|
|
485
673
|
async function installCa(sandbox, caPem) {
|
|
486
674
|
await sandbox.fs.uploadFile(Buffer.from(caPem, "utf8"), VERIS_CA_FILE);
|
|
675
|
+
await sandbox.fs.uploadFile(Buffer.from(bundledCaPatchScript(), "utf8"), BUNDLED_CA_PATCH_SCRIPT);
|
|
487
676
|
const script = [
|
|
488
677
|
`chmod 0644 ${VERIS_CA_FILE}`,
|
|
489
678
|
// Public roots first so they keep working; ours appended. `cat` of a
|
|
@@ -509,6 +698,10 @@ async function installCa(sandbox, caPem) {
|
|
|
509
698
|
);
|
|
510
699
|
}
|
|
511
700
|
}
|
|
701
|
+
async function patchBundledCas(sandbox) {
|
|
702
|
+
const r = await sh(sandbox, `sh ${BUNDLED_CA_PATCH_SCRIPT}`, 180).catch((e) => ({ exitCode: 1, result: String(e) }));
|
|
703
|
+
return (r.result ?? "").split("\n").filter((line) => line.startsWith(BUNDLED_CA_PATCHED_MARKER)).map((line) => line.slice(BUNDLED_CA_PATCHED_MARKER.length).trim()).filter(Boolean);
|
|
704
|
+
}
|
|
512
705
|
async function probeCanary(sandbox, canaryHost, expectedTwinId) {
|
|
513
706
|
if (!HOSTNAME_RE.test(canaryHost)) {
|
|
514
707
|
throw new ReceiptIntegrityError(
|
|
@@ -562,9 +755,15 @@ var VerisApiImpl = class {
|
|
|
562
755
|
this.ctx = ctx;
|
|
563
756
|
}
|
|
564
757
|
ctx;
|
|
758
|
+
get ownsTwin() {
|
|
759
|
+
return this.ctx.ownsTwin;
|
|
760
|
+
}
|
|
565
761
|
get sandboxId() {
|
|
566
762
|
return this.ctx.twinId;
|
|
567
763
|
}
|
|
764
|
+
get environmentId() {
|
|
765
|
+
return this.ctx.environmentId;
|
|
766
|
+
}
|
|
568
767
|
get mode() {
|
|
569
768
|
return "gateway";
|
|
570
769
|
}
|
|
@@ -590,10 +789,13 @@ var VerisApiImpl = class {
|
|
|
590
789
|
}
|
|
591
790
|
async receipt(service) {
|
|
592
791
|
await probeCanary(this.ctx.sandbox, this.ctx.canaryHost, this.ctx.twinId);
|
|
593
|
-
if (service !== void 0)
|
|
792
|
+
if (service !== void 0) {
|
|
793
|
+
const svc = await this.resolveService(service);
|
|
794
|
+
return fetchReceiptEntry(svc, this.watermark(svc.name));
|
|
795
|
+
}
|
|
594
796
|
const services = await this.services();
|
|
595
797
|
const entries = await Promise.all(
|
|
596
|
-
services.filter((s) => isHttpUrl(s.control_url)).map(async (svc) => [svc.name, await fetchReceiptEntry(svc)])
|
|
798
|
+
services.filter((s) => isHttpUrl(s.control_url)).map(async (svc) => [svc.name, await fetchReceiptEntry(svc, this.watermark(svc.name))])
|
|
597
799
|
);
|
|
598
800
|
return {
|
|
599
801
|
services: Object.fromEntries(entries),
|
|
@@ -609,10 +811,45 @@ var VerisApiImpl = class {
|
|
|
609
811
|
leaks() {
|
|
610
812
|
return ["udp-quic-possible", "ech-possible"];
|
|
611
813
|
}
|
|
814
|
+
/** Where this service's log was when the run began. 0 reads all of it. */
|
|
815
|
+
watermark(service) {
|
|
816
|
+
return this.ctx.watermarks?.[service] ?? 0;
|
|
817
|
+
}
|
|
818
|
+
async verifyIntegrity() {
|
|
819
|
+
await probeCanary(this.ctx.sandbox, this.ctx.canaryHost, this.ctx.twinId);
|
|
820
|
+
}
|
|
821
|
+
async receiptBaseline() {
|
|
822
|
+
await this.verifyIntegrity();
|
|
823
|
+
return captureBaseline(
|
|
824
|
+
this.ctx.twinId,
|
|
825
|
+
this.ctx.sandbox.id,
|
|
826
|
+
(await this.services()).filter((s) => isHttpUrl(s.control_url))
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
async receiptSince(baseline, service) {
|
|
830
|
+
await this.verifyIntegrity();
|
|
831
|
+
const services = (await this.services()).filter((s) => isHttpUrl(s.control_url));
|
|
832
|
+
if (service !== void 0 && !services.some((s) => s.name === service)) {
|
|
833
|
+
throw new VerisError(`unknown HTTP service '${service}'`, { phase: "receipt" });
|
|
834
|
+
}
|
|
835
|
+
await validateBaseline(baseline, this.ctx.twinId, this.ctx.sandbox.id, services);
|
|
836
|
+
const selected = service === void 0 ? services : services.filter((s) => s.name === service);
|
|
837
|
+
const entries = await Promise.all(selected.map(async (svc) => [svc.name, await fetchReceiptEntry(svc, baseline.services[svc.name].id)]));
|
|
838
|
+
await validateBaseline(baseline, this.ctx.twinId, this.ctx.sandbox.id, await this.services().then((s) => s.filter((v) => isHttpUrl(v.control_url))));
|
|
839
|
+
return { services: Object.fromEntries(entries), mode: "gateway", integrity: "verified", leaks: this.leaks() };
|
|
840
|
+
}
|
|
841
|
+
async control(service, resource, options) {
|
|
842
|
+
const svc = (await this.services()).find((s) => s.name === service);
|
|
843
|
+
if (!svc) throw new VerisError(`unknown service '${service}'`);
|
|
844
|
+
return serviceControl(svc, resource, options);
|
|
845
|
+
}
|
|
612
846
|
async assertTouched(service, match) {
|
|
613
847
|
const entry = await this.receipt(service);
|
|
614
848
|
const need = match?.minRequests ?? 1;
|
|
615
849
|
const matched = match ? entry.entries.filter((r) => (match.method === void 0 || r.method.toUpperCase() === match.method.toUpperCase()) && (match.path === void 0 || r.path.includes(match.path))) : entry.entries;
|
|
850
|
+
if (matched.length < need && entry.capped) {
|
|
851
|
+
throw new VerisError(`receipt for '${service}' is incomplete; insufficient evidence`, { phase: "receipt" });
|
|
852
|
+
}
|
|
616
853
|
if (matched.length < need) {
|
|
617
854
|
const what = match ? `matching ${match.method ?? "ANY"} ${match.path ?? "*"} (${matched.length}/${need})` : "any intercepted requests";
|
|
618
855
|
throw new VerisUntouchedError(
|
|
@@ -629,6 +866,36 @@ var VerisApiImpl = class {
|
|
|
629
866
|
getTrustEnv() {
|
|
630
867
|
return vendoredTrustEnv();
|
|
631
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* The same variables as a shell prelude, for a caller that has no env map to
|
|
871
|
+
* fill — anything running a command through a session or a shell it did not
|
|
872
|
+
* build the environment for. Daytona resets SSL_CERT_FILE,
|
|
873
|
+
* REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS to its own CA
|
|
874
|
+
* file inside the sandbox, and that file cannot verify the gateway's leaf, so
|
|
875
|
+
* a command that inherits them fails on certificate validation:
|
|
876
|
+
* `uv sync` dies with "invalid peer certificate: UnknownIssuer".
|
|
877
|
+
*
|
|
878
|
+
* sbx.process.executeCommand(`${sbx.veris.trustPrelude()} uv sync`)
|
|
879
|
+
*/
|
|
880
|
+
trustPrelude() {
|
|
881
|
+
return trustPrelude(this.getTrustEnv());
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Append the Veris CA to the CA bundles the code under test's own SDKs ship.
|
|
885
|
+
*
|
|
886
|
+
* The variables above reach every client that reads one. stripe-python does
|
|
887
|
+
* not: it passes `verify=stripe.ca_bundle_path`, so the first Stripe call
|
|
888
|
+
* fails with "Could not verify Stripe's SSL certificate" in a sandbox where
|
|
889
|
+
* curl, Node and `requests` all succeed. This patches the file itself, which
|
|
890
|
+
* is the Daytona-shaped version of the veris CLI's --patch-bundled-cas.
|
|
891
|
+
*
|
|
892
|
+
* Call it AFTER installing dependencies — the bundles arrive with them, so
|
|
893
|
+
* there is nothing to patch at create time. Safe to call again; it returns
|
|
894
|
+
* only the files it actually changed.
|
|
895
|
+
*/
|
|
896
|
+
patchBundledCas() {
|
|
897
|
+
return patchBundledCas(this.ctx.sandbox);
|
|
898
|
+
}
|
|
632
899
|
async deliverTo(target, opts = {}) {
|
|
633
900
|
const url = typeof target === "number" ? (await this.ctx.sandbox.getPreviewLink(target)).url : target;
|
|
634
901
|
await this.ctx.controlPlane.updateSandbox(
|
|
@@ -660,8 +927,70 @@ var VerisApiImpl = class {
|
|
|
660
927
|
}
|
|
661
928
|
};
|
|
662
929
|
|
|
930
|
+
// src/profile.ts
|
|
931
|
+
var import_node_fs = require("fs");
|
|
932
|
+
var import_node_os = require("os");
|
|
933
|
+
var import_node_path = require("path");
|
|
934
|
+
var import_yaml = require("yaml");
|
|
935
|
+
var DEFAULT_API_BASE = "https://svc.api.veris.ai";
|
|
936
|
+
function profilePath(home = (0, import_node_os.homedir)()) {
|
|
937
|
+
return (0, import_node_path.join)(home, ".veris", "twin.yaml");
|
|
938
|
+
}
|
|
939
|
+
function readOrUndefined(path) {
|
|
940
|
+
try {
|
|
941
|
+
return (0, import_node_fs.readFileSync)(path, "utf8");
|
|
942
|
+
} catch (e) {
|
|
943
|
+
if (e.code === "ENOENT") return void 0;
|
|
944
|
+
throw e;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function parseProfileFile(text, path) {
|
|
948
|
+
if (text === void 0) return void 0;
|
|
949
|
+
let parsed;
|
|
950
|
+
try {
|
|
951
|
+
parsed = (0, import_yaml.parse)(text);
|
|
952
|
+
} catch (cause) {
|
|
953
|
+
throw new VerisError(`${path} is not valid YAML`, { phase: "credentials", cause });
|
|
954
|
+
}
|
|
955
|
+
if (parsed === null || parsed === void 0) return {};
|
|
956
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
957
|
+
throw new VerisError(`${path} is not a profile file (expected a mapping)`, { phase: "credentials" });
|
|
958
|
+
}
|
|
959
|
+
return parsed;
|
|
960
|
+
}
|
|
961
|
+
function selectProfileName(env, file) {
|
|
962
|
+
return env.VERIS_PROFILE || file?.active_profile || "default";
|
|
963
|
+
}
|
|
964
|
+
function resolveVerisCredentials(opts = {}) {
|
|
965
|
+
const env = opts.env ?? process.env;
|
|
966
|
+
const path = opts.path ?? profilePath();
|
|
967
|
+
const file = parseProfileFile((opts.readFile ?? readOrUndefined)(path), path);
|
|
968
|
+
const name = selectProfileName(env, file);
|
|
969
|
+
const profile = file?.profiles?.[name] ?? void 0;
|
|
970
|
+
const found = profile !== void 0 && profile !== null;
|
|
971
|
+
const apiKey = opts.apiKey || env.VERIS_API_KEY || (found ? profile.api_key : void 0) || void 0;
|
|
972
|
+
const keySource = opts.apiKey ? "option" : env.VERIS_API_KEY ? "env" : apiKey ? "profile" : "none";
|
|
973
|
+
const profileBase = found ? profile.api_base : void 0;
|
|
974
|
+
const apiBase = (opts.apiBase || env.VERIS_API_BASE || profileBase || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
975
|
+
const baseSource = opts.apiBase ? "option" : env.VERIS_API_BASE ? "env" : profileBase ? "profile" : "default";
|
|
976
|
+
return { apiKey, apiBase, keySource, baseSource, profile: { name, path, found } };
|
|
977
|
+
}
|
|
978
|
+
function missingKeyWhere(c) {
|
|
979
|
+
const where = c.profile.found ? `profile '${c.profile.name}' in ${c.profile.path} has no api_key` : `${c.profile.path} has no profile '${c.profile.name}'`;
|
|
980
|
+
return `VERIS_API_KEY is not set, and ${where}`;
|
|
981
|
+
}
|
|
982
|
+
var KEY_SOURCES_HINT = "Run `veris login` (the veris CLI saves the key there; `--profile <name>` for another plane, and VERIS_PROFILE picks it here), or set VERIS_API_KEY in the environment. Get a key at https://studio.veris.ai";
|
|
983
|
+
function missingKeyMessage(c) {
|
|
984
|
+
return `no Veris API key: ${missingKeyWhere(c)}. ${KEY_SOURCES_HINT}`;
|
|
985
|
+
}
|
|
986
|
+
function requireVerisCredentials(opts = {}) {
|
|
987
|
+
const c = resolveVerisCredentials(opts);
|
|
988
|
+
if (!c.apiKey) throw new MissingCredentialsError(missingKeyMessage(c), { phase: "credentials" });
|
|
989
|
+
return c;
|
|
990
|
+
}
|
|
991
|
+
|
|
663
992
|
// src/version.ts
|
|
664
|
-
var SDK_VERSION = true ? "0.
|
|
993
|
+
var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
665
994
|
|
|
666
995
|
// src/daytona.ts
|
|
667
996
|
var LABEL = {
|
|
@@ -671,12 +1000,24 @@ var LABEL = {
|
|
|
671
1000
|
mode: "veris_mode",
|
|
672
1001
|
egress: "veris_egress",
|
|
673
1002
|
ownsTwin: "veris_owns_twin",
|
|
674
|
-
canaryHost: "veris_canary_host"
|
|
1003
|
+
canaryHost: "veris_canary_host",
|
|
1004
|
+
/** Unique to ONE create() call, and the only way back to a sandbox whose
|
|
1005
|
+
* create threw before it could hand the object over — see reapFailedCreate.
|
|
1006
|
+
* The twin id will not do: an attached twin is shared by every sandbox on
|
|
1007
|
+
* it, and deleting the wrong one of those would be worse than the leak. */
|
|
1008
|
+
createId: "veris_create_id"
|
|
675
1009
|
};
|
|
676
1010
|
var VERIS_LABEL_KEYS = Object.values(LABEL);
|
|
677
1011
|
function isVerisSandbox(sbx) {
|
|
678
1012
|
return typeof sbx.verisSandboxId === "string";
|
|
679
1013
|
}
|
|
1014
|
+
function verisTwinId(sandbox) {
|
|
1015
|
+
return sandbox.labels?.[LABEL.twinId];
|
|
1016
|
+
}
|
|
1017
|
+
function verisOwnsTwin(sandbox) {
|
|
1018
|
+
const labels = sandbox.labels ?? {};
|
|
1019
|
+
return labels[LABEL.twinId] !== void 0 && labels[LABEL.ownsTwin] !== "false";
|
|
1020
|
+
}
|
|
680
1021
|
var Daytona = class extends import_sdk.Daytona {
|
|
681
1022
|
verisDefaults;
|
|
682
1023
|
constructor(config) {
|
|
@@ -696,6 +1037,7 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
696
1037
|
const egress = v.egress ?? "strict";
|
|
697
1038
|
const ttlMinutes = v.ttlMinutes ?? 60;
|
|
698
1039
|
const ownsTwin = !v.attachSandboxId;
|
|
1040
|
+
const createId = crypto.randomUUID();
|
|
699
1041
|
const twin = await this.provisionTwin(controlPlane, v, coords, ttlMinutes);
|
|
700
1042
|
const cleanupTwin = async () => {
|
|
701
1043
|
if (ownsTwin) await controlPlane.deleteTwin(twin.environment_id, twin.id).catch(() => {
|
|
@@ -703,6 +1045,8 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
703
1045
|
};
|
|
704
1046
|
let sandbox;
|
|
705
1047
|
let credential;
|
|
1048
|
+
let watermarks = {};
|
|
1049
|
+
let daytonaAsked = false;
|
|
706
1050
|
try {
|
|
707
1051
|
credential = await controlPlane.mintEgressCredential(twin.environment_id, twin.id);
|
|
708
1052
|
if (!credential) {
|
|
@@ -719,17 +1063,19 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
719
1063
|
}
|
|
720
1064
|
const services = twin.services?.length ? twin.services : await controlPlane.services(twin.id);
|
|
721
1065
|
const proxyUrl = gatewayProxyUrl(credential);
|
|
722
|
-
const network = buildNetwork({
|
|
723
|
-
|
|
724
|
-
mode: egress,
|
|
725
|
-
// The gateway has to be reachable or nothing is: taken from the URL we
|
|
726
|
-
// are actually going to use, so the two can never disagree.
|
|
727
|
-
gatewayHosts: [new URL(proxyUrl).hostname, credential.canary_host].filter(Boolean),
|
|
728
|
-
allowOut: v.allowOut,
|
|
729
|
-
allowRegistries: v.allowRegistries
|
|
730
|
-
});
|
|
1066
|
+
const network = buildNetwork({ mode: egress, gatewayIps: await gatewayIps(credential, proxyUrl) });
|
|
1067
|
+
watermarks = await readWatermarks(services);
|
|
731
1068
|
const verisManaged = {
|
|
732
1069
|
...v.installCa !== false ? sanitizeTrustEnv(void 0) : {},
|
|
1070
|
+
// Node reads none of the variables above once Daytona has overwritten
|
|
1071
|
+
// them; the flag makes it read OpenSSL's store, which the CA install
|
|
1072
|
+
// fills. Appended, not replaced: a caller's own NODE_OPTIONS keep working.
|
|
1073
|
+
...v.installCa !== false ? { NODE_OPTIONS: nodeOptionsWithTrust(rest.envVars?.NODE_OPTIONS) } : {},
|
|
1074
|
+
// Node ignores HTTPS_PROXY unless told to, and Daytona blocks anything
|
|
1075
|
+
// that dials out directly — so without this every plain `https.get`
|
|
1076
|
+
// and `fetch` in Node 24+ dies with ECONNRESET instead of reaching
|
|
1077
|
+
// the gateway. Older Node needs a proxy agent; the variable is inert there.
|
|
1078
|
+
NODE_USE_ENV_PROXY: "1",
|
|
733
1079
|
...v.dataPlaneEnv !== false ? dataPlaneEnv(services) : {},
|
|
734
1080
|
VERIS_SANDBOX_ID: twin.id
|
|
735
1081
|
};
|
|
@@ -744,20 +1090,23 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
744
1090
|
[LABEL.egress]: egress,
|
|
745
1091
|
[LABEL.ownsTwin]: String(ownsTwin),
|
|
746
1092
|
[LABEL.mode]: "gateway",
|
|
747
|
-
[LABEL.canaryHost]: credential.canary_host
|
|
1093
|
+
[LABEL.canaryHost]: credential.canary_host,
|
|
1094
|
+
[LABEL.createId]: createId
|
|
748
1095
|
},
|
|
749
|
-
...network,
|
|
750
|
-
// 3. Where Daytona forwards everything
|
|
751
|
-
//
|
|
752
|
-
//
|
|
1096
|
+
...network.params,
|
|
1097
|
+
// 3. Where Daytona forwards everything. Chained, not advisory: an
|
|
1098
|
+
// unreachable gateway makes traffic 502 rather than quietly going
|
|
1099
|
+
// direct.
|
|
753
1100
|
outboundProxyUrl: proxyUrl,
|
|
754
1101
|
ttlMinutes: rest.ttlMinutes ?? ttlMinutes
|
|
755
1102
|
};
|
|
1103
|
+
daytonaAsked = true;
|
|
756
1104
|
sandbox = await this.baseCreate(createParams, options);
|
|
757
1105
|
} catch (cause) {
|
|
1106
|
+
const failed = daytonaAsked ? await reapFailedCreate((labels) => super.list({ labels }), createId) : void 0;
|
|
758
1107
|
await cleanupTwin();
|
|
759
1108
|
if (cause instanceof VerisError) throw cause;
|
|
760
|
-
throw new VerisError(
|
|
1109
|
+
throw new VerisError(sandboxCreateMessage(cause, failed, rest), {
|
|
761
1110
|
phase: "sandbox-create",
|
|
762
1111
|
verisSandboxId: twin.id,
|
|
763
1112
|
cause
|
|
@@ -778,7 +1127,8 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
778
1127
|
twinId: twin.id,
|
|
779
1128
|
egress,
|
|
780
1129
|
ownsTwin,
|
|
781
|
-
canaryHost: credential.canary_host
|
|
1130
|
+
canaryHost: credential.canary_host,
|
|
1131
|
+
watermarks
|
|
782
1132
|
});
|
|
783
1133
|
}
|
|
784
1134
|
/**
|
|
@@ -810,9 +1160,10 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
810
1160
|
const labels = sandbox.labels ?? {};
|
|
811
1161
|
const twinId = labels[LABEL.twinId];
|
|
812
1162
|
if (!twinId) return sandbox;
|
|
813
|
-
const
|
|
1163
|
+
const creds = resolveVerisCredentials({ apiKey: this.verisDefaults.apiKey, apiBase: this.verisDefaults.apiBase });
|
|
1164
|
+
const apiKey = creds.apiKey;
|
|
814
1165
|
if (!apiKey) return sandbox;
|
|
815
|
-
const trustedBase =
|
|
1166
|
+
const trustedBase = creds.baseSource === "default" ? void 0 : creds.apiBase;
|
|
816
1167
|
const labelBase = labels[LABEL.apiBase];
|
|
817
1168
|
if (trustedBase && labelBase && labelBase !== trustedBase) {
|
|
818
1169
|
throw new VerisError(
|
|
@@ -894,19 +1245,48 @@ var Daytona = class extends import_sdk.Daytona {
|
|
|
894
1245
|
};
|
|
895
1246
|
var daytona_default = Daytona;
|
|
896
1247
|
function resolveCoordinates(v) {
|
|
897
|
-
const
|
|
898
|
-
if (!apiKey) {
|
|
899
|
-
throw new MissingCredentialsError(
|
|
900
|
-
"no Veris API key: set VERIS_API_KEY in your environment, or pass veris.apiKey. Get one at https://studio.veris.ai",
|
|
901
|
-
{ phase: "credentials" }
|
|
902
|
-
);
|
|
903
|
-
}
|
|
1248
|
+
const creds = requireVerisCredentials({ apiKey: v.apiKey, apiBase: v.apiBase });
|
|
904
1249
|
return {
|
|
905
|
-
apiKey,
|
|
1250
|
+
apiKey: creds.apiKey,
|
|
906
1251
|
environmentId: v.environmentId ?? process.env.VERIS_ENVIRONMENT_ID,
|
|
907
|
-
apiBase:
|
|
1252
|
+
apiBase: creds.apiBase
|
|
908
1253
|
};
|
|
909
1254
|
}
|
|
1255
|
+
async function reapFailedCreate(list, createId) {
|
|
1256
|
+
try {
|
|
1257
|
+
for await (const sandbox of list({ [LABEL.createId]: createId })) {
|
|
1258
|
+
if (sandbox.labels?.[LABEL.createId] !== createId) continue;
|
|
1259
|
+
if (!sandbox.errorReason) await sandbox.refreshData().catch(() => {
|
|
1260
|
+
});
|
|
1261
|
+
const found = { id: sandbox.id, state: sandbox.state, errorReason: sandbox.errorReason };
|
|
1262
|
+
return { ...found, deleted: await sandbox.delete().then(() => true, () => false) };
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
return void 0;
|
|
1267
|
+
}
|
|
1268
|
+
function sandboxCreateMessage(cause, failed, params = {}) {
|
|
1269
|
+
const base = "Daytona sandbox create failed";
|
|
1270
|
+
if (!failed) return base;
|
|
1271
|
+
const reason = failed.errorReason?.trim().replace(/[.\s]+$/, "");
|
|
1272
|
+
const parts = [reason ? `${base}: ${reason}.` : `${base}.`];
|
|
1273
|
+
const image = typeof params.image === "string" ? params.image : void 0;
|
|
1274
|
+
if (image && failed.state === "build_failed") {
|
|
1275
|
+
parts.push(
|
|
1276
|
+
`Daytona builds "${image}" on its own servers and pulls it from a registry, so an image that exists only in your local Docker daemon is not one it can see.`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
parts.push(failed.deleted ? `The half-built sandbox ${failed.id} was deleted.` : `The half-built sandbox ${failed.id} could not be deleted \u2014 remove it with \`veris-daytona teardown ${failed.id}\`.`);
|
|
1280
|
+
const raw = cause instanceof Error ? cause.message : "";
|
|
1281
|
+
if (raw) parts.push(`(Daytona SDK: ${raw})`);
|
|
1282
|
+
return parts.join(" ");
|
|
1283
|
+
}
|
|
1284
|
+
async function readWatermarks(services) {
|
|
1285
|
+
const marks = await Promise.all(
|
|
1286
|
+
services.filter((s) => isHttpUrl(s.control_url)).map(async (svc) => [svc.name, await fetchWatermark(svc).catch(() => 0)])
|
|
1287
|
+
);
|
|
1288
|
+
return Object.fromEntries(marks);
|
|
1289
|
+
}
|
|
910
1290
|
function stripVeris(params) {
|
|
911
1291
|
const { veris: _veris, ...rest } = params ?? {};
|
|
912
1292
|
return rest;
|
|
@@ -918,13 +1298,84 @@ function reserveLabels(labels) {
|
|
|
918
1298
|
}
|
|
919
1299
|
return out;
|
|
920
1300
|
}
|
|
1301
|
+
|
|
1302
|
+
// src/daytona-key.ts
|
|
1303
|
+
var DAYTONA_API_URL = "https://app.daytona.io/api";
|
|
1304
|
+
var DELETE_SANDBOXES = "delete:sandboxes";
|
|
1305
|
+
function daytonaApiUrl(env = process.env) {
|
|
1306
|
+
return (env.DAYTONA_API_URL || env.DAYTONA_SERVER_URL || DAYTONA_API_URL).replace(/\/$/, "");
|
|
1307
|
+
}
|
|
1308
|
+
async function fetchDaytonaKey(apiKey, apiUrl = daytonaApiUrl()) {
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await fetch(`${apiUrl}/api-keys/current`, {
|
|
1311
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1312
|
+
});
|
|
1313
|
+
if (!res.ok) return void 0;
|
|
1314
|
+
const body = await res.json();
|
|
1315
|
+
if (!Array.isArray(body.permissions)) return void 0;
|
|
1316
|
+
return {
|
|
1317
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
1318
|
+
permissions: body.permissions.filter((p) => typeof p === "string")
|
|
1319
|
+
};
|
|
1320
|
+
} catch {
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function canDeleteSandboxes(key) {
|
|
1325
|
+
return key === void 0 ? void 0 : key.permissions.includes(DELETE_SANDBOXES);
|
|
1326
|
+
}
|
|
1327
|
+
function describeKey(key) {
|
|
1328
|
+
const name = key.name ? `"${key.name}" ` : "";
|
|
1329
|
+
return `${name}(permissions: ${key.permissions.join(", ") || "none"})`;
|
|
1330
|
+
}
|
|
1331
|
+
var DELETE_PERMISSION_FIX = 'create a key with the "delete sandboxes" permission at https://app.daytona.io/dashboard/keys';
|
|
1332
|
+
function cannotTeardownWarning(key, autoStopMinutes, autoDeleteMinutes) {
|
|
1333
|
+
return `DAYTONA_API_KEY ${describeKey(key)} lacks \`${DELETE_SANDBOXES}\`, so \`veris-daytona teardown\` will be refused for the sandbox about to be created, and \`run\` cannot delete its own. The box still stops after ${autoStopMinutes} idle minutes and is deleted ${autoDeleteMinutes} minutes after it stops. For a teardown that works, ${DELETE_PERMISSION_FIX} and use it instead.`;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/teardown.ts
|
|
1337
|
+
var TEARDOWN_USAGE = `usage: veris-daytona teardown <daytona-sandbox-id>
|
|
1338
|
+
|
|
1339
|
+
Deletes the Daytona sandbox. The id is the one \`veris-daytona provision\`
|
|
1340
|
+
printed as daytonaSandboxId \u2014 not the Veris twin's id.
|
|
1341
|
+
|
|
1342
|
+
The twin follows the rule it was created under: one this package created is
|
|
1343
|
+
deleted with the sandbox, one it attached to (\`provision\`, or \`run --sandbox\`)
|
|
1344
|
+
is yours and is left running. Which of the two happened is printed.
|
|
1345
|
+
|
|
1346
|
+
needs: DAYTONA_API_KEY with the ${DELETE_SANDBOXES} permission \u2014 a key made with
|
|
1347
|
+
"write sandboxes" alone is refused, and the refusal says when Daytona will stop
|
|
1348
|
+
and delete the box on its own. To reach a twin that must be deleted, a Veris
|
|
1349
|
+
key too: VERIS_API_KEY, or the profile \`veris login\` saved.
|
|
1350
|
+
|
|
1351
|
+
exit code: 0 when the sandbox is deleted; 1 when there is no such sandbox, or
|
|
1352
|
+
the key may not delete one.`;
|
|
1353
|
+
function isPermissionDenied(e) {
|
|
1354
|
+
return typeof e === "object" && e !== null && e.statusCode === 403;
|
|
1355
|
+
}
|
|
1356
|
+
function sandboxBrakes(r) {
|
|
1357
|
+
const stop = r.autoStopMinutes && r.autoStopMinutes > 0 ? `stops after ${r.autoStopMinutes} idle minutes` : "never stops on its own";
|
|
1358
|
+
const del = r.autoDeleteMinutes === void 0 || r.autoDeleteMinutes < 0 ? "is never auto-deleted" : r.autoDeleteMinutes === 0 ? "is deleted as soon as it stops" : `is deleted ${r.autoDeleteMinutes} minutes after it stops`;
|
|
1359
|
+
const destroy = r.expiresAt ? `, and is destroyed at ${r.expiresAt} whatever state it is in` : "";
|
|
1360
|
+
return `${stop} and ${del}${destroy}`;
|
|
1361
|
+
}
|
|
1362
|
+
function teardownRefusedMessage(r) {
|
|
1363
|
+
const key = r.key ? `this DAYTONA_API_KEY ${describeKey(r.key)} lacks the \`${DELETE_SANDBOXES}\` permission` : `Daytona answered 403 (Access denied), which is what a DAYTONA_API_KEY without the \`${DELETE_SANDBOXES}\` permission gets \u2014 the key's own record could not be read to confirm it`;
|
|
1364
|
+
const twin = r.twinId === void 0 ? "" : r.ownsTwin ? ` Its twin ${r.twinId} was deleted before Daytona refused, so nothing is left on the Veris side.` : ` Twin ${r.twinId} is yours and was not touched.`;
|
|
1365
|
+
return `Daytona refused to delete sandbox ${r.sandboxId}: ${key}. The sandbox is still there: it ${sandboxBrakes(r)}.${twin} To delete it sooner, ${DELETE_PERMISSION_FIX}, then run \`veris-daytona teardown ${r.sandboxId}\` with it \u2014 or delete the sandbox in the Daytona dashboard.`;
|
|
1366
|
+
}
|
|
921
1367
|
// Annotate the CommonJS export names for ESM import in node:
|
|
922
1368
|
0 && (module.exports = {
|
|
1369
|
+
BUNDLED_CA_FILES,
|
|
1370
|
+
BUNDLED_CA_PATCH_SCRIPT,
|
|
923
1371
|
CA_CERT_PATH,
|
|
924
1372
|
ControlPlane,
|
|
925
|
-
|
|
1373
|
+
DAYTONA_API_URL,
|
|
1374
|
+
DEFAULT_API_BASE,
|
|
1375
|
+
DELETE_SANDBOXES,
|
|
926
1376
|
Daytona,
|
|
927
1377
|
MissingCredentialsError,
|
|
1378
|
+
NODE_TRUST_FLAG,
|
|
928
1379
|
ReceiptIntegrityError,
|
|
929
1380
|
SDK_VERSION,
|
|
930
1381
|
SYSTEM_BUNDLE,
|
|
@@ -938,13 +1389,32 @@ function reserveLabels(labels) {
|
|
|
938
1389
|
VerisGatewayUnreachableError,
|
|
939
1390
|
VerisUntouchedError,
|
|
940
1391
|
assertHttpControlPlane,
|
|
1392
|
+
buildNetwork,
|
|
1393
|
+
canDeleteSandboxes,
|
|
1394
|
+
cannotTeardownWarning,
|
|
941
1395
|
dataPlaneHosts,
|
|
1396
|
+
daytonaApiUrl,
|
|
1397
|
+
directTwinHosts,
|
|
1398
|
+
fetchDaytonaKey,
|
|
942
1399
|
fetchManual,
|
|
1400
|
+
gatewayIps,
|
|
943
1401
|
gatewayProxyUrl,
|
|
1402
|
+
isPermissionDenied,
|
|
944
1403
|
isVerisSandbox,
|
|
1404
|
+
missingKeyMessage,
|
|
1405
|
+
nodeOptionsWithTrust,
|
|
1406
|
+
patchBundledCas,
|
|
1407
|
+
profilePath,
|
|
1408
|
+
requireVerisCredentials,
|
|
1409
|
+
resolveVerisCredentials,
|
|
1410
|
+
sandboxBrakes,
|
|
1411
|
+
teardownRefusedMessage,
|
|
1412
|
+
trustPrelude,
|
|
945
1413
|
twinHosts,
|
|
946
1414
|
vendorHosts,
|
|
947
1415
|
vendoredTrustEnv,
|
|
1416
|
+
verisOwnsTwin,
|
|
1417
|
+
verisTwinId,
|
|
948
1418
|
...require("@daytona/sdk")
|
|
949
1419
|
});
|
|
950
1420
|
//# sourceMappingURL=index.cjs.map
|