@brimble/sandbox 0.1.0 → 0.1.2

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.
Files changed (61) hide show
  1. package/README.md +9 -5
  2. package/dist/package.json +10 -3
  3. package/dist/src/client.d.ts +3 -3
  4. package/dist/src/client.js +3 -3
  5. package/dist/src/index.d.ts +3 -3
  6. package/dist/src/index.js +2 -2
  7. package/dist/src/resources/files.d.ts +6 -1
  8. package/dist/src/resources/files.js +36 -0
  9. package/dist/src/resources/sandbox-handle.d.ts +6 -1
  10. package/dist/src/resources/sandbox-handle.js +8 -0
  11. package/dist/src/resources/scoped-sandbox.d.ts +3 -1
  12. package/dist/src/resources/scoped-sandbox.js +4 -0
  13. package/dist/src/types/files.d.ts +16 -0
  14. package/dist/src/types/index.d.ts +1 -1
  15. package/package.json +7 -3
  16. package/CODEX.md +0 -188
  17. package/PLAN.md +0 -364
  18. package/src/client.ts +0 -61
  19. package/src/constants.ts +0 -17
  20. package/src/enums/code-language.ts +0 -4
  21. package/src/enums/destroy-reason.ts +0 -8
  22. package/src/enums/destroy-timeout.ts +0 -8
  23. package/src/enums/index.ts +0 -7
  24. package/src/enums/sandbox-status.ts +0 -9
  25. package/src/enums/snapshot-mode.ts +0 -4
  26. package/src/enums/snapshot-status.ts +0 -5
  27. package/src/enums/volume-type.ts +0 -3
  28. package/src/errors/index.ts +0 -2
  29. package/src/errors/sandbox-api-error.ts +0 -54
  30. package/src/index.ts +0 -71
  31. package/src/resources/exec.ts +0 -56
  32. package/src/resources/files.ts +0 -46
  33. package/src/resources/index.ts +0 -8
  34. package/src/resources/path.ts +0 -16
  35. package/src/resources/sandbox-handle.ts +0 -215
  36. package/src/resources/sandboxes.ts +0 -297
  37. package/src/resources/scoped-sandbox.ts +0 -65
  38. package/src/resources/snapshots.ts +0 -104
  39. package/src/resources/stats.ts +0 -30
  40. package/src/resources/volumes.ts +0 -95
  41. package/src/transport/auth.ts +0 -4
  42. package/src/transport/http.ts +0 -501
  43. package/src/transport/pagination.ts +0 -10
  44. package/src/types/exec.ts +0 -42
  45. package/src/types/files.ts +0 -1
  46. package/src/types/index.ts +0 -23
  47. package/src/types/pagination.ts +0 -16
  48. package/src/types/region.ts +0 -19
  49. package/src/types/sandbox.ts +0 -103
  50. package/src/types/snapshot.ts +0 -17
  51. package/src/types/stats.ts +0 -35
  52. package/src/types/template.ts +0 -5
  53. package/src/types/volume.ts +0 -26
  54. package/test/integration/sandbox.integration.test.ts +0 -269
  55. package/test/unit/client.test.ts +0 -87
  56. package/test/unit/sandboxes.test.ts +0 -69
  57. package/test/unit/transport.test.ts +0 -126
  58. package/test/unit/volumes.test.ts +0 -122
  59. package/tsconfig.json +0 -16
  60. package/vitest.config.ts +0 -12
  61. package/vitest.integration.config.ts +0 -15
