@veris-ai/daytona 0.1.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 ADDED
@@ -0,0 +1,127 @@
1
+ # @veris-ai/daytona
2
+
3
+ Run code in a [Daytona](https://daytona.io) sandbox where calls to
4
+ `api.stripe.com` and the rest of your vendor stack are answered by **Veris
5
+ twins** — stateful, contract-accurate fakes — with the code under test
6
+ completely unmodified.
7
+
8
+ No base-URL overrides, no injected config, no mocking library. Your code keeps
9
+ its production hostnames, credentials and SDKs; the network layer does the rest.
10
+ And every run ends with a **receipt** of what the vendor actually received.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm i @veris-ai/daytona @daytona/sdk
16
+ ```
17
+
18
+ Both, because `@daytona/sdk` is a peer dependency — that is what keeps
19
+ `err instanceof DaytonaNotFoundError` working across the package boundary.
20
+
21
+ | variable | where from |
22
+ |---|---|
23
+ | `DAYTONA_API_KEY` | [app.daytona.io/dashboard/keys](https://app.daytona.io/dashboard/keys) |
24
+ | `VERIS_API_KEY` | [studio.veris.ai](https://studio.veris.ai) |
25
+ | `VERIS_ENVIRONMENT_ID` | a Veris environment — it decides which vendor services your twin gets |
26
+
27
+ ## Use
28
+
29
+ Change one import. Everything else in your Daytona code stays as it was.
30
+
31
+ ```ts
32
+ import { Daytona } from '@veris-ai/daytona' // was '@daytona/sdk'
33
+
34
+ const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
35
+ const sbx = await daytona.create({ image: 'python:3.12-slim' })
36
+
37
+ await sbx.process.executeCommand('pytest tests/integration')
38
+
39
+ // The assertion the whole thing exists for: did the twin actually see it?
40
+ await sbx.veris.assertTouched('stripe', { method: 'POST', path: '/v1/charges' })
41
+
42
+ await sbx.delete() // deletes the twin too
43
+ ```
44
+
45
+ This package re-exports everything from `@daytona/sdk`, so it is the only import
46
+ you need to change. No particular sandbox image is required.
47
+
48
+ ### Why `assertTouched` and not just a green suite
49
+
50
+ A test suite that skipped its integration and one that exercised it look
51
+ identical from inside the sandbox. So does a call your code believed it made.
52
+ The receipt is the only thing that separates them, and `assertTouched` throws
53
+ when it is empty.
54
+
55
+ ## `sbx.veris`
56
+
57
+ | method | what it gives you |
58
+ |---|---|
59
+ | `receipt()` | every request the twin received, with the mode and integrity of the run |
60
+ | `receipt(service)` | the same for one service |
61
+ | `assertTouched(service, match?)` | throws unless the twin saw matching traffic |
62
+ | `services()` | what the twin answers for, and where |
63
+ | `getDataPlaneEnv()` | `{ DATABASE_URL: … }` for non-HTTP twin services |
64
+ | `getTrustEnv()` | the CA variables injected into the sandbox |
65
+ | `deliverTo(port \| url)` | point vendor webhooks back at your sandbox |
66
+
67
+ `sbx.verisSandboxId` is the twin's id — not to be confused with `sbx.id`, which
68
+ is Daytona's.
69
+
70
+ ## Options
71
+
72
+ ```ts
73
+ await daytona.create({
74
+ image: 'node:22',
75
+ veris: {
76
+ egress: 'strict', // default. 'open' sets no allowlist — debugging only
77
+ allowOut: ['internal.corp'],
78
+ allowRegistries: true, // default. npm, PyPI, apt, crates…
79
+ installCa: true, // default
80
+ ttlMinutes: 60,
81
+ attachSandboxId: 'sbx_…', // reuse an existing twin; delete() will not remove it
82
+ disabled: false, // true = a plain Daytona sandbox, no twin
83
+ },
84
+ })
85
+ ```
86
+
87
+ Coordinates can also come from `veris.apiKey` / `veris.environmentId` /
88
+ `veris.apiBase` instead of the environment.
89
+
90
+ ## How it works
91
+
92
+ Every sandbox is created with two Daytona parameters: a `domainAllowList` (the
93
+ vendor hostnames the twin answers for, the gateway, the twin's data planes, and
94
+ package registries — nothing else leaves) and an `outboundProxyUrl` pointing at
95
+ the Veris gateway over HTTP CONNECT.
96
+
97
+ Daytona chains them: sandbox traffic reaches Daytona's own proxy, which drops
98
+ anything not allowlisted and forwards the rest to the gateway, which answers
99
+ vendor hostnames from the twin. Nothing of ours runs inside the sandbox, which
100
+ is why any image works.
101
+
102
+ Before `create()` resolves, a canary probe dials a reserved hostname only the
103
+ gateway answers, whose body carries the twin id. It proves in one request that
104
+ egress is tunnelled, that the credential reached the right twin, and that trust
105
+ is wired — and it cannot pass by accident, because outside the tunnel that host
106
+ has no listener. It runs again on every `receipt()`, so a receipt is never
107
+ reported from a sandbox whose egress cannot be vouched for.
108
+
109
+ ## Errors
110
+
111
+ Every error is a `VerisError` with a `phase`, so a failure says which of the
112
+ four systems involved refused:
113
+
114
+ `credentials` · `twin-provision` · `credential-mint` · `sandbox-create` ·
115
+ `ca-install` · `canary` · `receipt` · `attach`
116
+
117
+ ## Limitations
118
+
119
+ - **Requires a Veris control plane that serves an HTTP CONNECT gateway.**
120
+ Daytona accepts only `http`/`https` outbound proxies, so a SOCKS-only gateway
121
+ cannot be used; `create()` then fails at `credential-mint` saying so.
122
+ - **QUIC/HTTP3 and ECH are not intercepted.** The gateway relays TCP. Both are
123
+ reported in the receipt's `leaks` rather than silently omitted.
124
+
125
+ ## License
126
+
127
+ Apache-2.0. Source: [veris-ai/veris-daytona](https://github.com/veris-ai/veris-daytona).
@@ -0,0 +1,89 @@
1
+ export interface RouteEntry {
2
+ /** A real vendor hostname the service answers for. */
3
+ host: string;
4
+ /** Path prefixes narrowing the claim when several services share the host. */
5
+ paths?: string[] | null;
6
+ }
7
+ export interface ServiceInfo {
8
+ name: string;
9
+ status: string;
10
+ /** What the code under test points at: gateway URL for http services, a DSN for e.g. postgres. */
11
+ url: string;
12
+ /** Where /veris/* lives — always an http URL. */
13
+ control_url: string;
14
+ env_hint?: string | null;
15
+ routes?: RouteEntry[] | null;
16
+ }
17
+ export interface TwinSandbox {
18
+ id: string;
19
+ environment_id: string;
20
+ status: 'provisioning' | 'ready' | 'failed' | 'degraded' | 'terminating' | string;
21
+ created_at?: string | null;
22
+ expires_at?: string | null;
23
+ services: ServiceInfo[];
24
+ failure_reason?: string | null;
25
+ metadata?: Record<string, string>;
26
+ }
27
+ /** Response of POST /v1/sandboxes/{sid}/egress-credential (gateway mode). */
28
+ export interface EgressCredential {
29
+ /** SOCKS5 endpoint. What @veris-ai/e2b uses; Daytona cannot (it accepts only
30
+ * http/https outbound proxies). */
31
+ socks_address: string;
32
+ /** HTTP CONNECT endpoint, host:port. Absent on a control plane that predates
33
+ * the CONNECT listener — which is a hard error here, since Daytona has no
34
+ * other way to reach the gateway. */
35
+ connect_address?: string;
36
+ /** The CONNECT endpoint as a complete proxy URL, credentials included.
37
+ * Preferred over building one from connect_address: the gateway owns its own
38
+ * auth format, and guessing it is how you end up sending the wrong password. */
39
+ http_proxy_url?: string;
40
+ username: string;
41
+ password: string;
42
+ ca_pem: string;
43
+ canary_host: string;
44
+ min_sdk?: string;
45
+ expires_at?: string;
46
+ /** Server-served CA trust env map; the SDK's vendored list is the fallback. */
47
+ trust_env?: Record<string, string>;
48
+ }
49
+ /** Mutable fields of a running twin. An OMITTED key is left alone; an explicit
50
+ * null is a value (client_base_url: null unregisters). */
51
+ export interface SandboxPatch {
52
+ ttl_minutes?: number;
53
+ client_base_url?: string | null;
54
+ }
55
+ export interface ControlPlaneOpts {
56
+ apiKey: string;
57
+ apiBase: string;
58
+ /** Sent as X-Veris-SDK on every request, so the server can version-gate. */
59
+ sdkVersion: string;
60
+ }
61
+ export declare class ControlPlane {
62
+ readonly apiBase: string;
63
+ private readonly headers;
64
+ constructor(opts: ControlPlaneOpts);
65
+ private request;
66
+ private json;
67
+ createTwin(environmentId: string, opts?: {
68
+ ttlMinutes?: number;
69
+ metadata?: Record<string, string>;
70
+ }): Promise<TwinSandbox>;
71
+ getTwin(sandboxId: string): Promise<TwinSandbox | null>;
72
+ /** Poll until the twin reports ready. "failed" is terminal per the API docs. */
73
+ waitReady(sandboxId: string, timeoutMs: number): Promise<TwinSandbox>;
74
+ services(sandboxId: string): Promise<ServiceInfo[]>;
75
+ deleteTwin(environmentId: string, sandboxId: string): Promise<boolean>;
76
+ /**
77
+ * Mint (or re-mint) the gateway egress credential for a twin. Returns null
78
+ * when the control plane does not offer gateway mode at all (404 — route
79
+ * absent), so `mode: 'auto'` can fall back; throws VerisGatewayNotOfferedError
80
+ * on an explicit version refusal (409 sdk_too_old).
81
+ */
82
+ mintEgressCredential(environmentId: string, sandboxId: string): Promise<EgressCredential | null>;
83
+ /** PATCH the twin resource. Omitted fields are untouched by the server. */
84
+ updateSandbox(environmentId: string, sandboxId: string, patch: SandboxPatch): Promise<void>;
85
+ /** Extend a twin's TTL so it stays in lockstep with an extended Daytona sandbox. */
86
+ extendTtl(environmentId: string, sandboxId: string, ttlMinutes: number): Promise<void>;
87
+ /** Create-time preflight: is the gateway infrastructure up, per the control plane? */
88
+ gatewayHealth(): Promise<void>;
89
+ }
@@ -0,0 +1,92 @@
1
+ import { Daytona as BaseDaytona } from '@daytona/sdk';
2
+ import type { CreateSandboxFromImageParams, CreateSandboxFromSnapshotParams, DaytonaConfig, ListSandboxesQuery, Sandbox } from '@daytona/sdk';
3
+ import type { VerisApi } from './veris-api';
4
+ import type { EgressMode } from './network';
5
+ import { CA_CERT_PATH } from './trust';
6
+ export interface VerisOpts {
7
+ /** Veris API key. Falls back to process.env.VERIS_API_KEY. Required. */
8
+ apiKey?: string;
9
+ /** Veris environment the twin is deployed from. Falls back to process.env.VERIS_ENVIRONMENT_ID. */
10
+ environmentId?: string;
11
+ /** Control plane base. Falls back to process.env.VERIS_API_BASE, then 'https://svc.api.veris.ai'. */
12
+ apiBase?: string;
13
+ /** Attach to an EXISTING twin instead of provisioning one (advanced). delete() will NOT remove it. */
14
+ attachSandboxId?: string;
15
+ /** Twin TTL backstop, minutes. Default 60, kept in step with the sandbox's ttlMinutes. */
16
+ ttlMinutes?: number;
17
+ /** 'strict' (default): the sandbox reaches only its twin, its data planes,
18
+ * the control plane and package registries. 'open': no allowlist at all —
19
+ * debugging only, because a bypassing client then reaches the real vendor. */
20
+ egress?: EgressMode;
21
+ /** Extra hostnames to allow out. */
22
+ allowOut?: string[];
23
+ /** Allow package registries (npm, PyPI, apt, …). Default true: a coding
24
+ * sandbox that cannot install dependencies is not usable. */
25
+ allowRegistries?: boolean;
26
+ /** Install the gateway CA into the sandbox trust store. Default true; without
27
+ * it every HTTPS call to a vendor host fails certificate validation. */
28
+ installCa?: boolean;
29
+ /** Inject { [env_hint]: dsn } for non-HTTP twin services. Default true. */
30
+ dataPlaneEnv?: boolean;
31
+ /** Turn Veris off entirely for this create — a plain Daytona sandbox. */
32
+ disabled?: boolean;
33
+ }
34
+ export interface VerisDaytonaConfig extends DaytonaConfig {
35
+ veris?: VerisOpts;
36
+ }
37
+ /** A Daytona sandbox with the Veris surface attached. */
38
+ export type VerisSandbox = Sandbox & {
39
+ /** Receipts, services, assertions. Everything Veris adds. */
40
+ veris: VerisApi;
41
+ /** The Veris twin's id. Not to be confused with `sandbox.id`, the Daytona one. */
42
+ verisSandboxId: string;
43
+ };
44
+ /** Is this sandbox one of ours? Narrows for callers who hold a bare Sandbox. */
45
+ export declare function isVerisSandbox(sbx: Sandbox): sbx is VerisSandbox;
46
+ export declare class Daytona extends BaseDaytona {
47
+ private readonly verisDefaults;
48
+ constructor(config?: VerisDaytonaConfig);
49
+ create(params?: CreateSandboxFromSnapshotParams & {
50
+ veris?: VerisOpts;
51
+ }, options?: {
52
+ timeout?: number;
53
+ }): Promise<Sandbox>;
54
+ create(params?: CreateSandboxFromImageParams & {
55
+ veris?: VerisOpts;
56
+ }, options?: {
57
+ onSnapshotCreateLogs?: (chunk: string) => void;
58
+ timeout?: number;
59
+ }): Promise<Sandbox>;
60
+ /**
61
+ * Rehydrate the Veris surface on an existing sandbox.
62
+ *
63
+ * Not an optimisation — a necessity. The OpenCode plugin reconnects to a
64
+ * sandbox with get() on every resumed session and deletes through get() too,
65
+ * so a get() that returned a bare Sandbox would mean no receipts after any
66
+ * restart and a leaked twin on every delete.
67
+ */
68
+ get(sandboxIdOrName: string): Promise<Sandbox>;
69
+ /** Same rehydration for the sandboxes a list() streams. */
70
+ list(query?: ListSandboxesQuery): AsyncIterableIterator<Sandbox>;
71
+ /**
72
+ * Attach the Veris surface to a sandbox whose labels say it has a twin.
73
+ * A sandbox without our labels is passed through untouched — callers can use
74
+ * this client for ordinary Daytona work.
75
+ */
76
+ private rehydrate;
77
+ private provisionTwin;
78
+ /**
79
+ * Hang the Veris surface off the instance, and wrap delete() so teardown is
80
+ * automatic.
81
+ *
82
+ * Wrapping rather than asking callers to remember is the point: the OpenCode
83
+ * plugin's existing `sandbox.delete()` then removes the twin too, with no
84
+ * change to the plugin at all. A twin outlives its sandbox otherwise, until
85
+ * its TTL reaps it — invisible until the bill arrives.
86
+ */
87
+ private attach;
88
+ /** super.create through the overload the params actually match. */
89
+ private baseCreate;
90
+ }
91
+ export default Daytona;
92
+ export { CA_CERT_PATH };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Every failure phase a Veris error can name. Bring-up spans four systems
3
+ * (control plane, Daytona API, the sandbox, the Veris gateway), so errors
4
+ * carry a structured phase rather than a wall of logs.
5
+ */
6
+ export type VerisErrorPhase = 'credentials' | 'twin-provision' | 'gateway-preflight' | 'credential-mint' | 'sandbox-create' | 'ca-install' | 'canary' | 'receipt' | 'attach';
7
+ /**
8
+ * Base class for every error this package throws. Deliberately NOT a subclass
9
+ * of any Daytona error: `e instanceof VerisError` cleanly separates Veris
10
+ * failures from Daytona failures in one catch.
11
+ */
12
+ export declare class VerisError extends Error {
13
+ readonly phase?: VerisErrorPhase;
14
+ /** The Veris twin's sandbox id, when one exists yet. */
15
+ readonly verisSandboxId?: string;
16
+ /** Verbatim control-plane response body, when the failure came from an API call. */
17
+ readonly responseBody?: unknown;
18
+ constructor(message: string, opts?: {
19
+ phase?: VerisErrorPhase;
20
+ verisSandboxId?: string;
21
+ responseBody?: unknown;
22
+ cause?: unknown;
23
+ });
24
+ }
25
+ /** A required credential/coordinate is missing. Thrown before any network call, naming the exact variable. */
26
+ export declare class MissingCredentialsError extends VerisError {
27
+ }
28
+ /** The Veris gateway infrastructure is down (control-plane health said so). */
29
+ export declare class VerisGatewayUnreachableError extends VerisError {
30
+ }
31
+ /** The control plane does not offer gateway mode (endpoint absent, or this SDK version is below min_sdk). */
32
+ export declare class VerisGatewayNotOfferedError extends VerisError {
33
+ /** Server-announced minimum SDK version, when the refusal carried one. */
34
+ readonly minSdk?: string;
35
+ constructor(message: string, opts?: ConstructorParameters<typeof VerisError>[1] & {
36
+ minSdk?: string;
37
+ });
38
+ }
39
+ /** The canary probe failed: egress is not (or no longer) reaching the twin. */
40
+ export declare class ReceiptIntegrityError extends VerisError {
41
+ }
42
+ /** assertTouched(): the named service saw zero intercepted requests — green without the receipt. */
43
+ export declare class VerisUntouchedError extends VerisError {
44
+ readonly service: string;
45
+ constructor(message: string, service: string, opts?: ConstructorParameters<typeof VerisError>[1]);
46
+ }
47
+ /** The Daytona sandbox is alive but its Veris twin is gone (TTL expiry, delete, reset). */
48
+ export declare class TwinExpiredError extends VerisError {
49
+ }
50
+ /** The sandbox image cannot host the Veris layer — in practice, no
51
+ * ca-certificates, so the gateway CA cannot be trusted. */
52
+ export declare class SnapshotUnsupportedError extends VerisError {
53
+ }
54
+ /** An inherited Daytona operation that would break the one-sandbox-one-twin invariant (e.g. fork). */
55
+ export declare class UnsupportedOperationError extends VerisError {
56
+ }
@@ -0,0 +1,65 @@
1
+ import type { Sandbox } from '@daytona/sdk';
2
+ /** Single-quote a string for POSIX sh. */
3
+ export declare function shellQuote(s: string): string;
4
+ /**
5
+ * Build the proxy URL Daytona is handed.
6
+ *
7
+ * The username is the tenant demux key — the gateway reads the sandbox id out
8
+ * of it. There is no separate secret: the id IS the capability, exactly as the
9
+ * SOCKS path treats it, so the password is a placeholder the gateway ignores.
10
+ *
11
+ * RFC 3986 §3.2.1 notes that `user:password` in userinfo is deprecated. It is
12
+ * also the only form HTTP proxy clients accept for RFC 7617 Basic credentials,
13
+ * and it is what Daytona forwards, so it is what we emit.
14
+ *
15
+ * `encodeURIComponent` is exactly right for userinfo: everything it leaves raw
16
+ * is unreserved or a sub-delim, and it escapes both separators — `:` to %3A and
17
+ * `@` to %40 — so a username can never break out into the authority.
18
+ *
19
+ * The address is validated rather than interpolated: it arrives from the
20
+ * control plane, and an unchecked value here would land in a URL and then in
21
+ * every client's proxy configuration.
22
+ */
23
+ export declare function gatewayProxyUrl(credential: {
24
+ http_proxy_url?: string;
25
+ connect_address?: string;
26
+ username: string;
27
+ }): string;
28
+ /**
29
+ * Make the gateway's CA trusted, without requiring anything of the image.
30
+ *
31
+ * Defensive rather than load-bearing on Daytona today, and the distinction is
32
+ * worth recording. Daytona's own proxy terminates TLS with a certificate signed
33
+ * by ITS CA — already trusted in the image — and re-originates to the gateway,
34
+ * so the client never validates our forged leaf. Verified: a vendor call
35
+ * succeeds with --cacert naming only Daytona's CA.
36
+ *
37
+ * We install ours regardless, because the day Daytona tunnels CONNECT
38
+ * end-to-end (the ordinary behaviour for an HTTP proxy) the gateway's leaf
39
+ * reaches the client directly and nothing works without it. One upload and one
40
+ * shell command against a total outage is a trade worth making.
41
+ *
42
+ * The obvious approach — drop the cert in /usr/local/share/ca-certificates and
43
+ * run update-ca-certificates — needs root AND that tool, and Daytona's default
44
+ * image has neither. So the artefact is a bundle we build ourselves at a
45
+ * world-writable path: the distribution's roots (when it has any) plus ours.
46
+ *
47
+ * Daytona overrides the best-known trust variables with its own CA, correctly
48
+ * for its proxy. The dozen it does not set still point at this bundle, which
49
+ * carries both CAs and every public root — so those tools verify rather than
50
+ * break.
51
+ *
52
+ * The system-store install still runs when it can, for anything that reads the
53
+ * store directly rather than honouring the variables. It is best-effort.
54
+ */
55
+ export declare function installCa(sandbox: Sandbox, caPem: string): Promise<void>;
56
+ /**
57
+ * The canary probe: one HTTPS request from inside the sandbox to a reserved
58
+ * hostname only the gateway answers, with the twin id in the body.
59
+ *
60
+ * Green proves three things in a single request — egress really is tunnelled
61
+ * through the gateway, the credential demuxed to the right twin, and the CA
62
+ * install worked. Dialled outside the tunnel the host has no listener, so this
63
+ * cannot pass by accident, which is what makes a receipt worth reading.
64
+ */
65
+ export declare function probeCanary(sandbox: Sandbox, canaryHost: string, expectedTwinId: string): Promise<void>;