@sandbank.dev/cloud 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,73 @@
1
+ # @sandbank.dev/cloud
2
+
3
+ > Sandbank Cloud adapter for [Sandbank](../../README.md) with built-in x402 payment support.
4
+
5
+ Connect to [Sandbank Cloud](https://sandbank.dev/cloud) — managed bare-metal KVM sandboxes with sub-second start times. Pay per sandbox with USDC via the x402 payment protocol, or use an API token for authenticated access.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @sandbank.dev/core @sandbank.dev/cloud
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### x402 Payment (pay-per-use)
16
+
17
+ ```typescript
18
+ import { createProvider } from '@sandbank.dev/core'
19
+ import { SandbankCloudAdapter } from '@sandbank.dev/cloud'
20
+
21
+ const provider = createProvider(
22
+ new SandbankCloudAdapter({
23
+ walletPrivateKey: process.env.WALLET_PRIVATE_KEY,
24
+ })
25
+ )
26
+
27
+ const sandbox = await provider.create({
28
+ image: 'codebox',
29
+ resources: { cpu: 2, memory: 1024 },
30
+ ports: [[0, 7681], [0, 8080]],
31
+ })
32
+
33
+ const { stdout } = await sandbox.exec('node -e "console.log(42)"')
34
+ console.log(stdout) // 42
35
+
36
+ await provider.destroy(sandbox.id)
37
+ ```
38
+
39
+ ### API Token (authenticated access)
40
+
41
+ ```typescript
42
+ const provider = createProvider(
43
+ new SandbankCloudAdapter({
44
+ apiToken: process.env.SANDBANK_API_TOKEN,
45
+ })
46
+ )
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ | Option | Description |
52
+ |--------|-------------|
53
+ | `url` | Sandbank Cloud API URL (default: `https://cloud.sandbank.dev`) |
54
+ | `walletPrivateKey` | EVM wallet private key (hex, `0x` prefix) for x402 USDC payments |
55
+ | `apiToken` | Bearer token for authenticated (internal) access — bypasses x402 |
56
+
57
+ ## Capabilities
58
+
59
+ | Capability | Supported |
60
+ |------------|:---------:|
61
+ | `exec.stream` | ✅ |
62
+ | `port.expose` | ✅ |
63
+
64
+ ## How x402 Payment Works
65
+
66
+ 1. `POST /v1/boxes` returns HTTP 402 with payment requirements
67
+ 2. The adapter signs a USDC payment on Base (eip155:8453) using your wallet
68
+ 3. The request is retried with the payment signature header
69
+ 4. The sandbox is created — $0.02 per sandbox (includes 10 min)
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { AdapterSandbox, Capability, CreateConfig, ListFilter, SandboxAdapter, SandboxInfo } from '@sandbank.dev/core';
2
+ import type { SandbankCloudConfig } from './types.js';
3
+ export declare class SandbankCloudAdapter implements SandboxAdapter {
4
+ readonly name = "sandbank-cloud";
5
+ readonly capabilities: ReadonlySet<Capability>;
6
+ private readonly api;
7
+ constructor(config?: SandbankCloudConfig);
8
+ createSandbox(config: CreateConfig): Promise<AdapterSandbox>;
9
+ getSandbox(id: string): Promise<AdapterSandbox>;
10
+ listSandboxes(filter?: ListFilter): Promise<SandboxInfo[]>;
11
+ destroySandbox(id: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EAGZ,UAAU,EACV,cAAc,EACd,WAAW,EAEZ,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,mBAAmB,EAA6B,MAAM,YAAY,CAAA;AAmIhF,qBAAa,oBAAqB,YAAW,cAAc;IACzD,QAAQ,CAAC,IAAI,oBAAmB;IAChC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAG5C;IAEF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoC;gBAE5C,MAAM,GAAE,mBAAwB;IAItC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAuB5D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAU/C,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB1D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQhD"}
@@ -0,0 +1,196 @@
1
+ import { SandboxNotFoundError, ProviderError } from '@sandbank.dev/core';
2
+ import { createX402Fetch } from './x402-fetch.js';
3
+ function mapState(status) {
4
+ switch (status) {
5
+ case 'running':
6
+ return 'running';
7
+ case 'stopped':
8
+ case 'terminated':
9
+ return 'stopped';
10
+ default:
11
+ return 'error';
12
+ }
13
+ }
14
+ function isNotFound(err) {
15
+ const msg = err instanceof Error ? err.message : String(err);
16
+ return msg.includes('404') || msg.includes('Not found');
17
+ }
18
+ function wrapBox(box, api) {
19
+ const portMap = new Map();
20
+ if (box.ports) {
21
+ for (const [guest, host] of Object.entries(box.ports)) {
22
+ portMap.set(parseInt(guest), host);
23
+ }
24
+ }
25
+ return {
26
+ get id() { return box.id; },
27
+ get state() { return mapState(box.status); },
28
+ get createdAt() { return box.created_at; },
29
+ async exec(command, options) {
30
+ const result = await api.x402Fetch(`/boxes/${box.id}/exec`, {
31
+ method: 'POST',
32
+ body: JSON.stringify({
33
+ cmd: ['bash', '-c', command],
34
+ working_dir: options?.cwd,
35
+ timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
36
+ }),
37
+ });
38
+ return {
39
+ exitCode: result.exit_code,
40
+ stdout: result.stdout ?? '',
41
+ stderr: result.stderr ?? '',
42
+ };
43
+ },
44
+ async execStream(command, options) {
45
+ const result = await api.x402Fetch(`/boxes/${box.id}/exec`, {
46
+ method: 'POST',
47
+ body: JSON.stringify({
48
+ cmd: ['bash', '-c', command],
49
+ working_dir: options?.cwd,
50
+ timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
51
+ }),
52
+ });
53
+ const encoder = new TextEncoder();
54
+ return new ReadableStream({
55
+ start(controller) {
56
+ if (result.stdout)
57
+ controller.enqueue(encoder.encode(result.stdout));
58
+ if (result.stderr)
59
+ controller.enqueue(encoder.encode(result.stderr));
60
+ controller.close();
61
+ },
62
+ });
63
+ },
64
+ async uploadArchive(archive, destDir) {
65
+ let data;
66
+ if (archive instanceof Uint8Array) {
67
+ data = archive;
68
+ }
69
+ else {
70
+ const reader = archive.getReader();
71
+ const chunks = [];
72
+ while (true) {
73
+ const { done, value } = await reader.read();
74
+ if (done)
75
+ break;
76
+ chunks.push(value);
77
+ }
78
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
79
+ data = new Uint8Array(totalLength);
80
+ let offset = 0;
81
+ for (const chunk of chunks) {
82
+ data.set(chunk, offset);
83
+ offset += chunk.length;
84
+ }
85
+ }
86
+ const path = destDir ?? '/';
87
+ const resp = await api.x402FetchRaw(`/boxes/${box.id}/files?path=${encodeURIComponent(path)}`, {
88
+ method: 'PUT',
89
+ headers: { 'Content-Type': 'application/x-tar' },
90
+ body: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
91
+ });
92
+ if (!resp.ok) {
93
+ const body = await resp.text();
94
+ throw new Error(`Upload failed ${resp.status}: ${body}`);
95
+ }
96
+ },
97
+ async downloadArchive(srcDir) {
98
+ const path = srcDir ?? '/';
99
+ const resp = await api.x402FetchRaw(`/boxes/${box.id}/files?path=${encodeURIComponent(path)}`, {
100
+ headers: { 'Accept': 'application/x-tar' },
101
+ });
102
+ if (!resp.ok) {
103
+ const body = await resp.text();
104
+ throw new Error(`Download failed ${resp.status}: ${body}`);
105
+ }
106
+ if (!resp.body)
107
+ throw new Error('No response body');
108
+ return resp.body;
109
+ },
110
+ async exposePort(port) {
111
+ const hostPort = portMap.get(port);
112
+ if (hostPort) {
113
+ try {
114
+ const host = new URL(api.baseUrl).hostname;
115
+ return { url: `http://${host}:${hostPort}` };
116
+ }
117
+ catch { }
118
+ }
119
+ // Fallback to proxy URL
120
+ return { url: `${api.baseUrl}/v1/boxes/${box.id}/proxy/${port}/` };
121
+ },
122
+ };
123
+ }
124
+ export class SandbankCloudAdapter {
125
+ name = 'sandbank-cloud';
126
+ capabilities = new Set([
127
+ 'exec.stream',
128
+ 'port.expose',
129
+ ]);
130
+ api;
131
+ constructor(config = {}) {
132
+ this.api = createX402Fetch(config);
133
+ }
134
+ async createSandbox(config) {
135
+ try {
136
+ const body = {
137
+ image: config.image ?? 'codebox',
138
+ cpu: config.resources?.cpu,
139
+ memory_mb: config.resources?.memory,
140
+ };
141
+ if (config.ports) {
142
+ body.ports = config.ports;
143
+ }
144
+ const box = await this.api.x402Fetch('/boxes', {
145
+ method: 'POST',
146
+ body: JSON.stringify(body),
147
+ });
148
+ return wrapBox(box, this.api);
149
+ }
150
+ catch (err) {
151
+ if (err instanceof ProviderError)
152
+ throw err;
153
+ throw new ProviderError('sandbank-cloud', err);
154
+ }
155
+ }
156
+ async getSandbox(id) {
157
+ try {
158
+ const box = await this.api.x402Fetch(`/boxes/${id}`);
159
+ return wrapBox(box, this.api);
160
+ }
161
+ catch (err) {
162
+ if (isNotFound(err))
163
+ throw new SandboxNotFoundError('sandbank-cloud', id);
164
+ throw new ProviderError('sandbank-cloud', err, id);
165
+ }
166
+ }
167
+ async listSandboxes(filter) {
168
+ try {
169
+ const qs = filter?.state ? `?status=${filter.state}` : '';
170
+ const boxes = await this.api.x402Fetch(`/boxes${qs}`);
171
+ let infos = boxes.map((b) => ({
172
+ id: b.id,
173
+ state: mapState(b.status),
174
+ createdAt: b.created_at,
175
+ image: b.image,
176
+ }));
177
+ if (filter?.limit) {
178
+ infos = infos.slice(0, filter.limit);
179
+ }
180
+ return infos;
181
+ }
182
+ catch (err) {
183
+ throw new ProviderError('sandbank-cloud', err);
184
+ }
185
+ }
186
+ async destroySandbox(id) {
187
+ try {
188
+ await this.api.x402Fetch(`/boxes/${id}`, { method: 'DELETE' });
189
+ }
190
+ catch (err) {
191
+ if (isNotFound(err))
192
+ return;
193
+ throw new ProviderError('sandbank-cloud', err, id);
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,3 @@
1
+ export { SandbankCloudAdapter } from './adapter.js';
2
+ export type { SandbankCloudConfig, CloudBox, CloudExecResult } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @sandbank.dev/cloud — Sandbank Cloud adapter with x402 payment support
2
+ export { SandbankCloudAdapter } from './adapter.js';
@@ -0,0 +1,38 @@
1
+ /** Configuration for SandbankCloudAdapter */
2
+ export interface SandbankCloudConfig {
3
+ /** Sandbank Cloud API URL (default: 'https://cloud.sandbank.dev') */
4
+ url?: string;
5
+ /**
6
+ * EVM wallet private key for x402 payments (hex string with 0x prefix).
7
+ * Required for x402 payment — if omitted, paid endpoints will fail with 402.
8
+ */
9
+ walletPrivateKey?: string;
10
+ /**
11
+ * Bearer API token for authenticated (internal) access.
12
+ * When set, x402 payment is bypassed.
13
+ */
14
+ apiToken?: string;
15
+ }
16
+ /** Sandbank Cloud box response */
17
+ export interface CloudBox {
18
+ id: string;
19
+ name: string | null;
20
+ status: string;
21
+ created_at: string;
22
+ image: string;
23
+ cpu: number;
24
+ memory_mb: number;
25
+ disk_size_gb?: number;
26
+ ports?: Record<string, number>;
27
+ }
28
+ /** Exec response */
29
+ export interface CloudExecResult {
30
+ id: string;
31
+ box_id: string;
32
+ cmd: string[];
33
+ status: string;
34
+ exit_code: number;
35
+ stdout: string;
36
+ stderr: string;
37
+ }
38
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,kCAAkC;AAClC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,oBAAoB;AACpB,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { SandbankCloudConfig } from './types.js';
2
+ /**
3
+ * Create a fetch wrapper that handles x402 payment automatically.
4
+ * On HTTP 402 responses, it parses payment requirements, signs with the wallet,
5
+ * and retries the request with the payment header.
6
+ */
7
+ export declare function createX402Fetch(config: SandbankCloudConfig): {
8
+ x402Fetch: <T>(path: string, options?: RequestInit) => Promise<T>;
9
+ x402FetchRaw: (path: string, options?: RequestInit) => Promise<Response>;
10
+ baseUrl: string;
11
+ };
12
+ //# sourceMappingURL=x402-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"x402-fetch.d.ts","sourceRoot":"","sources":["../src/x402-fetch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,mBAAmB;gBAgBhC,CAAC,QAClB,MAAM,YACH,WAAW,KACnB,OAAO,CAAC,CAAC,CAAC;yBAkEL,MAAM,YACH,WAAW,KACnB,OAAO,CAAC,QAAQ,CAAC;;EAYrB"}
@@ -0,0 +1,85 @@
1
+ import { x402Client, x402HTTPClient } from '@x402/core/client';
2
+ import { registerExactEvmScheme } from '@x402/evm/exact/client';
3
+ import { toClientEvmSigner } from '@x402/evm';
4
+ import { createPublicClient, http } from 'viem';
5
+ import { privateKeyToAccount } from 'viem/accounts';
6
+ import { base } from 'viem/chains';
7
+ /**
8
+ * Create a fetch wrapper that handles x402 payment automatically.
9
+ * On HTTP 402 responses, it parses payment requirements, signs with the wallet,
10
+ * and retries the request with the payment header.
11
+ */
12
+ export function createX402Fetch(config) {
13
+ const baseUrl = (config.url || 'https://cloud.sandbank.dev').replace(/\/$/, '');
14
+ const apiToken = config.apiToken;
15
+ // Setup x402 HTTP client if wallet is provided
16
+ let httpClient = null;
17
+ if (config.walletPrivateKey) {
18
+ const account = privateKeyToAccount(config.walletPrivateKey);
19
+ const publicClient = createPublicClient({ chain: base, transport: http() });
20
+ const signer = toClientEvmSigner(account, publicClient);
21
+ const client = new x402Client();
22
+ registerExactEvmScheme(client, { signer });
23
+ httpClient = new x402HTTPClient(client);
24
+ }
25
+ async function x402Fetch(path, options = {}) {
26
+ const url = `${baseUrl}/v1${path}`;
27
+ const headers = {
28
+ 'Content-Type': 'application/json',
29
+ ...options.headers,
30
+ };
31
+ if (apiToken) {
32
+ headers['Authorization'] = `Bearer ${apiToken}`;
33
+ }
34
+ const response = await fetch(url, { ...options, headers });
35
+ // Handle x402 payment
36
+ if (response.status === 402 && httpClient) {
37
+ const paymentRequired = httpClient.getPaymentRequiredResponse((name) => response.headers.get(name), await response.clone().json().catch(() => undefined));
38
+ // Try hooks first (e.g., cached tokens)
39
+ const hookHeaders = await httpClient.handlePaymentRequired(paymentRequired);
40
+ if (hookHeaders) {
41
+ const retryResp = await fetch(url, {
42
+ ...options,
43
+ headers: { ...headers, ...hookHeaders },
44
+ });
45
+ if (retryResp.ok) {
46
+ const text = await retryResp.text();
47
+ return text ? JSON.parse(text) : {};
48
+ }
49
+ }
50
+ // Create payment payload and retry
51
+ const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
52
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
53
+ const paidResponse = await fetch(url, {
54
+ ...options,
55
+ headers: { ...headers, ...paymentHeaders },
56
+ });
57
+ if (!paidResponse.ok) {
58
+ const body = await paidResponse.text();
59
+ throw new Error(`Sandbank Cloud API error ${paidResponse.status}: ${body}`);
60
+ }
61
+ const text = await paidResponse.text();
62
+ return text ? JSON.parse(text) : {};
63
+ }
64
+ if (response.status === 402) {
65
+ throw new Error('Sandbank Cloud: HTTP 402 Payment Required — provide walletPrivateKey for x402 payment or apiToken for authenticated access');
66
+ }
67
+ if (!response.ok) {
68
+ const body = await response.text();
69
+ throw new Error(`Sandbank Cloud API error ${response.status}: ${body}`);
70
+ }
71
+ const text = await response.text();
72
+ return text ? JSON.parse(text) : {};
73
+ }
74
+ async function x402FetchRaw(path, options = {}) {
75
+ const url = `${baseUrl}/v1${path}`;
76
+ const headers = {
77
+ ...options.headers,
78
+ };
79
+ if (apiToken) {
80
+ headers['Authorization'] = `Bearer ${apiToken}`;
81
+ }
82
+ return fetch(url, { ...options, headers });
83
+ }
84
+ return { x402Fetch, x402FetchRaw, baseUrl };
85
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@sandbank.dev/cloud",
3
+ "version": "0.1.0",
4
+ "description": "Sandbank Cloud adapter with x402 payment support",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://sandbank.dev/cloud",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/chekusu/sandbank.git",
11
+ "directory": "packages/cloud"
12
+ },
13
+ "keywords": [
14
+ "sandbox",
15
+ "ai-agent",
16
+ "sandbank-cloud",
17
+ "bare-metal",
18
+ "kvm",
19
+ "x402",
20
+ "usdc"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "typecheck": "tsc --noEmit",
34
+ "clean": "rm -rf dist"
35
+ },
36
+ "dependencies": {
37
+ "@sandbank.dev/core": "workspace:*",
38
+ "@x402/core": "^2.6.0",
39
+ "@x402/evm": "^2.6.0",
40
+ "viem": "^2.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^25.3.0",
44
+ "typescript": "^5.7.3"
45
+ }
46
+ }