package/PLAN.md DELETED
@@ -1,364 +0,0 @@
1
- # `@brimble/sandbox` — SDK Plan
2
-
3
- A TypeScript SDK that wraps the Brimble Sandbox API (`https://sandbox.brimble.io`). It exposes two top-level resources — **sandboxes** and **volumes** — plus their nested sub-resources (exec, code, files, stats, snapshots).
4
-
5
- The SDK is the canonical way for first- and third-party clients (Node services, CLIs, CI, the Brimble dashboard) to talk to the Sandbox API. It must work in Node ≥ 20. Browser support is **out of scope for v1**.
6
-
7
- This plan follows the rules in [CODEX.md](../CODEX.md): one responsibility per file, flat logic, no runtime paranoia, no reinvented helpers, no `any`, consistent error handling.
8
-
9
- ---
10
-
11
- ## 1. Goals & non-goals
12
-
13
- ### Goals
14
- - Type-safe wrapper over every route in [`src/routes/v1/sandbox.route.ts`](../src/routes/v1/sandbox.route.ts) and [`src/routes/v1/volume.route.ts`](../src/routes/v1/volume.route.ts).
15
- - Match the OpenAPI contract in [`docs/sandbox-openapi.yaml`](./sandbox-openapi.yaml) exactly — request shape, response shape, status codes, query params.
16
- - One client instance, configured once, used for every call. No per-call URL/auth juggling.
17
- - First-class file streaming (PUT/GET `/files/*`) without buffering whole files in memory.
18
- - Errors surface as a single typed class (`SandboxApiError`) carrying status + server message — never a thrown raw `Response`.
19
- - Zero hidden defaults. The caller chooses retries, timeouts, fetch impl. No magic numbers in source.
20
-
21
- ### Non-goals (v1)
22
- - Browser bundle / `window.fetch` polyfilling.
23
- - Ably real-time subscription helpers — the API events (`sandbox:ready`, `sandbox:paused`, etc.) are documented in the OpenAPI; subscription is the caller's problem for now. Tracked as a v1.1 follow-up.
24
- - HMAC request signing — middleware is a no-op today ([`src/middlewares/hmac.middleware.ts:1`](../src/middlewares/hmac.middleware.ts)). When it's switched on we'll add a `signer` option, not before.
25
- - A CLI. Separate package, separate plan.
26
-
27
- ---
28
-
29
- ## 2. Package layout
30
-
31
- ```
32
- packages/sandbox-sdk/
33
- ├── src/
34
- │ ├── client.ts # SandboxClient — composes resources, owns transport
35
- │ ├── transport/
36
- │ │ ├── http.ts # request() — fetch wrapper, envelope unwrap, error mapping
37
- │ │ ├── auth.ts # buildAuthHeader(token)
38
- │ │ └── pagination.ts # toPaginationQuery({ page, limit })
39
- │ ├── resources/
40
- │ │ ├── sandboxes.ts # SandboxesResource — list/get/create/destroy/pause/resume
41
- │ │ ├── exec.ts # ExecResource — exec, runCode
42
- │ │ ├── files.ts # FilesResource — put/get with streams
43
- │ │ ├── stats.ts # StatsResource — stats
44
- │ │ ├── snapshots.ts # SnapshotsResource — create/list/listAll/delete
45
- │ │ └── volumes.ts # VolumesResource — list/get/create/delete
46
- │ ├── errors/
47
- │ │ ├── sandbox-api-error.ts
48
- │ │ └── index.ts
49
- │ ├── types/
50
- │ │ ├── sandbox.ts # Sandbox, SandboxStatus, SandboxSpecs, CreateSandboxInput…
51
- │ │ ├── snapshot.ts
52
- │ │ ├── volume.ts
53
- │ │ ├── exec.ts
54
- │ │ ├── stats.ts
55
- │ │ ├── pagination.ts
56
- │ │ └── index.ts
57
- │ ├── enums/
58
- │ │ ├── sandbox-status.ts
59
- │ │ ├── destroy-reason.ts
60
- │ │ ├── snapshot-mode.ts
61
- │ │ ├── code-language.ts
62
- │ │ ├── volume-type.ts
63
- │ │ └── index.ts
64
- │ ├── constants.ts # DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, MAX_PAGE_LIMIT…
65
- │ └── index.ts # public re-exports — SandboxClient + every type/enum/error
66
- ├── test/
67
- │ ├── sandboxes.test.ts
68
- │ ├── volumes.test.ts
69
- │ ├── exec.test.ts
70
- │ ├── files.test.ts
71
- │ ├── stats.test.ts
72
- │ ├── snapshots.test.ts
73
- │ ├── errors.test.ts
74
- │ └── fixtures/ # canned API responses
75
- ├── examples/
76
- │ ├── create-and-exec.ts
77
- │ ├── upload-download.ts
78
- │ ├── persistent-with-volume.ts
79
- │ └── snapshot-and-restore.ts
80
- ├── package.json
81
- ├── tsconfig.json
82
- ├── tsup.config.ts # build config (ESM + CJS + d.ts)
83
- ├── vitest.config.ts
84
- └── README.md
85
- ```
86
-
87
- Rationale: one responsibility per file (CODEX §1). Types and enums live in their own folders (CODEX §1, §11). Transport (HTTP, auth, pagination) is independent of business resources so it's testable in isolation.
88
-
89
- ---
90
-
91
- ## 3. Public surface
92
-
93
- The whole SDK is one class. Sub-resources are properties.
94
-
95
- ```ts
96
- import { SandboxClient } from '@brimble/sandbox';
97
-
98
- const brimble = new SandboxClient({
99
- apiKey: process.env.BRIMBLE_API_KEY, // bearer token (JWT or API key)
100
- baseUrl: 'https://sandbox.brimble.io', // optional, defaults to prod
101
- timeoutMs: 30_000, // optional
102
- });
103
-
104
- // Sandboxes
105
- await brimble.sandboxes.list({ page: 1, limit: 15 });
106
- await brimble.sandboxes.get(id);
107
- await brimble.sandboxes.create({ region, template, persistent: true, persistentDiskGB: 20 });
108
- await brimble.sandboxes.destroy(id);
109
- await brimble.sandboxes.pause(id);
110
- await brimble.sandboxes.resume(id);
111
-
112
- // Runtime — nested under .sandboxes(id) for ergonomics, but routes to the same transport
113
- const sb = brimble.sandboxes.scoped(id);
114
- await sb.exec({ cmd: 'ls -la', timeout_seconds: 30 });
115
- await sb.runCode({ language: CodeLanguage.Python, code: 'print(1+1)' });
116
- await sb.files.put('tmp/notes.txt', readable);
117
- const stream = await sb.files.get('tmp/notes.txt');
118
-
119
- // Stats
120
- await sb.stats({ hoursAgo: 6 });
121
-
122
- // Snapshots
123
- await sb.snapshots.create({ name: 'before-migration' });
124
- await sb.snapshots.list({ page: 1, limit: 15 });
125
- await brimble.snapshots.listAll({ page: 1, limit: 50 });
126
- await brimble.snapshots.delete(snapshotId);
127
-
128
- // Volumes
129
- await brimble.volumes.list({ page: 1, limit: 15 });
130
- await brimble.volumes.get(volumeId);
131
- await brimble.volumes.create({ name: 'pg-data', sizeGB: 20, region, type: VolumeType.Database });
132
- await brimble.volumes.delete(volumeId);
133
- ```
134
-
135
- ### Method ↔ route map
136
-
137
- | Method | HTTP | Notes |
138
- |-----------------------------------------------|---------------------------------------------|-------|
139
- | `sandboxes.create(input)` | `POST /sandboxes` | Returns `CreateSandboxResult`. Async transition — caller subscribes to Ably for `sandbox:ready` |
140
- | `sandboxes.list(query)` | `GET /sandboxes` | Paginated |
141
- | `sandboxes.get(id)` | `GET /sandboxes/:id` | |
142
- | `sandboxes.destroy(id)` | `DELETE /sandboxes/:id` | Idempotent, returns `void` on `204` |
143
- | `sandboxes.pause(id)` | `POST /sandboxes/:id/pause` | Persistent only — server enforces |
144
- | `sandboxes.resume(id)` | `POST /sandboxes/:id/resume` | |
145
- | `sb.exec(input)` | `POST /sandboxes/:id/exec` | |
146
- | `sb.runCode(input)` | `POST /sandboxes/:id/code` | |
147
- | `sb.files.put(path, body)` | `PUT /sandboxes/:id/files/{path}` | `body` is `ReadableStream \| Buffer \| Uint8Array`. Streams pass-through; never buffer. |
148
- | `sb.files.get(path)` | `GET /sandboxes/:id/files/{path}` | Returns `ReadableStream` |
149
- | `sb.stats(query)` | `GET /sandboxes/:id/stats` | |
150
- | `sb.snapshots.create(input)` | `POST /sandboxes/:id/snapshots` | `202` accepted |
151
- | `sb.snapshots.list(query)` | `GET /sandboxes/:id/snapshots` | |
152
- | `snapshots.listAll(query)` | `GET /sandboxes/snapshots` | Caller-wide |
153
- | `snapshots.delete(snapshotId)` | `DELETE /sandboxes/snapshots/:snapshotId` | |
154
- | `volumes.list(query)` | `GET /volumes` | |
155
- | `volumes.create(input)` | `POST /volumes` | |
156
- | `volumes.get(volumeId)` | `GET /volumes/:volumeId` | |
157
- | `volumes.delete(volumeId)` | `DELETE /volumes/:volumeId` | |
158
-
159
- Method naming: `delete` is a reserved word but valid as a property name; we use it because consumers will reach for it first. No `_id` suffixes in method names (CODEX §5 — community idioms).
160
-
161
- ---
162
-
163
- ## 4. Transport layer
164
-
165
- A single `request()` function in [`transport/http.ts`](#) does:
166
-
167
- 1. Build the URL (`${baseUrl}${path}`).
168
- 2. Build headers (`Authorization`, `Content-Type`, caller-provided).
169
- 3. Call `fetch` with an `AbortSignal` from `AbortSignal.timeout(timeoutMs)`.
170
- 4. If `response.status === 204` → return `undefined`.
171
- 5. If `response.ok` and the response is JSON → parse, unwrap the `{ message, data }` envelope, return `data`.
172
- 6. If `response.ok` and the response is binary (file download) → return `response.body` (`ReadableStream`).
173
- 7. Otherwise → parse `{ message }`, throw `SandboxApiError`.
174
-
175
- This is the only place that touches `fetch`. Resources call `this.transport.request(...)` and never see the envelope.
176
-
177
- ### Why one envelope-unwrapping path
178
- The OpenAPI guarantees every non-204 JSON response uses `{ message, data }`. Centralising the unwrap means resources return the right shape without each one duplicating the same `(await res.json()).data` line. If the envelope ever changes, one file changes. (CODEX §4 — no defensive paranoia at every call site; CODEX §10 — one error strategy per layer.)
179
-
180
- ### File streaming
181
- - `PUT /files/*`: pass the caller's `ReadableStream | Buffer | Uint8Array` straight to `fetch` as `body`. Set `Content-Type: application/octet-stream`. If the caller passes a `Buffer`, set `Content-Length` so the server can reject oversize uploads before reading the body (per OpenAPI guidance).
182
- - `GET /files/*`: return `response.body`. Caller is responsible for piping / consuming. We **do not** `await response.arrayBuffer()` — that defeats the point.
183
- - Path encoding: the API rejects `%2F`. We split the user's path on `/` and `encodeURIComponent` each segment, then `.join('/')`. Documented in the JSDoc on `files.put` / `files.get`.
184
-
185
- ### Retries
186
- Out of scope for v1. Sandbox state transitions are async and idempotent semantics vary per endpoint (DELETE is documented idempotent; POST is not). A naive retry on `POST /sandboxes` could spin up duplicates. We expose `fetch` as an option so callers who want retries can wrap their own fetch (e.g. `undici`'s retry agent).
187
-
188
- ### Timeouts
189
- Single `timeoutMs` on the client, overridable per-call via a third arg `{ signal }` on every method. Default is **30 s** — exposed as `DEFAULT_TIMEOUT_MS` in `constants.ts` (CODEX §11 — no magic numbers).
190
-
191
- ---
192
-
193
- ## 5. Auth
194
-
195
- Bearer token only. Constructor takes `apiKey` (named that, not `token`, since the route mounting uses `apiKeyMiddleware`). Header is `Authorization: Bearer <apiKey>`.
196
-
197
- The SDK does **not** read environment variables itself. The caller passes the key. (CODEX §15 — no hardcoded env values in source; that includes "automatically read `BRIMBLE_API_KEY`" magic that surprises people.) Examples in `README.md` show the `process.env` pattern at the call site.
198
-
199
- Future: when HMAC signing is enabled server-side, add a `signer?: RequestSigner` option that takes `(req) => headers`. Not implemented in v1.
200
-
201
- ---
202
-
203
- ## 6. Types & enums
204
-
205
- Generated by hand from the OpenAPI for v1. We do **not** ship `openapi-typescript` output — the generated unions are noisy and we want the SDK types to be the source of truth for consumers.
206
-
207
- Every domain type from the OpenAPI gets a `.ts` file in `src/types/`:
208
-
209
- - `Sandbox`, `CreateSandboxInput`, `CreateSandboxResult`, `SandboxSpecs`
210
- - `Snapshot`, `CreateSnapshotInput`
211
- - `Volume`, `CreateVolumeInput`
212
- - `ExecInput`, `ExecResult`, `CodeInput`
213
- - `Stats`, `StatsAverageNumeric`, `StatsAverageNetwork`, `StatsTimelinePoint`
214
- - `Pagination`, `Paginated<T>`
215
-
216
- Every closed string union in the OpenAPI becomes a TypeScript `enum`:
217
-
218
- - `SandboxStatus` (`starting | ready | pausing | paused | resuming | failed | destroyed`)
219
- - `DestroyReason` (`user | idle_ttl | max_lifetime | one_shot_stopped | failed | paused_too_long`)
220
- - `SnapshotMode` (`manual | automatic`)
221
- - `SnapshotStatus` (`creating | ready | failed`)
222
- - `CodeLanguage` (`python | node`)
223
- - `DestroyTimeout` (`30m | 1h | 3h | 6h | 12h | 18h`)
224
- - `VolumeType` (`web | database | sandbox`) — matches [`src/enum/index.ts:171`](../src/enum/index.ts)
225
-
226
- Rule: no inline string literals in resource code. `if (sandbox.status === SandboxStatus.Ready)`, not `=== 'ready'` (CODEX §11).
227
-
228
- Field-naming note: the OpenAPI uses `snake_case` for response fields (`created_at`, `last_activity_at`, `expires_at`, `exit_code`, `duration_ms`). The SDK preserves that — **no auto-camelCasing of response payloads.** A consumer who sees `sandbox.created_at` in the API docs should see the same in TypeScript. Request inputs use whatever case the OpenAPI specifies (a mix — `teamId`, `volumeId`, `persistentDiskGB`, `hoursAgo`, but `timeout_seconds`). We mirror the spec; we don't normalise it.
229
-
230
- ---
231
-
232
- ## 7. Errors
233
-
234
- One class, in [`errors/sandbox-api-error.ts`](#):
235
-
236
- ```ts
237
- export class SandboxApiError extends Error {
238
- readonly status: number;
239
- readonly endpoint: string; // e.g. 'POST /sandboxes/:id/exec'
240
- readonly responseBody: unknown; // parsed JSON or raw text — for debugging
241
-
242
- constructor(args: { status: number; message: string; endpoint: string; responseBody: unknown }) {
243
- super(args.message);
244
- this.name = 'SandboxApiError';
245
- this.status = args.status;
246
- this.endpoint = args.endpoint;
247
- this.responseBody = args.responseBody;
248
- }
249
- }
250
- ```
251
-
252
- That's it. No subclass-per-status (`NotFoundError`, `ForbiddenError`, …) in v1 — consumers branch on `err.status` if they care. We can add tagged subclasses later without breaking callers.
253
-
254
- Network failures and aborts surface as the raw `TypeError` / `AbortError` from `fetch` — we don't wrap them. CODEX §10: don't catch errors only to re-throw them unchanged.
255
-
256
- ---
257
-
258
- ## 8. Pagination
259
-
260
- Helper in `transport/pagination.ts`:
261
-
262
- ```ts
263
- export function toPaginationQuery({ page, limit }: Pagination): URLSearchParams { ... }
264
- ```
265
-
266
- Constants:
267
-
268
- ```ts
269
- export const DEFAULT_PAGE = 1;
270
- export const DEFAULT_PAGE_LIMIT = 15;
271
- export const MAX_PAGE_LIMIT = 100;
272
- ```
273
-
274
- (Matches OpenAPI defaults / caps; CODEX §11.)
275
-
276
- No client-side validation that `limit <= 100` — the server enforces, and re-validating at every boundary is paranoia (CODEX §4). Server returns `400` → consumer gets a clear `SandboxApiError`.
277
-
278
- Optional `auto-paginate` async iterator helpers (`for await (const sandbox of brimble.sandboxes.iterate())`) are nice-to-have but **not in v1**. Tracked as v1.1.
279
-
280
- ---
281
-
282
- ## 9. Build & tooling
283
-
284
- - **Language**: TypeScript, `strict: true`, no `any`, no `as unknown as T` (CODEX §8).
285
- - **Bundler**: `tsup` → `dist/index.js` (CJS), `dist/index.mjs` (ESM), `dist/index.d.ts`. `package.json` declares both via `exports`.
286
- - **Target**: Node 20. We rely on `globalThis.fetch`, `AbortSignal.timeout`, and `ReadableStream` from undici — all GA in Node 20.
287
- - **Lint/format**: ESLint + Prettier with the monorepo defaults (CODEX §5).
288
- - **Tests**: Vitest. `msw` (Mock Service Worker) intercepts `fetch` and serves canned responses from `test/fixtures/`. No live API calls in CI.
289
- - **CI**: GitHub Actions matrix on Node 20 and 22 — lint, typecheck, test, build.
290
- - **Publishing**: `npm publish --access public` to npm under `@brimble`. Semver. Changesets for changelog.
291
-
292
- ---
293
-
294
- ## 10. Testing strategy
295
-
296
- One spec file per resource. Each spec covers:
297
-
298
- 1. **Happy path** — fixture returns the envelope, SDK returns unwrapped `data`. Assert the exact request URL, method, headers, body.
299
- 2. **204 path** — for `destroy`, `delete`, `files.put`. Assert the method returns `undefined`, not `{ message }`.
300
- 3. **Error path** — fixture returns 400/403/404 with `{ message }`. Assert a `SandboxApiError` with the right `status`, `message`, `endpoint`.
301
- 4. **Streaming** — for `files.put` / `files.get`. Use a `Readable` from `node:stream` and assert the body reaches the mocked endpoint un-buffered (msw exposes the raw request).
302
- 5. **Auth header** — every method sends `Authorization: Bearer <key>`.
303
-
304
- Integration tests (live API) are run manually against staging, not in CI — covered in [verify.md](https://example) (out of scope here).
305
-
306
- Coverage target: **90% line, 100% on transport/**. Transport is the load-bearing piece; resources are mostly thin wiring.
307
-
308
- ---
309
-
310
- ## 11. Coding standards (recap against CODEX.md)
311
-
312
- | Rule | How the SDK applies it |
313
- |-------------------------------|------------------------|
314
- | §1 Separation of concerns | One file per resource, per type, per enum. Transport split from resources. |
315
- | §2 No nested ternaries | Use `if/else` in `transport/http.ts` status branching. |
316
- | §3 No `typeof` everywhere | Discriminate `Buffer \| Uint8Array \| ReadableStream` with `body instanceof Readable` / `Buffer.isBuffer`, not `typeof`. |
317
- | §4 No over-engineered guards | No client-side re-validation of OpenAPI patterns. Server is authoritative. |
318
- | §5 Community conventions | ESLint + Prettier, camelCase methods, PascalCase types, kebab-case files. |
319
- | §6 Comments earn their place | Public methods get JSDoc (it's a published SDK). Internal helpers stay uncommented. |
320
- | §7 No reinvention | Use `URL`, `URLSearchParams`, `AbortSignal.timeout`, `globalThis.fetch`. No custom retry / debounce / deepClone. |
321
- | §8 No `any`, no `as unknown as T` | Every public method is fully typed. Internal `JSON.parse` result is typed via a single `parseEnvelope<T>` helper backed by a runtime shape check at the transport edge only. |
322
- | §9 Delete dead code | No "// will be useful later" branches. |
323
- | §10 Consistent errors | One `SandboxApiError` class. No mixed throw / Result / null. |
324
- | §11 No magic numbers | `DEFAULT_TIMEOUT_MS`, `DEFAULT_PAGE_LIMIT`, `MAX_PAGE_LIMIT` in `constants.ts`. |
325
- | §12 Async correctness | Every `fetch` is awaited. No `.then()` chains mixed with `await`. |
326
- | §13 No mutation of args | Resource methods treat input objects as read-only — body is `JSON.stringify(input)`, never `input.foo = …`. |
327
- | §14 Respect existing patterns | Mirror server-side naming (`teamId`, `environmentId`, `persistentDiskGB`) so docs and SDK line up. |
328
- | §15 No hardcoded env values | `apiKey` is required at construction; no `process.env` reads inside the package. |
329
-
330
- ---
331
-
332
- ## 12. Delivery plan
333
-
334
- 1. **Skeleton** — package layout, `tsup` + Vitest + ESLint wired up. README stub. (≈ 0.5 day)
335
- 2. **Transport + auth + errors + pagination** — covered by unit tests with msw. (≈ 1 day)
336
- 3. **Sandboxes resource** — create / list / get / destroy / pause / resume + types/enums. (≈ 1 day)
337
- 4. **Runtime resource** — exec / runCode + types. (≈ 0.5 day)
338
- 5. **Files resource** — put / get with streaming, path-encoding helper, tests. (≈ 1 day)
339
- 6. **Stats resource** — stats. (≈ 0.5 day)
340
- 7. **Snapshots resource** — create / list / listAll / delete. (≈ 0.5 day)
341
- 8. **Volumes resource** — list / create / get / delete. (≈ 0.5 day)
342
- 9. **Examples + README** — 4 runnable example scripts; README with auth, install, quickstart, full API reference auto-linked to OpenAPI sections. (≈ 1 day)
343
- 10. **Manual smoke against staging** — every method, every status path. (≈ 0.5 day)
344
- 11. **Publish v0.1.0** to npm with `--tag next`. Internal teams kick the tires for a week. Promote to `latest` once we've taken feedback and bumped to `v1.0.0`.
345
-
346
- Total: ~7 dev-days. Single owner.
347
-
348
- ---
349
-
350
- ## 13. Open questions
351
-
352
- 1. **Region resolution** — `CreateSandboxInput.region` is a region `_id`. Should the SDK expose a `brimble.regions.list()` helper, even though the OpenAPI doesn't include the regions endpoint? Defer to v1.1 unless someone asks for it.
353
- 2. **Ably channel helper** — if we ship a `brimble.events.subscribe(sandboxId, handler)` helper, do we pull in `ably` as a dep, or expose just the channel name and let callers BYO client? Lean toward the latter — keeps the install slim. Decide before v1.1.
354
- 3. **Browser support** — same SDK or a separate `@brimble/sandbox-browser`? Sandbox API is bearer-token only, which is risky to expose in a browser. Punt until there's a real use case.
355
- 4. **HMAC** — once the server enforces `X-Brimble-Signature`, we'll need a `signer` option. Confirm the signing payload format with platform team before shipping.
356
-
357
- ---
358
-
359
- ## 14. Reference
360
-
361
- - API spec: [`docs/sandbox-openapi.yaml`](./sandbox-openapi.yaml)
362
- - Server routes: [`src/routes/v1/sandbox.route.ts`](../src/routes/v1/sandbox.route.ts), [`src/routes/v1/volume.route.ts`](../src/routes/v1/volume.route.ts)
363
- - Server-side validation (mirror the rules, don't duplicate them client-side): [`src/validations/sandbox.validation.ts`](../src/validations/sandbox.validation.ts), [`src/validations/volume.validation.ts`](../src/validations/volume.validation.ts)
364
- - Coding standards: [`CODEX.md`](../CODEX.md)
package/src/client.ts DELETED
@@ -1,61 +0,0 @@
1
- import { DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, SANDBOX_API_KEY_ENV_NAME } from './constants';
2
- import { SandboxesResource, SnapshotsResource, VolumesResource } from './resources';
3
- import { HttpTransport } from './transport/http';
4
- import type { RetryOptions } from './transport/http';
5
-
6
- export type SandboxClientOptions = {
7
- apiKey?: string;
8
- baseUrl?: string;
9
- timeoutMs?: number;
10
- retry?: RetryOptions;
11
- fetchImpl?: typeof fetch;
12
- };
13
-
14
- /**
15
- * Resolves the API key from the constructor options first,
16
- * then falls back to BRIMBLE_SANDBOX_KEY.
17
- */
18
- function resolveApiKey(options: SandboxClientOptions): string {
19
- if (options.apiKey) {
20
- return options.apiKey;
21
- }
22
-
23
- const apiKeyFromEnv = process.env[SANDBOX_API_KEY_ENV_NAME];
24
-
25
- if (apiKeyFromEnv) {
26
- return apiKeyFromEnv;
27
- }
28
-
29
- throw new Error(
30
- `Sandbox API key is required. Pass "apiKey" explicitly or set ${SANDBOX_API_KEY_ENV_NAME} in your environment.`,
31
- );
32
- }
33
-
34
- export class SandboxClient {
35
- /** Access sandbox lifecycle and per-sandbox scoped operations. */
36
- public readonly sandboxes: SandboxesResource;
37
- /** Access account-level snapshot operations. */
38
- public readonly snapshots: SnapshotsResource;
39
- /** Access volume lifecycle operations. */
40
- public readonly volumes: VolumesResource;
41
-
42
- private readonly transport: HttpTransport;
43
-
44
- /**
45
- * Creates a client instance for the Brimble Sandbox API.
46
- * Pass `apiKey` directly or set `BRIMBLE_SANDBOX_KEY` in your environment.
47
- */
48
- public constructor(options: SandboxClientOptions = {}) {
49
- this.transport = new HttpTransport({
50
- apiKey: resolveApiKey(options),
51
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
52
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
53
- retry: options.retry,
54
- fetchImpl: options.fetchImpl,
55
- });
56
-
57
- this.sandboxes = new SandboxesResource(this.transport);
58
- this.snapshots = new SnapshotsResource(this.transport);
59
- this.volumes = new VolumesResource(this.transport);
60
- }
61
- }
package/src/constants.ts DELETED
@@ -1,17 +0,0 @@
1
- import packageJson from '../package.json';
2
-
3
- export const DEFAULT_BASE_URL = 'https://sandbox.brimble.io';
4
- export const DEFAULT_TIMEOUT_MS = 30_000;
5
- export const SANDBOX_API_KEY_ENV_NAME = 'BRIMBLE_SANDBOX_KEY';
6
- export const SDK_PACKAGE_VERSION = packageJson.version;
7
-
8
- export const DEFAULT_PAGE = 1;
9
- export const DEFAULT_PAGE_LIMIT = 15;
10
- export const MAX_PAGE_LIMIT = 100;
11
- export const MIN_VOLUME_SIZE_GB = 10;
12
- export const DEFAULT_SANDBOX_READY_TIMEOUT_MS = 60_000;
13
- export const DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS = 2_000;
14
- export const DEFAULT_RETRY_MAX_ATTEMPTS = 1;
15
- export const DEFAULT_RETRY_BASE_DELAY_MS = 300;
16
- export const DEFAULT_RETRY_MAX_DELAY_MS = 3_000;
17
- export const DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504] as const;
@@ -1,4 +0,0 @@
1
- export enum CodeLanguage {
2
- Python = 'python',
3
- Node = 'node',
4
- }
@@ -1,8 +0,0 @@
1
- export enum DestroyReason {
2
- User = 'user',
3
- IdleTtl = 'idle_ttl',
4
- MaxLifetime = 'max_lifetime',
5
- OneShotStopped = 'one_shot_stopped',
6
- Failed = 'failed',
7
- PausedTooLong = 'paused_too_long',
8
- }
@@ -1,8 +0,0 @@
1
- export enum DestroyTimeout {
2
- ThirtyMinutes = '30m',
3
- OneHour = '1h',
4
- ThreeHours = '3h',
5
- SixHours = '6h',
6
- TwelveHours = '12h',
7
- EighteenHours = '18h',
8
- }
@@ -1,7 +0,0 @@
1
- export { CodeLanguage } from './code-language';
2
- export { DestroyReason } from './destroy-reason';
3
- export { DestroyTimeout } from './destroy-timeout';
4
- export { SandboxStatus } from './sandbox-status';
5
- export { SnapshotMode } from './snapshot-mode';
6
- export { SnapshotStatus } from './snapshot-status';
7
- export { VolumeType } from './volume-type';
@@ -1,9 +0,0 @@
1
- export enum SandboxStatus {
2
- Starting = 'starting',
3
- Ready = 'ready',
4
- Pausing = 'pausing',
5
- Paused = 'paused',
6
- Resuming = 'resuming',
7
- Failed = 'failed',
8
- Destroyed = 'destroyed',
9
- }
@@ -1,4 +0,0 @@
1
- export enum SnapshotMode {
2
- Manual = 'manual',
3
- Automatic = 'automatic',
4
- }
@@ -1,5 +0,0 @@
1
- export enum SnapshotStatus {
2
- Creating = 'creating',
3
- Ready = 'ready',
4
- Failed = 'failed',
5
- }
@@ -1,3 +0,0 @@
1
- export enum VolumeType {
2
- Sandbox = 'sandbox',
3
- }
@@ -1,2 +0,0 @@
1
- export { AuthError, NotFoundError, RateLimitError, SandboxApiError, ValidationError } from './sandbox-api-error';
2
- export type { SandboxApiErrorArgs } from './sandbox-api-error';
@@ -1,54 +0,0 @@
1
- export type SandboxApiErrorArgs = {
2
- status: number;
3
- message: string;
4
- endpoint: string;
5
- responseBody: unknown;
6
- requestId?: string | null;
7
- };
8
-
9
- export class SandboxApiError extends Error {
10
- public readonly status: number;
11
- public readonly endpoint: string;
12
- public readonly responseBody: unknown;
13
- public readonly requestId: string | null;
14
-
15
- public constructor(args: SandboxApiErrorArgs) {
16
- super(args.message);
17
- this.name = 'SandboxApiError';
18
- this.status = args.status;
19
- this.endpoint = args.endpoint;
20
- this.responseBody = args.responseBody;
21
- this.requestId = args.requestId ?? null;
22
- }
23
- }
24
-
25
- export class AuthError extends SandboxApiError {
26
- public constructor(args: SandboxApiErrorArgs) {
27
- super(args);
28
- this.name = 'AuthError';
29
- }
30
- }
31
-
32
- export class ValidationError extends SandboxApiError {
33
- public constructor(args: SandboxApiErrorArgs) {
34
- super(args);
35
- this.name = 'ValidationError';
36
- }
37
- }
38
-
39
- export class NotFoundError extends SandboxApiError {
40
- public constructor(args: SandboxApiErrorArgs) {
41
- super(args);
42
- this.name = 'NotFoundError';
43
- }
44
- }
45
-
46
- export class RateLimitError extends SandboxApiError {
47
- public readonly retryAfterSeconds: number | null;
48
-
49
- public constructor(args: SandboxApiErrorArgs & { retryAfterSeconds?: number | null }) {
50
- super(args);
51
- this.name = 'RateLimitError';
52
- this.retryAfterSeconds = args.retryAfterSeconds ?? null;
53
- }
54
- }
package/src/index.ts DELETED
@@ -1,71 +0,0 @@
1
- export {
2
- DEFAULT_BASE_URL,
3
- DEFAULT_PAGE,
4
- DEFAULT_PAGE_LIMIT,
5
- DEFAULT_RETRY_BASE_DELAY_MS,
6
- DEFAULT_RETRY_MAX_ATTEMPTS,
7
- DEFAULT_RETRY_MAX_DELAY_MS,
8
- DEFAULT_RETRY_STATUSES,
9
- DEFAULT_TIMEOUT_MS,
10
- MAX_PAGE_LIMIT,
11
- SANDBOX_API_KEY_ENV_NAME,
12
- } from './constants';
13
- export { SandboxClient } from './client';
14
- export type { SandboxClientOptions } from './client';
15
-
16
- export { AuthError, NotFoundError, RateLimitError, SandboxApiError, ValidationError } from './errors';
17
- export type { SandboxApiErrorArgs } from './errors';
18
-
19
- export {
20
- CodeLanguage,
21
- DestroyReason,
22
- DestroyTimeout,
23
- SandboxStatus,
24
- SnapshotMode,
25
- SnapshotStatus,
26
- VolumeType,
27
- } from './enums';
28
-
29
- export {
30
- ExecResource,
31
- FilesResource,
32
- SandboxHandle,
33
- SandboxesResource,
34
- ScopedSandboxResource,
35
- SnapshotScopeResource,
36
- SnapshotsResource,
37
- StatsResource,
38
- VolumesResource,
39
- } from './resources';
40
-
41
- export type {
42
- AckMessage,
43
- CodeInput,
44
- CreateSandboxInput,
45
- CreateSandboxResult,
46
- CreateSnapshotInput,
47
- CreateVolumeInput,
48
- ExecInput,
49
- ExecResult,
50
- ExecStreamFrame,
51
- FileUploadBody,
52
- Paginated,
53
- Pagination,
54
- RegionSummary,
55
- SandboxRegion,
56
- SandboxRegionsResult,
57
- Sandbox,
58
- SandboxSpecs,
59
- Snapshot,
60
- Stats,
61
- StatsAverageNetwork,
62
- StatsAverageNumeric,
63
- StatsQuery,
64
- StatsTimelinePoint,
65
- TeamScopedPagination,
66
- WaitUntilReadyOptions,
67
- Volume,
68
- } from './types';
69
-
70
- export type { RequestOptions } from './transport/http';
71
- export type { RetryOptions } from './transport/http';