@brimble/sandbox 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.
Files changed (121) hide show
  1. package/CODEX.md +188 -0
  2. package/PLAN.md +364 -0
  3. package/README.md +147 -0
  4. package/dist/package.json +23 -0
  5. package/dist/src/client.d.ts +23 -0
  6. package/dist/src/client.js +46 -0
  7. package/dist/src/constants.d.ts +14 -0
  8. package/dist/src/constants.js +21 -0
  9. package/dist/src/enums/code-language.d.ts +4 -0
  10. package/dist/src/enums/code-language.js +8 -0
  11. package/dist/src/enums/destroy-reason.d.ts +8 -0
  12. package/dist/src/enums/destroy-reason.js +12 -0
  13. package/dist/src/enums/destroy-timeout.d.ts +8 -0
  14. package/dist/src/enums/destroy-timeout.js +12 -0
  15. package/dist/src/enums/index.d.ts +7 -0
  16. package/dist/src/enums/index.js +17 -0
  17. package/dist/src/enums/sandbox-status.d.ts +9 -0
  18. package/dist/src/enums/sandbox-status.js +13 -0
  19. package/dist/src/enums/snapshot-mode.d.ts +4 -0
  20. package/dist/src/enums/snapshot-mode.js +8 -0
  21. package/dist/src/enums/snapshot-status.d.ts +5 -0
  22. package/dist/src/enums/snapshot-status.js +9 -0
  23. package/dist/src/enums/volume-type.d.ts +3 -0
  24. package/dist/src/enums/volume-type.js +7 -0
  25. package/dist/src/errors/index.d.ts +2 -0
  26. package/dist/src/errors/index.js +9 -0
  27. package/dist/src/errors/sandbox-api-error.d.ts +29 -0
  28. package/dist/src/errors/sandbox-api-error.js +48 -0
  29. package/dist/src/index.d.ts +10 -0
  30. package/dist/src/index.js +40 -0
  31. package/dist/src/resources/exec.d.ts +19 -0
  32. package/dist/src/resources/exec.js +45 -0
  33. package/dist/src/resources/files.d.ts +16 -0
  34. package/dist/src/resources/files.js +41 -0
  35. package/dist/src/resources/index.d.ts +8 -0
  36. package/dist/src/resources/index.js +20 -0
  37. package/dist/src/resources/path.d.ts +7 -0
  38. package/dist/src/resources/path.js +19 -0
  39. package/dist/src/resources/sandbox-handle.d.ts +78 -0
  40. package/dist/src/resources/sandbox-handle.js +151 -0
  41. package/dist/src/resources/sandboxes.d.ts +64 -0
  42. package/dist/src/resources/sandboxes.js +224 -0
  43. package/dist/src/resources/scoped-sandbox.d.ts +39 -0
  44. package/dist/src/resources/scoped-sandbox.js +51 -0
  45. package/dist/src/resources/snapshots.d.ts +26 -0
  46. package/dist/src/resources/snapshots.js +88 -0
  47. package/dist/src/resources/stats.d.ts +11 -0
  48. package/dist/src/resources/stats.js +26 -0
  49. package/dist/src/resources/volumes.d.ts +21 -0
  50. package/dist/src/resources/volumes.js +80 -0
  51. package/dist/src/transport/auth.d.ts +2 -0
  52. package/dist/src/transport/auth.js +7 -0
  53. package/dist/src/transport/http.d.ts +73 -0
  54. package/dist/src/transport/http.js +354 -0
  55. package/dist/src/transport/pagination.d.ts +3 -0
  56. package/dist/src/transport/pagination.js +11 -0
  57. package/dist/src/types/exec.d.ts +34 -0
  58. package/dist/src/types/exec.js +2 -0
  59. package/dist/src/types/files.d.ts +1 -0
  60. package/dist/src/types/files.js +2 -0
  61. package/dist/src/types/index.d.ts +9 -0
  62. package/dist/src/types/index.js +2 -0
  63. package/dist/src/types/pagination.d.ts +14 -0
  64. package/dist/src/types/pagination.js +2 -0
  65. package/dist/src/types/region.d.ts +17 -0
  66. package/dist/src/types/region.js +2 -0
  67. package/dist/src/types/sandbox.d.ts +90 -0
  68. package/dist/src/types/sandbox.js +2 -0
  69. package/dist/src/types/snapshot.d.ts +15 -0
  70. package/dist/src/types/snapshot.js +2 -0
  71. package/dist/src/types/stats.d.ts +31 -0
  72. package/dist/src/types/stats.js +2 -0
  73. package/dist/src/types/template.d.ts +5 -0
  74. package/dist/src/types/template.js +2 -0
  75. package/dist/src/types/volume.d.ts +24 -0
  76. package/dist/src/types/volume.js +2 -0
  77. package/package.json +26 -0
  78. package/src/client.ts +61 -0
  79. package/src/constants.ts +17 -0
  80. package/src/enums/code-language.ts +4 -0
  81. package/src/enums/destroy-reason.ts +8 -0
  82. package/src/enums/destroy-timeout.ts +8 -0
  83. package/src/enums/index.ts +7 -0
  84. package/src/enums/sandbox-status.ts +9 -0
  85. package/src/enums/snapshot-mode.ts +4 -0
  86. package/src/enums/snapshot-status.ts +5 -0
  87. package/src/enums/volume-type.ts +3 -0
  88. package/src/errors/index.ts +2 -0
  89. package/src/errors/sandbox-api-error.ts +54 -0
  90. package/src/index.ts +71 -0
  91. package/src/resources/exec.ts +56 -0
  92. package/src/resources/files.ts +46 -0
  93. package/src/resources/index.ts +8 -0
  94. package/src/resources/path.ts +16 -0
  95. package/src/resources/sandbox-handle.ts +215 -0
  96. package/src/resources/sandboxes.ts +297 -0
  97. package/src/resources/scoped-sandbox.ts +65 -0
  98. package/src/resources/snapshots.ts +104 -0
  99. package/src/resources/stats.ts +30 -0
  100. package/src/resources/volumes.ts +95 -0
  101. package/src/transport/auth.ts +4 -0
  102. package/src/transport/http.ts +501 -0
  103. package/src/transport/pagination.ts +10 -0
  104. package/src/types/exec.ts +42 -0
  105. package/src/types/files.ts +1 -0
  106. package/src/types/index.ts +23 -0
  107. package/src/types/pagination.ts +16 -0
  108. package/src/types/region.ts +19 -0
  109. package/src/types/sandbox.ts +103 -0
  110. package/src/types/snapshot.ts +17 -0
  111. package/src/types/stats.ts +35 -0
  112. package/src/types/template.ts +5 -0
  113. package/src/types/volume.ts +26 -0
  114. package/test/integration/sandbox.integration.test.ts +269 -0
  115. package/test/unit/client.test.ts +87 -0
  116. package/test/unit/sandboxes.test.ts +69 -0
  117. package/test/unit/transport.test.ts +126 -0
  118. package/test/unit/volumes.test.ts +122 -0
  119. package/tsconfig.json +16 -0
  120. package/vitest.config.ts +12 -0
  121. package/vitest.integration.config.ts +15 -0
package/CODEX.md ADDED
@@ -0,0 +1,188 @@
1
+ # Coding Guidelines
2
+
3
+ These are the rules you follow when writing or modifying code in this codebase. Read them once, apply them always.
4
+
5
+ ## 1. Separation of concerns
6
+
7
+ Keep functions, types, enums, and classes in their own files (or at minimum, their own clearly-scoped sections). Do not mix concerns.
8
+
9
+ - One responsibility per unit. A function does one thing; a class models one concept; a type describes one shape.
10
+ - Types and enums live in dedicated files (`types.ts`, `enums.ts`, or a `types/` / `enums/` folder) unless they are trivially local to a single consumer.
11
+ - Do not co-locate unrelated helpers inside a class or module just because they happen to touch the same data.
12
+ - If a file starts doing two jobs, split it.
13
+
14
+ ## 2. Ternaries: short and flat, or not at all
15
+
16
+ Use a ternary only when it is short, single-line, and obvious at a glance.
17
+
18
+ ```ts
19
+ // Good
20
+ const label = isActive ? 'on' : 'off';
21
+
22
+ // Bad — nested
23
+ const label = isActive ? (isAdmin ? 'admin-on' : 'user-on') : isAdmin ? 'admin-off' : 'user-off';
24
+ ```
25
+
26
+ No nested ternaries. Ever. If branching gets past one level, use `if`/`else` or extract a function. Readability wins over cleverness.
27
+
28
+ ## 3. Stop using `typeof` everywhere
29
+
30
+ `typeof` is a narrowing tool, not a default check. Do not reach for it when a simple truthy/falsy check is enough.
31
+
32
+ ```ts
33
+ // Good
34
+ if (!user) return;
35
+ if (value) process(value);
36
+
37
+ // Bad
38
+ if (typeof user !== "undefined" && user !== null) { ... }
39
+ if (typeof value === "string" && value.length > 0) { ... } // when you just need truthiness
40
+ ```
41
+
42
+ Use `typeof` only when you genuinely need to discriminate primitive types (e.g. inside a union where `string | number` matters). Otherwise, trust truthiness, optional chaining, and nullish coalescing.
43
+
44
+ ## 4. No over-engineered guard rails
45
+
46
+ Write defenses for realistic failure modes, not hypothetical ones.
47
+
48
+ - Validate at system boundaries (HTTP input, DB reads, external APIs). Do not re-validate the same data at every internal function call.
49
+ - No `assert` chains at the top of ever4y function.
50
+ - No wrapping every call in try/catch "just in case" — let errors bubble to a single handler that knows what to do with them.
51
+ - If a guard clause is protecting against a case that cannot happen given the types, delete it.
52
+ - Prefer types and schemas over runtime paranoia.
53
+
54
+ The goal is correctness, not defensiveness theatre.
55
+
56
+ ## 5. Follow popular conventions
57
+
58
+ Use the community standard for the language and ecosystem. Do not invent personal styles.
59
+
60
+ - **TypeScript/JavaScript**: ESLint + Prettier defaults, camelCase for variables/functions, PascalCase for types/classes, `kebab-case` or `camelCase` file names consistent with the project.
61
+ - **Go**: `gofmt`, idiomatic error handling (`if err != nil`), package names short and lowercase, no stutter.
62
+ - **Python**: PEP 8, `ruff`/`black` formatting, `snake_case`.
63
+ - Match the project's existing style before imposing anything new. When in doubt, grep the codebase and do what it already does.
64
+
65
+ ## 6. Comments: only when they add something
66
+
67
+ Code should read itself. Comments are for things the code cannot say.
68
+
69
+ Write a comment when:
70
+
71
+ - The _why_ is non-obvious (business rule, workaround for an upstream bug, performance-sensitive choice).
72
+ - A public API needs a docstring for consumers.
73
+ - A `TODO` / `FIXME` with context and ownership.
74
+
75
+ Do not write a comment when:
76
+
77
+ - It restates the code (`// increment counter` above `counter++`).
78
+ - It narrates obvious control flow (`// loop through users`).
79
+ - It was auto-generated boilerplate that nobody reads.
80
+
81
+ Delete stale comments on sight. A wrong comment is worse than no comment.
82
+
83
+ ## 7. Don't re-invent what already exists
84
+
85
+ Before writing a helper, check:
86
+
87
+ 1. The language's stdlib.
88
+ 2. The framework the project uses.
89
+ 3. Utilities already in this codebase.
90
+
91
+ Do not write a custom `debounce`, `deepClone`, `groupBy`, UUID generator, date parser, retry loop, or config loader when a well-tested one is already available. Do not introduce a second HTTP client, a second logger, or a second error class when the repo already has one — use what's there.
92
+
93
+ The only reasons to roll your own: the existing option is genuinely insufficient, or pulling it in costs more than writing it.
94
+
95
+ ## 8. No `any`, no `as unknown as T`
96
+
97
+ When types get annoying, fix the type — don't escape it.
98
+
99
+ - `any` is banned except at genuine interop edges (and even then, narrow immediately).
100
+ - `as` casts are a last resort. If you're writing `as unknown as T`, the real answer is usually a type guard, a discriminated union, or a schema parse (Zod, etc.).
101
+ - `// @ts-ignore` and `// @ts-expect-error` require a comment explaining why.
102
+
103
+ The type system earns its keep when you respect it.
104
+
105
+ ## 9. Delete dead code
106
+
107
+ - No commented-out blocks "for reference." Git remembers.
108
+ - No unused imports, unused variables, unused parameters (prefix with `_` if the signature forces it).
109
+ - No stale feature flags still wired up months after launch.
110
+ - No "v2" helper sitting next to the original with no caller.
111
+
112
+ If it isn't used, it leaves.
113
+
114
+ ## 10. Consistent error handling
115
+
116
+ Pick one error strategy per layer and stick to it.
117
+
118
+ - Don't mix `throw`, `Result<T, E>`, `null` returns, and `{ error, data }` tuples in the same module.
119
+ - Don't catch errors only to re-throw them unchanged — that's noise.
120
+ - Don't swallow errors with empty `catch {}`. If you genuinely want to ignore one, log it or leave a comment saying why.
121
+ - Handle errors at the layer that knows what to do with them (usually the HTTP handler or job runner), not at every function on the way down.
122
+
123
+ ## 11. No magic numbers or magic strings
124
+
125
+ Named constants and enums beat literals scattered through the code.
126
+
127
+ ```ts
128
+ // Bad
129
+ setTimeout(retry, 86400000);
130
+ if (user.status === "active") { ... }
131
+
132
+ // Good
133
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
134
+ setTimeout(retry, ONE_DAY_MS);
135
+ if (user.status === UserStatus.Active) { ... }
136
+ ```
137
+
138
+ If a value appears more than once, or its meaning isn't obvious from the number/string alone, name it.
139
+
140
+ ## 12. Async correctness
141
+
142
+ - Always `await` or explicitly handle the promise. No dangling promises.
143
+ - Parallelize independent work with `Promise.all`; don't serialize it in a `for` loop by accident.
144
+ - Don't mix `.then()` chains and `await` in the same function — pick one.
145
+ - Handle rejections. An unhandled rejection in production is a bug waiting to page you.
146
+
147
+ ## 13. Don't mutate function arguments
148
+
149
+ Treat inputs as read-only. If you need a modified version, return a new one.
150
+
151
+ ```ts
152
+ // Bad
153
+ function addTag(user, tag) {
154
+ user.tags.push(tag);
155
+ return user;
156
+ }
157
+
158
+ // Good
159
+ function addTag(user, tag) {
160
+ return { ...user, tags: [...user.tags, tag] };
161
+ }
162
+ ```
163
+
164
+ Hidden mutation through a function call is one of the worst bugs to trace in a large system.
165
+
166
+ ## 14. Respect the codebase's existing patterns
167
+
168
+ Before introducing something new, look around:
169
+
170
+ - If the project uses a specific HTTP client, logger, error class, config loader, or validation library — use it.
171
+ - If there's an established folder structure, follow it.
172
+ - If there's a naming convention in neighboring files, match it.
173
+
174
+ Don't bring in a new dependency or pattern just because it's what you reached for first. Consistency across the codebase is worth more than any individual preference.
175
+
176
+ ## 15. No hardcoded environment values
177
+
178
+ URLs, ports, credentials, feature flags, and tunable limits belong in config or environment variables — not in source.
179
+
180
+ - Secrets never land in the repo. Not even temporarily.
181
+ - Defaults for local dev are fine in a `.env.example` or config file, but the real values are injected.
182
+ - If a value differs between staging and prod, it's config.
183
+
184
+ ---
185
+
186
+ ## Summary
187
+
188
+ Clean separation, flat logic, minimal runtime paranoia, community idioms, and comments only where they earn their place. Reuse what exists, respect the type system, delete what's dead, handle errors consistently, and keep configuration out of source. If a change adds complexity without earning it, reject the change.
package/PLAN.md ADDED
@@ -0,0 +1,364 @@
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)