@miosa/sdk 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +181 -0
  2. package/dist/index.d.ts +4689 -0
  3. package/dist/index.js +6045 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +63 -0
  6. package/src/client.ts +249 -0
  7. package/src/errors.ts +136 -0
  8. package/src/http.test.ts +374 -0
  9. package/src/http.ts +390 -0
  10. package/src/index.ts +452 -0
  11. package/src/resources/admin.ts +348 -0
  12. package/src/resources/analytics.ts +60 -0
  13. package/src/resources/api-keys.ts +119 -0
  14. package/src/resources/audit-log.ts +64 -0
  15. package/src/resources/benchmarks.ts +104 -0
  16. package/src/resources/builder-sessions.ts +75 -0
  17. package/src/resources/channels.ts +143 -0
  18. package/src/resources/checkpoints.ts +225 -0
  19. package/src/resources/command-center.ts +73 -0
  20. package/src/resources/community.ts +103 -0
  21. package/src/resources/completions.ts +104 -0
  22. package/src/resources/computer-auto-stop.ts +43 -0
  23. package/src/resources/computer-env.ts +76 -0
  24. package/src/resources/computer-logs.ts +50 -0
  25. package/src/resources/computer-osa.ts +76 -0
  26. package/src/resources/computer-ports.ts +91 -0
  27. package/src/resources/computer-terminal.ts +61 -0
  28. package/src/resources/computer-volumes.ts +64 -0
  29. package/src/resources/computer.ts +530 -0
  30. package/src/resources/computers.ts +75 -0
  31. package/src/resources/credits.ts +43 -0
  32. package/src/resources/cron-jobs.ts +191 -0
  33. package/src/resources/custom_domains.ts +123 -0
  34. package/src/resources/dashboard.ts +49 -0
  35. package/src/resources/databases.ts +218 -0
  36. package/src/resources/deployments.ts +777 -0
  37. package/src/resources/desktop.ts +134 -0
  38. package/src/resources/email.ts +212 -0
  39. package/src/resources/embeddings.ts +36 -0
  40. package/src/resources/events.ts +296 -0
  41. package/src/resources/exec.ts +319 -0
  42. package/src/resources/external-keys.ts +79 -0
  43. package/src/resources/files.test.ts +339 -0
  44. package/src/resources/files.ts +220 -0
  45. package/src/resources/flat-custom-domains.ts +127 -0
  46. package/src/resources/functions.ts +178 -0
  47. package/src/resources/health-checks.ts +165 -0
  48. package/src/resources/integrations.ts +183 -0
  49. package/src/resources/mcp.ts +70 -0
  50. package/src/resources/models.ts +45 -0
  51. package/src/resources/network_policy.ts +73 -0
  52. package/src/resources/open-computers/agents.ts +91 -0
  53. package/src/resources/open-computers/apps.ts +102 -0
  54. package/src/resources/open-computers/clusters.ts +88 -0
  55. package/src/resources/open-computers/desktop.ts +34 -0
  56. package/src/resources/open-computers/files.ts +97 -0
  57. package/src/resources/open-computers/hosts.ts +85 -0
  58. package/src/resources/open-computers/index.ts +98 -0
  59. package/src/resources/open-computers/jobs.ts +75 -0
  60. package/src/resources/open-computers/open_computers.test.ts +288 -0
  61. package/src/resources/open-computers/secrets.ts +115 -0
  62. package/src/resources/open-computers/terminal.ts +33 -0
  63. package/src/resources/open-computers/tunnels.ts +87 -0
  64. package/src/resources/open-computers/types.ts +343 -0
  65. package/src/resources/open-computers/workspaces.ts +135 -0
  66. package/src/resources/project-auth.ts +142 -0
  67. package/src/resources/project-integrations.ts +133 -0
  68. package/src/resources/provider-defaults.ts +89 -0
  69. package/src/resources/regions.ts +94 -0
  70. package/src/resources/sandbox-templates.ts +195 -0
  71. package/src/resources/sandboxes.live.test.ts +92 -0
  72. package/src/resources/sandboxes.test.ts +624 -0
  73. package/src/resources/sandboxes.ts +1173 -0
  74. package/src/resources/settings.ts +143 -0
  75. package/src/resources/snapshots-standalone.ts +51 -0
  76. package/src/resources/storage.ts +221 -0
  77. package/src/resources/tenant.ts +39 -0
  78. package/src/resources/usage.ts +85 -0
  79. package/src/resources/volumes.ts +117 -0
  80. package/src/resources/webhooks.ts +171 -0
  81. package/src/types.ts +460 -0
@@ -0,0 +1,1173 @@
1
+ import { MiosaError } from "../errors.js";
2
+ import { HttpClient } from "../http.js";
3
+
4
+ export const SANDBOX_TEMPLATE = "miosa-sandbox";
5
+
6
+ export type SandboxId = string & { readonly __brand: "SandboxId" };
7
+ export type SandboxState =
8
+ | "provisioning"
9
+ | "running"
10
+ | "paused"
11
+ | "destroyed"
12
+ | "error";
13
+
14
+ export interface SandboxCreateParams {
15
+ templateId?: string;
16
+ template_id?: string;
17
+ image?: string;
18
+ cpuCount?: number;
19
+ cpu_count?: number;
20
+ memoryMb?: number;
21
+ memory_mb?: number;
22
+ diskMb?: number;
23
+ disk_mb?: number;
24
+ diskSizeMb?: number;
25
+ disk_size_mb?: number;
26
+ timeoutSec?: number;
27
+ timeout_sec?: number;
28
+ idleTimeoutSec?: number;
29
+ idle_timeout_sec?: number;
30
+ alwaysOn?: boolean;
31
+ always_on?: boolean;
32
+ env?: Record<string, string>;
33
+ metadata?: Record<string, unknown>;
34
+ services?: Array<Record<string, unknown>>;
35
+ readinessProbe?: Record<string, unknown>;
36
+ readiness_probe?: Record<string, unknown>;
37
+ database?: Record<string, unknown> | boolean;
38
+ githubRepoUrl?: string;
39
+ github_repo_url?: string;
40
+ githubBranch?: string;
41
+ github_branch?: string;
42
+ githubClonePath?: string;
43
+ github_clone_path?: string;
44
+ name?: string;
45
+ region?: string;
46
+ entrypoint?: string;
47
+ tags?: string[];
48
+ idempotencyKey?: string;
49
+ idempotency_key?: string;
50
+ // White-label attribution. See platform/attribution docs.
51
+ externalWorkspaceId?: string;
52
+ external_workspace_id?: string;
53
+ externalUserId?: string;
54
+ external_user_id?: string;
55
+ externalProjectId?: string;
56
+ external_project_id?: string;
57
+ }
58
+
59
+ export interface SandboxListParams {
60
+ state?: SandboxState | string;
61
+ tags?: string[];
62
+ externalWorkspaceId?: string;
63
+ external_workspace_id?: string;
64
+ externalUserId?: string;
65
+ external_user_id?: string;
66
+ externalProjectId?: string;
67
+ external_project_id?: string;
68
+ }
69
+
70
+ export interface SandboxExecOptions {
71
+ cwd?: string;
72
+ workingDir?: string;
73
+ working_dir?: string;
74
+ env?: Record<string, string>;
75
+ timeout?: number;
76
+ timeoutSec?: number;
77
+ timeout_sec?: number;
78
+ }
79
+
80
+ export interface SandboxExecResult {
81
+ stdout: string;
82
+ stderr: string;
83
+ exitCode: number;
84
+ exit_code: number;
85
+ durationMs?: number;
86
+ duration_ms?: number;
87
+ }
88
+
89
+ export type SandboxExecEvent =
90
+ | { type?: "stdout"; line: string }
91
+ | { type?: "stderr"; line: string }
92
+ | { type?: "exit"; exit_code: number; exitCode?: number }
93
+ | Record<string, unknown>;
94
+
95
+ export interface SandboxExecRunner {
96
+ (command: string, options?: SandboxExecOptions): Promise<SandboxExecResult>;
97
+ run(
98
+ command: string,
99
+ options?: SandboxExecOptions,
100
+ ): Promise<SandboxExecResult>;
101
+ stream(
102
+ command: string,
103
+ options?: SandboxExecOptions,
104
+ ): AsyncIterableIterator<SandboxExecEvent>;
105
+ }
106
+
107
+ export interface SandboxData {
108
+ id: SandboxId;
109
+ state: SandboxState;
110
+ ready?: boolean;
111
+ template_id?: string;
112
+ image_id?: string | null;
113
+ cpu_count?: number | null;
114
+ memory_mb?: number | null;
115
+ disk_mb?: number | null;
116
+ disk_size_mb?: number | null;
117
+ timeout_sec?: number | null;
118
+ boot_path?: string | null;
119
+ boot_ms?: number | null;
120
+ ready_at?: string | null;
121
+ preview_url?: string | null;
122
+ metadata?: Record<string, unknown>;
123
+ inserted_at?: string;
124
+ created_at?: string;
125
+ started_at?: string | null;
126
+ destroyed_at?: string | null;
127
+ total_runtime_sec?: number | null;
128
+ }
129
+
130
+ export interface SandboxTemplate {
131
+ id: string;
132
+ name?: string;
133
+ slug?: string;
134
+ description?: string;
135
+ image_id?: string;
136
+ built_in?: boolean;
137
+ status?: string;
138
+ preview_port?: number | null;
139
+ workdir?: string;
140
+ install_command?: string | null;
141
+ start_command?: string | null;
142
+ readiness_probe?: Record<string, unknown> | null;
143
+ artifact_paths?: string[];
144
+ runtimes?: string[];
145
+ tags?: string[];
146
+ build_spec?: Record<string, unknown>;
147
+ current_build_id?: string | null;
148
+ }
149
+
150
+ export interface SandboxTemplateList {
151
+ data: SandboxTemplate[];
152
+ default_template_id?: string;
153
+ }
154
+
155
+ export interface SandboxBuildSpec {
156
+ from: string;
157
+ vcpu?: number;
158
+ memoryMib?: number;
159
+ diskMib?: number;
160
+ steps?: Array<Record<string, unknown>>;
161
+ env?: Record<string, string>;
162
+ workdir?: string;
163
+ user?: string;
164
+ startCmd?: string;
165
+ readyCmd?: string;
166
+ previewPort?: number;
167
+ artifactPaths?: string[];
168
+ }
169
+
170
+ export interface SandboxBuildSpecError {
171
+ code: string;
172
+ field: string;
173
+ message: string;
174
+ }
175
+
176
+ export interface SandboxBuildSpecValidation {
177
+ valid: boolean;
178
+ build_spec?: Record<string, unknown>;
179
+ errors?: SandboxBuildSpecError[];
180
+ }
181
+
182
+ export interface SandboxTemplateCreateParams {
183
+ name: string;
184
+ buildSpec?: SandboxBuildSpec;
185
+ build_spec?: SandboxBuildSpec;
186
+ slug?: string;
187
+ description?: string;
188
+ metadata?: Record<string, unknown>;
189
+ }
190
+
191
+ export interface SandboxTemplateBuildCreateParams {
192
+ buildSpec?: SandboxBuildSpec;
193
+ build_spec?: SandboxBuildSpec;
194
+ metadata?: Record<string, unknown>;
195
+ }
196
+
197
+ export interface SandboxTemplateBuild {
198
+ id: string;
199
+ sandbox_template_id: string;
200
+ source_type: "build_spec" | string;
201
+ state:
202
+ | "queued"
203
+ | "building"
204
+ | "certifying"
205
+ | "snapshotting"
206
+ | "ready"
207
+ | "failed"
208
+ | "cancelled";
209
+ image_id?: string | null;
210
+ rootfs_path?: string | null;
211
+ snapshot_manifest?: Record<string, unknown>;
212
+ log_url?: string | null;
213
+ error_code?: string | null;
214
+ error_message?: string | null;
215
+ started_at?: string | null;
216
+ finished_at?: string | null;
217
+ duration_ms?: number | null;
218
+ build_spec?: Record<string, unknown>;
219
+ inserted_at?: string;
220
+ updated_at?: string;
221
+ }
222
+
223
+ export interface SandboxFileEntry {
224
+ name: string;
225
+ path: string;
226
+ size?: number;
227
+ is_dir?: boolean;
228
+ isDir?: boolean;
229
+ modified_at?: string;
230
+ modifiedAt?: string;
231
+ [key: string]: unknown;
232
+ }
233
+
234
+ export interface SandboxFileList {
235
+ path?: string;
236
+ entries: SandboxFileEntry[];
237
+ }
238
+
239
+ export interface SandboxFileStat {
240
+ path: string;
241
+ size?: number;
242
+ is_dir?: boolean;
243
+ isDir?: boolean;
244
+ mode?: string;
245
+ modified_at?: string;
246
+ modifiedAt?: string;
247
+ [key: string]: unknown;
248
+ }
249
+
250
+ export interface SandboxSnapshot {
251
+ id: string;
252
+ sandbox_id?: string;
253
+ status?: string;
254
+ comment?: string;
255
+ created_at?: string;
256
+ updated_at?: string;
257
+ [key: string]: unknown;
258
+ }
259
+
260
+ export interface SandboxDeployParams {
261
+ name?: string;
262
+ deploymentId?: string;
263
+ deployment_id?: string;
264
+ path?: string;
265
+ sourcePath?: string;
266
+ source_path?: string;
267
+ outputPath?: string;
268
+ output_path?: string;
269
+ sourceSnapshotPath?: string;
270
+ source_snapshot_path?: string;
271
+ entrypoint?: string;
272
+ domain?: string;
273
+ customDomain?: string;
274
+ custom_domain?: string;
275
+ idempotencyKey?: string;
276
+ idempotency_key?: string;
277
+ }
278
+
279
+ type WireEnvelope<T> = T | { data: T };
280
+
281
+ function unwrap<T>(payload: WireEnvelope<T>): T {
282
+ if (
283
+ payload !== null &&
284
+ typeof payload === "object" &&
285
+ "data" in payload &&
286
+ (payload as { data?: unknown }).data !== undefined
287
+ ) {
288
+ return (payload as { data: T }).data;
289
+ }
290
+ return payload as T;
291
+ }
292
+
293
+ function listItems(
294
+ payload:
295
+ | WireEnvelope<SandboxData[]>
296
+ | { sandboxes: SandboxData[] }
297
+ | { items: SandboxData[] },
298
+ ): SandboxData[] {
299
+ if (Array.isArray(payload)) return payload;
300
+ if ("data" in payload && Array.isArray(payload.data)) return payload.data;
301
+ if ("sandboxes" in payload && Array.isArray(payload.sandboxes))
302
+ return payload.sandboxes;
303
+ if ("items" in payload && Array.isArray(payload.items)) return payload.items;
304
+ return [];
305
+ }
306
+
307
+ function createBody(params: SandboxCreateParams = {}): Record<string, unknown> {
308
+ const templateId =
309
+ params.templateId ?? params.template_id ?? params.image ?? SANDBOX_TEMPLATE;
310
+ return stripUndefined({
311
+ template_id: templateId,
312
+ cpu_count: params.cpuCount ?? params.cpu_count,
313
+ memory_mb: params.memoryMb ?? params.memory_mb,
314
+ disk_mb: params.diskMb ?? params.disk_mb,
315
+ disk_size_mb: params.diskSizeMb ?? params.disk_size_mb,
316
+ timeout_sec: params.timeoutSec ?? params.timeout_sec,
317
+ idle_timeout_sec: params.idleTimeoutSec ?? params.idle_timeout_sec,
318
+ always_on: params.alwaysOn ?? params.always_on,
319
+ env: params.env,
320
+ metadata: params.metadata,
321
+ services: params.services,
322
+ readiness_probe: params.readinessProbe ?? params.readiness_probe,
323
+ database: params.database,
324
+ github_repo_url: params.githubRepoUrl ?? params.github_repo_url,
325
+ github_branch: params.githubBranch ?? params.github_branch,
326
+ github_clone_path: params.githubClonePath ?? params.github_clone_path,
327
+ name: params.name,
328
+ region: params.region,
329
+ entrypoint: params.entrypoint,
330
+ tags: params.tags,
331
+ external_workspace_id:
332
+ params.externalWorkspaceId ?? params.external_workspace_id,
333
+ external_user_id: params.externalUserId ?? params.external_user_id,
334
+ external_project_id: params.externalProjectId ?? params.external_project_id,
335
+ });
336
+ }
337
+
338
+ function execBody(
339
+ command: string,
340
+ options: SandboxExecOptions = {},
341
+ ): Record<string, unknown> {
342
+ return stripUndefined({
343
+ command,
344
+ cwd: options.cwd ?? options.workingDir ?? options.working_dir,
345
+ env: options.env,
346
+ timeout: options.timeout ?? options.timeoutSec ?? options.timeout_sec,
347
+ });
348
+ }
349
+
350
+ function stripUndefined(
351
+ input: Record<string, unknown>,
352
+ ): Record<string, unknown> {
353
+ return Object.fromEntries(
354
+ Object.entries(input).filter(([, value]) => value !== undefined),
355
+ );
356
+ }
357
+
358
+ export class SandboxCommands {
359
+ constructor(private readonly sandbox: Sandbox) {}
360
+
361
+ run(
362
+ command: string,
363
+ options?: SandboxExecOptions,
364
+ ): Promise<SandboxExecResult> {
365
+ return this.sandbox.exec.run(command, options);
366
+ }
367
+
368
+ stream(
369
+ command: string,
370
+ options?: SandboxExecOptions,
371
+ ): AsyncIterableIterator<SandboxExecEvent> {
372
+ return this.sandbox.exec.stream(command, options);
373
+ }
374
+ }
375
+
376
+ export class SandboxFiles {
377
+ constructor(private readonly sandbox: Sandbox) {}
378
+
379
+ async write(path: string, content: string | Uint8Array): Promise<void> {
380
+ await this.sandbox.writeFile(path, content);
381
+ }
382
+
383
+ async read(path: string): Promise<Uint8Array> {
384
+ return this.sandbox.download(path);
385
+ }
386
+
387
+ async readText(path: string): Promise<string> {
388
+ return new TextDecoder().decode(await this.read(path));
389
+ }
390
+
391
+ list(path = "/workspace"): Promise<SandboxFileList> {
392
+ return this.sandbox.listFiles(path);
393
+ }
394
+
395
+ stat(path: string): Promise<SandboxFileStat> {
396
+ return this.sandbox.statFile(path);
397
+ }
398
+
399
+ upload(path: string, content: string | Uint8Array): Promise<void> {
400
+ return this.write(path, content);
401
+ }
402
+
403
+ download(path: string): Promise<Uint8Array> {
404
+ return this.read(path);
405
+ }
406
+ }
407
+
408
+ export class SandboxPreview {
409
+ constructor(private readonly sandbox: Sandbox) {}
410
+
411
+ expose(port?: number): Promise<string> {
412
+ return this.sandbox.expose(port);
413
+ }
414
+ }
415
+
416
+ export class SandboxArtifacts {
417
+ constructor(private readonly sandbox: Sandbox) {}
418
+
419
+ list(): Promise<Record<string, unknown>> {
420
+ return this.sandbox.getArtifacts();
421
+ }
422
+ }
423
+
424
+ export class SandboxLogs {
425
+ constructor(private readonly sandbox: Sandbox) {}
426
+
427
+ get(lines?: number): Promise<string | Record<string, unknown>> {
428
+ return this.sandbox.getLogs(lines);
429
+ }
430
+
431
+ stream(): AsyncIterableIterator<Record<string, unknown>> {
432
+ return this.sandbox.streamLogs();
433
+ }
434
+ }
435
+
436
+ export class SandboxSnapshots {
437
+ constructor(private readonly sandbox: Sandbox) {}
438
+
439
+ create(comment?: string): Promise<SandboxSnapshot> {
440
+ return this.sandbox.createSnapshot(comment);
441
+ }
442
+
443
+ list(): Promise<SandboxSnapshot[]> {
444
+ return this.sandbox.listSnapshots();
445
+ }
446
+
447
+ restore(snapshotId: string): Promise<Sandbox> {
448
+ return this.sandbox.restoreSnapshot(snapshotId);
449
+ }
450
+
451
+ delete(snapshotId: string): Promise<void> {
452
+ return this.sandbox.deleteSnapshot(snapshotId);
453
+ }
454
+ }
455
+
456
+ export class SandboxTerminal {
457
+ constructor(private readonly sandbox: Sandbox) {}
458
+
459
+ async create(
460
+ params: {
461
+ cols?: number;
462
+ rows?: number;
463
+ shell?: string;
464
+ cwd?: string;
465
+ env?: Record<string, string>;
466
+ } = {},
467
+ ): Promise<Record<string, unknown>> {
468
+ const body = Object.fromEntries(
469
+ Object.entries(params).filter(([, v]) => v !== undefined),
470
+ );
471
+ const response = unwrap(
472
+ await (this.sandbox as unknown as { http: HttpClient }).http.post<
473
+ WireEnvelope<Record<string, unknown>>
474
+ >(`/sandboxes/${this.sandbox.id}/terminal`, body),
475
+ );
476
+ return response as Record<string, unknown>;
477
+ }
478
+
479
+ async delete(sessionId: string): Promise<void> {
480
+ await (this.sandbox as unknown as { http: HttpClient }).http.delete(
481
+ `/sandboxes/${this.sandbox.id}/terminal/${sessionId}`,
482
+ );
483
+ }
484
+ }
485
+
486
+ export class SandboxEvents {
487
+ constructor(private readonly sandbox: Sandbox) {}
488
+
489
+ /** Stream live sandbox events via SSE. */
490
+ stream(): AsyncIterableIterator<Record<string, unknown>> {
491
+ return (this.sandbox as unknown as { http: HttpClient }).http.stream<
492
+ Record<string, unknown>
493
+ >(`/sandboxes/${this.sandbox.id}/events`);
494
+ }
495
+ }
496
+
497
+ export class SandboxPreviews {
498
+ constructor(private readonly sandbox: Sandbox) {}
499
+
500
+ private get http(): HttpClient {
501
+ return (this.sandbox as unknown as { http: HttpClient }).http;
502
+ }
503
+
504
+ async list(): Promise<Record<string, unknown>[]> {
505
+ const response = await this.http.get<unknown>(
506
+ `/sandboxes/${this.sandbox.id}/previews`,
507
+ );
508
+ if (Array.isArray(response)) return response as Record<string, unknown>[];
509
+ if (response && typeof response === "object") {
510
+ const r = response as Record<string, unknown>;
511
+ for (const k of ["data", "previews", "items"]) {
512
+ if (Array.isArray(r[k])) return r[k] as Record<string, unknown>[];
513
+ }
514
+ }
515
+ return [];
516
+ }
517
+
518
+ async create(
519
+ port: number,
520
+ opts: Record<string, unknown> = {},
521
+ ): Promise<Record<string, unknown>> {
522
+ const body = {
523
+ port,
524
+ ...Object.fromEntries(
525
+ Object.entries(opts).filter(([, v]) => v !== undefined),
526
+ ),
527
+ };
528
+ return unwrap(
529
+ await this.http.post<WireEnvelope<Record<string, unknown>>>(
530
+ `/sandboxes/${this.sandbox.id}/previews`,
531
+ body,
532
+ ),
533
+ ) as Record<string, unknown>;
534
+ }
535
+
536
+ async get(previewId: string): Promise<Record<string, unknown>> {
537
+ return unwrap(
538
+ await this.http.get<WireEnvelope<Record<string, unknown>>>(
539
+ `/sandboxes/${this.sandbox.id}/previews/${previewId}`,
540
+ ),
541
+ ) as Record<string, unknown>;
542
+ }
543
+
544
+ async delete(previewId: string): Promise<void> {
545
+ await this.http.delete(
546
+ `/sandboxes/${this.sandbox.id}/previews/${previewId}`,
547
+ );
548
+ }
549
+
550
+ /** Mint a share token for previewId. */
551
+ async share(
552
+ previewId: string,
553
+ opts: { ttl_seconds?: number; expires_in_sec?: number } = {},
554
+ ): Promise<Record<string, unknown>> {
555
+ return unwrap(
556
+ await this.http.post<WireEnvelope<Record<string, unknown>>>(
557
+ `/sandboxes/${this.sandbox.id}/previews/${previewId}/share`,
558
+ { ttl_seconds: opts.ttl_seconds ?? opts.expires_in_sec ?? 3600 },
559
+ ),
560
+ ) as Record<string, unknown>;
561
+ }
562
+
563
+ /** Invalidate every share token associated with previewId. */
564
+ async revokeShare(previewId: string): Promise<void> {
565
+ await this.http.delete(
566
+ `/sandboxes/${this.sandbox.id}/previews/${previewId}/share`,
567
+ );
568
+ }
569
+ }
570
+
571
+ export class SandboxEnv {
572
+ constructor(private readonly sandbox: Sandbox) {}
573
+
574
+ /**
575
+ * Read-only listing of sandbox env vars.
576
+ * The backend has no per-name CRUD route; use Sandbox.create(env=...) to set values.
577
+ */
578
+ async list(): Promise<Record<string, unknown>> {
579
+ return unwrap(
580
+ await (this.sandbox as unknown as { http: HttpClient }).http.get<
581
+ WireEnvelope<Record<string, unknown>>
582
+ >(`/sandboxes/${this.sandbox.id}/env`),
583
+ ) as Record<string, unknown>;
584
+ }
585
+ }
586
+
587
+ export class SandboxTags {
588
+ constructor(private readonly sandbox: Sandbox) {}
589
+
590
+ /** Replace the full tag list with tags. */
591
+ async set(tags: string[]): Promise<Record<string, unknown>> {
592
+ return unwrap(
593
+ await (this.sandbox as unknown as { http: HttpClient }).http.patch<
594
+ WireEnvelope<Record<string, unknown>>
595
+ >(`/sandboxes/${this.sandbox.id}/tags`, { tags }),
596
+ ) as Record<string, unknown>;
597
+ }
598
+ }
599
+
600
+ export class Sandbox {
601
+ data: SandboxData;
602
+ readonly commands: SandboxCommands;
603
+ readonly exec: SandboxExecRunner;
604
+ readonly files: SandboxFiles;
605
+ readonly preview: SandboxPreview;
606
+ readonly artifacts: SandboxArtifacts;
607
+ readonly logs: SandboxLogs;
608
+ readonly snapshots: SandboxSnapshots;
609
+ /** PTY session control — create/delete. */
610
+ readonly terminal: SandboxTerminal;
611
+ /** SSE event stream. */
612
+ readonly events: SandboxEvents;
613
+ /** Preview CRUD + share/revokeShare. */
614
+ readonly previews: SandboxPreviews;
615
+ /** Read-only env var listing. */
616
+ readonly env: SandboxEnv;
617
+ /** Tag replacement. */
618
+ readonly tags: SandboxTags;
619
+
620
+ constructor(
621
+ private readonly http: HttpClient,
622
+ data: SandboxData,
623
+ ) {
624
+ this.data = data;
625
+ const runExec = (command: string, options?: SandboxExecOptions) =>
626
+ this.runExec(command, options);
627
+ this.exec = Object.assign(runExec, {
628
+ run: runExec,
629
+ stream: (command: string, options?: SandboxExecOptions) =>
630
+ this.execStream(command, options),
631
+ });
632
+ this.commands = new SandboxCommands(this);
633
+ this.files = new SandboxFiles(this);
634
+ this.preview = new SandboxPreview(this);
635
+ this.artifacts = new SandboxArtifacts(this);
636
+ this.logs = new SandboxLogs(this);
637
+ this.snapshots = new SandboxSnapshots(this);
638
+ this.terminal = new SandboxTerminal(this);
639
+ this.events = new SandboxEvents(this);
640
+ this.previews = new SandboxPreviews(this);
641
+ this.env = new SandboxEnv(this);
642
+ this.tags = new SandboxTags(this);
643
+ }
644
+
645
+ get id(): SandboxId {
646
+ return this.data.id;
647
+ }
648
+
649
+ get state(): SandboxState {
650
+ return this.data.state;
651
+ }
652
+
653
+ get ready(): boolean {
654
+ return this.data.ready ?? this.data.state === "running";
655
+ }
656
+
657
+ get templateId(): string {
658
+ return this.data.template_id ?? this.data.image_id ?? "";
659
+ }
660
+
661
+ async refresh(): Promise<Sandbox> {
662
+ this.data = unwrap(
663
+ await this.http.get<WireEnvelope<SandboxData>>(`/sandboxes/${this.id}`),
664
+ );
665
+ return this;
666
+ }
667
+
668
+ private async runExec(
669
+ command: string,
670
+ options?: SandboxExecOptions,
671
+ ): Promise<SandboxExecResult> {
672
+ this.assertRunning("exec");
673
+ const response = unwrap(
674
+ await this.http.post<WireEnvelope<Record<string, unknown>>>(
675
+ `/sandboxes/${this.id}/exec`,
676
+ execBody(command, options),
677
+ ),
678
+ );
679
+ const exitCode = Number(response.exit_code ?? response.exitCode ?? 0);
680
+ const result: SandboxExecResult = {
681
+ stdout: String(response.stdout ?? ""),
682
+ stderr: String(response.stderr ?? ""),
683
+ exitCode,
684
+ exit_code: exitCode,
685
+ };
686
+ if (typeof response.duration_ms === "number") {
687
+ result.durationMs = response.duration_ms;
688
+ result.duration_ms = response.duration_ms;
689
+ }
690
+ return result;
691
+ }
692
+
693
+ private execStream(
694
+ command: string,
695
+ options?: SandboxExecOptions,
696
+ ): AsyncIterableIterator<SandboxExecEvent> {
697
+ this.assertRunning("exec.stream");
698
+ return this.http.stream<SandboxExecEvent>(
699
+ `/sandboxes/${this.id}/exec/stream`,
700
+ {
701
+ method: "POST",
702
+ body: execBody(command, options),
703
+ },
704
+ );
705
+ }
706
+
707
+ async writeFile(path: string, content: string | Uint8Array): Promise<void> {
708
+ this.assertRunning("writeFile");
709
+ const bytes =
710
+ typeof content === "string" ? new TextEncoder().encode(content) : content;
711
+ await this.http.post(`/sandboxes/${this.id}/files`, {
712
+ path,
713
+ content: toBase64(bytes),
714
+ });
715
+ }
716
+
717
+ async download(path: string): Promise<Uint8Array> {
718
+ this.assertRunning("download");
719
+ return this.http.getBinary(
720
+ `/sandboxes/${this.id}/files/${path.replace(/^\/+/, "")}`,
721
+ );
722
+ }
723
+
724
+ async readFile(path: string): Promise<string> {
725
+ return new TextDecoder().decode(await this.download(path));
726
+ }
727
+
728
+ async listFiles(path = "/workspace"): Promise<SandboxFileList> {
729
+ this.assertRunning("files.list");
730
+ const response = unwrap(
731
+ await this.http.get<WireEnvelope<SandboxFileList>>(
732
+ `/sandboxes/${this.id}/files`,
733
+ { path },
734
+ ),
735
+ );
736
+ return response;
737
+ }
738
+
739
+ async statFile(path: string): Promise<SandboxFileStat> {
740
+ this.assertRunning("files.stat");
741
+ return unwrap(
742
+ await this.http.post<WireEnvelope<SandboxFileStat>>(
743
+ `/sandboxes/${this.id}/files/stat`,
744
+ { path },
745
+ ),
746
+ );
747
+ }
748
+
749
+ async expose(port?: number): Promise<string> {
750
+ this.assertRunning("expose");
751
+ const response = unwrap(
752
+ await this.http.post<WireEnvelope<Record<string, unknown>>>(
753
+ `/sandboxes/${this.id}/expose`,
754
+ port === undefined ? {} : { port },
755
+ ),
756
+ );
757
+ return String(response.url ?? response.preview_url ?? "");
758
+ }
759
+
760
+ async startTemplate(
761
+ options: Record<string, unknown> = {},
762
+ ): Promise<Record<string, unknown>> {
763
+ this.assertRunning("startTemplate");
764
+ return unwrap(
765
+ await this.http.post<WireEnvelope<Record<string, unknown>>>(
766
+ `/sandboxes/${this.id}/template/start`,
767
+ options,
768
+ ),
769
+ );
770
+ }
771
+
772
+ async getArtifacts(): Promise<Record<string, unknown>> {
773
+ return unwrap(
774
+ await this.http.get<WireEnvelope<Record<string, unknown>>>(
775
+ `/sandboxes/${this.id}/artifacts`,
776
+ ),
777
+ );
778
+ }
779
+
780
+ async getLogs(lines?: number): Promise<string | Record<string, unknown>> {
781
+ const response = await this.http.get<WireEnvelope<Record<string, unknown>>>(
782
+ `/sandboxes/${this.id}/logs`,
783
+ { lines },
784
+ );
785
+ return unwrap(response);
786
+ }
787
+
788
+ streamLogs(): AsyncIterableIterator<Record<string, unknown>> {
789
+ return this.http.stream<Record<string, unknown>>(
790
+ `/sandboxes/${this.id}/logs/stream`,
791
+ );
792
+ }
793
+
794
+ async createSnapshot(comment?: string): Promise<SandboxSnapshot> {
795
+ this.assertRunning("snapshots.create");
796
+ return unwrap(
797
+ await this.http.post<WireEnvelope<SandboxSnapshot>>(
798
+ `/sandboxes/${this.id}/snapshots`,
799
+ comment ? { comment } : {},
800
+ ),
801
+ );
802
+ }
803
+
804
+ async listSnapshots(): Promise<SandboxSnapshot[]> {
805
+ return unwrap(
806
+ await this.http.get<WireEnvelope<SandboxSnapshot[]>>(
807
+ `/sandboxes/${this.id}/snapshots`,
808
+ ),
809
+ );
810
+ }
811
+
812
+ async restoreSnapshot(snapshotId: string): Promise<Sandbox> {
813
+ const data = unwrap(
814
+ await this.http.post<WireEnvelope<SandboxData>>(
815
+ `/sandboxes/${this.id}/restore/${snapshotId}`,
816
+ {},
817
+ ),
818
+ );
819
+ return new Sandbox(this.http, data);
820
+ }
821
+
822
+ async deleteSnapshot(snapshotId: string): Promise<void> {
823
+ await this.http.delete(`/sandboxes/${this.id}/snapshots/${snapshotId}`);
824
+ }
825
+
826
+ async pause(): Promise<Sandbox> {
827
+ const data = unwrap(
828
+ await this.http.post<WireEnvelope<SandboxData>>(
829
+ `/sandboxes/${this.id}/pause`,
830
+ {},
831
+ ),
832
+ );
833
+ this.data = data;
834
+ return this;
835
+ }
836
+
837
+ async resume(): Promise<Sandbox> {
838
+ const data = unwrap(
839
+ await this.http.post<WireEnvelope<SandboxData>>(
840
+ `/sandboxes/${this.id}/resume`,
841
+ {},
842
+ ),
843
+ );
844
+ this.data = data;
845
+ return this;
846
+ }
847
+
848
+ async deploy(
849
+ params: SandboxDeployParams = {},
850
+ ): Promise<Record<string, unknown>> {
851
+ const idempotencyKey = params.idempotencyKey ?? params.idempotency_key;
852
+ const requestOptions: Parameters<HttpClient["request"]>[1] = {
853
+ method: "POST",
854
+ body: stripUndefined({
855
+ name: params.name,
856
+ deployment_id: params.deploymentId ?? params.deployment_id,
857
+ output_path:
858
+ params.outputPath ??
859
+ params.output_path ??
860
+ params.path ??
861
+ params.sourcePath ??
862
+ params.source_path,
863
+ source_snapshot_path:
864
+ params.sourceSnapshotPath ?? params.source_snapshot_path,
865
+ entrypoint: params.entrypoint,
866
+ domain: params.domain,
867
+ custom_domain: params.customDomain ?? params.custom_domain,
868
+ }),
869
+ };
870
+ if (idempotencyKey) {
871
+ requestOptions.headers = { "Idempotency-Key": idempotencyKey };
872
+ }
873
+
874
+ return unwrap(
875
+ await this.http.request<WireEnvelope<Record<string, unknown>>>(
876
+ `/sandboxes/${this.id}/deploy`,
877
+ requestOptions,
878
+ ),
879
+ );
880
+ }
881
+
882
+ /** Check readiness of the sandbox (GET /sandboxes/:id/readiness). */
883
+ async readiness(): Promise<Record<string, unknown>> {
884
+ return unwrap(
885
+ await this.http.get<WireEnvelope<Record<string, unknown>>>(
886
+ `/sandboxes/${this.id}/readiness`,
887
+ ),
888
+ ) as Record<string, unknown>;
889
+ }
890
+
891
+ /**
892
+ * Block until the sandbox reports ready, or *timeout* seconds elapse.
893
+ *
894
+ * When `stream` is `true` (the default) this opens an SSE connection
895
+ * to `GET /sandboxes/:id/readiness/stream` and waits for an
896
+ * `event: ready` frame. The server emits `ready` immediately if the
897
+ * sandbox is already ready, otherwise as soon as the readiness PubSub
898
+ * message fires.
899
+ *
900
+ * Returns `true` once the sandbox is ready, `false` on `event: timeout`
901
+ * or when the local timeout elapses before ready.
902
+ *
903
+ * If the SSE endpoint returns 404 (server pre-dates the streaming
904
+ * endpoint) this transparently falls back to polling
905
+ * {@link readiness} every 10 ms until ready or timeout.
906
+ */
907
+ async waitUntilReady(
908
+ options: { timeout?: number; stream?: boolean } = {},
909
+ ): Promise<boolean> {
910
+ const timeout = options.timeout ?? 30;
911
+ const stream = options.stream ?? true;
912
+
913
+ if (stream) {
914
+ const sseResult = await this.tryReadinessStream(timeout);
915
+ if (sseResult !== null) return sseResult;
916
+ // null === fall through to polling fallback (404 or transport error)
917
+ }
918
+
919
+ // Polling fallback — fixed 10 ms tick, no exponential backoff.
920
+ const deadlineMs = Date.now() + timeout * 1000;
921
+ while (Date.now() < deadlineMs) {
922
+ try {
923
+ const data = await this.readiness();
924
+ if (data.ready === true || data.status === "ready") return true;
925
+ } catch {
926
+ // swallow transient errors and keep polling
927
+ }
928
+ await new Promise((resolve) => setTimeout(resolve, 10));
929
+ }
930
+ return false;
931
+ }
932
+
933
+ /**
934
+ * Returns `true` / `false` for terminal SSE events, or `null` if the
935
+ * stream endpoint is unavailable (404 or transport error) so callers
936
+ * can fall back to polling.
937
+ */
938
+ private async tryReadinessStream(
939
+ timeoutSec: number,
940
+ ): Promise<boolean | null> {
941
+ const abort = new AbortController();
942
+ const timer = setTimeout(() => abort.abort(), (timeoutSec + 5) * 1000);
943
+ try {
944
+ const headers: Record<string, string> = {
945
+ Authorization: `Bearer ${this.http.apiKey}`,
946
+ Accept: "text/event-stream",
947
+ "User-Agent": "@miosa/sdk/1.0.0",
948
+ };
949
+ const response = await fetch(
950
+ `${this.http.baseUrl}/sandboxes/${this.id}/readiness/stream`,
951
+ { method: "GET", headers, signal: abort.signal },
952
+ );
953
+ if (response.status === 404) return null;
954
+ if (!response.ok || !response.body) return null;
955
+
956
+ const reader = response.body.getReader();
957
+ const decoder = new TextDecoder();
958
+ let buffer = "";
959
+ try {
960
+ while (true) {
961
+ const { done, value } = await reader.read();
962
+ if (done) break;
963
+ buffer += decoder.decode(value, { stream: true });
964
+ let newlineIdx: number;
965
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
966
+ const line = buffer.slice(0, newlineIdx).replace(/\r$/, "");
967
+ buffer = buffer.slice(newlineIdx + 1);
968
+ if (line.startsWith("event:")) {
969
+ const evt = line.slice(6).trim();
970
+ if (evt === "ready") return true;
971
+ if (evt === "timeout") return false;
972
+ }
973
+ }
974
+ }
975
+ } finally {
976
+ try {
977
+ reader.releaseLock();
978
+ } catch {
979
+ // already released
980
+ }
981
+ }
982
+ // stream closed without a terminal event — let caller decide
983
+ return null;
984
+ } catch {
985
+ return null;
986
+ } finally {
987
+ clearTimeout(timer);
988
+ try {
989
+ abort.abort();
990
+ } catch {
991
+ // signal already aborted
992
+ }
993
+ }
994
+ }
995
+
996
+ async destroy(): Promise<void> {
997
+ if (this.data.state === "destroyed") return;
998
+ await this.http.delete(`/sandboxes/${this.id}`);
999
+ this.data = {
1000
+ ...this.data,
1001
+ state: "destroyed",
1002
+ destroyed_at: new Date().toISOString(),
1003
+ };
1004
+ }
1005
+
1006
+ delete(): Promise<void> {
1007
+ return this.destroy();
1008
+ }
1009
+
1010
+ private assertRunning(operation: string): void {
1011
+ if (this.data.state === "destroyed") {
1012
+ throw new MiosaError(
1013
+ `Sandbox ${this.id} has been destroyed`,
1014
+ 400,
1015
+ "SANDBOX_DESTROYED",
1016
+ );
1017
+ }
1018
+ if (this.data.state !== "running") {
1019
+ throw new MiosaError(
1020
+ `Cannot ${operation} on sandbox ${this.id}: state is ${this.data.state}, expected running`,
1021
+ 409,
1022
+ "SANDBOX_NOT_RUNNING",
1023
+ );
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ export class Sandboxes {
1029
+ constructor(private readonly http: HttpClient) {}
1030
+
1031
+ async create(params: SandboxCreateParams = {}): Promise<Sandbox> {
1032
+ const idempotencyKey = params.idempotencyKey ?? params.idempotency_key;
1033
+ const requestOptions: Parameters<HttpClient["request"]>[1] = {
1034
+ method: "POST",
1035
+ body: createBody(params),
1036
+ };
1037
+ if (idempotencyKey) {
1038
+ requestOptions.headers = { "Idempotency-Key": idempotencyKey };
1039
+ }
1040
+
1041
+ const data = unwrap(
1042
+ await this.http.request<WireEnvelope<SandboxData>>(
1043
+ "/sandboxes",
1044
+ requestOptions,
1045
+ ),
1046
+ );
1047
+ return new Sandbox(this.http, data);
1048
+ }
1049
+
1050
+ async list(params: SandboxListParams = {}): Promise<Sandbox[]> {
1051
+ const data = await this.http.get<
1052
+ | WireEnvelope<SandboxData[]>
1053
+ | { sandboxes: SandboxData[] }
1054
+ | { items: SandboxData[] }
1055
+ >("/sandboxes", {
1056
+ state: params.state,
1057
+ tags: params.tags?.join(","),
1058
+ external_workspace_id:
1059
+ params.externalWorkspaceId ?? params.external_workspace_id,
1060
+ external_user_id: params.externalUserId ?? params.external_user_id,
1061
+ external_project_id:
1062
+ params.externalProjectId ?? params.external_project_id,
1063
+ });
1064
+ return listItems(data).map((item) => new Sandbox(this.http, item));
1065
+ }
1066
+
1067
+ async get(id: SandboxId | string): Promise<Sandbox> {
1068
+ const data = unwrap(
1069
+ await this.http.get<WireEnvelope<SandboxData>>(`/sandboxes/${id}`),
1070
+ );
1071
+ return new Sandbox(this.http, data);
1072
+ }
1073
+
1074
+ connect(id: SandboxId | string): Promise<Sandbox> {
1075
+ return this.get(id);
1076
+ }
1077
+
1078
+ async delete(id: SandboxId | string): Promise<void> {
1079
+ await this.http.delete(`/sandboxes/${id}`);
1080
+ }
1081
+
1082
+ async listTemplates(
1083
+ options: { includeAliases?: boolean } = {},
1084
+ ): Promise<SandboxTemplateList> {
1085
+ return this.http.get<SandboxTemplateList>("/sandbox-templates", {
1086
+ include_aliases: options.includeAliases,
1087
+ });
1088
+ }
1089
+
1090
+ async getTemplate(id: string): Promise<SandboxTemplate> {
1091
+ return this.http.get<SandboxTemplate>(`/sandbox-templates/${id}`);
1092
+ }
1093
+
1094
+ async getBuildSpecSchema(): Promise<Record<string, unknown>> {
1095
+ return this.http.get<Record<string, unknown>>(
1096
+ "/sandbox-templates/build-spec",
1097
+ );
1098
+ }
1099
+
1100
+ async validateBuildSpec(
1101
+ buildSpec: SandboxBuildSpec,
1102
+ ): Promise<SandboxBuildSpecValidation> {
1103
+ return this.http.post<SandboxBuildSpecValidation>(
1104
+ "/sandbox-templates/validate",
1105
+ {
1106
+ build_spec: buildSpec,
1107
+ },
1108
+ );
1109
+ }
1110
+
1111
+ async createTemplate(
1112
+ params: SandboxTemplateCreateParams,
1113
+ ): Promise<SandboxTemplate> {
1114
+ const response = await this.http.post<WireEnvelope<SandboxTemplate>>(
1115
+ "/sandbox-templates",
1116
+ stripUndefined({
1117
+ name: params.name,
1118
+ slug: params.slug,
1119
+ description: params.description,
1120
+ build_spec: params.buildSpec ?? params.build_spec,
1121
+ metadata: params.metadata,
1122
+ }),
1123
+ );
1124
+ return unwrap(response);
1125
+ }
1126
+
1127
+ async createTemplateBuild(
1128
+ templateId: string,
1129
+ params: SandboxTemplateBuildCreateParams = {},
1130
+ ): Promise<SandboxTemplateBuild> {
1131
+ const response = await this.http.post<WireEnvelope<SandboxTemplateBuild>>(
1132
+ `/sandbox-templates/${templateId}/builds`,
1133
+ stripUndefined({
1134
+ build_spec: params.buildSpec ?? params.build_spec,
1135
+ metadata: params.metadata,
1136
+ }),
1137
+ );
1138
+ return unwrap(response);
1139
+ }
1140
+
1141
+ async listTemplateBuilds(
1142
+ templateId: string,
1143
+ ): Promise<SandboxTemplateBuild[]> {
1144
+ const response = await this.http.get<WireEnvelope<SandboxTemplateBuild[]>>(
1145
+ `/sandbox-templates/${templateId}/builds`,
1146
+ );
1147
+ return unwrap(response);
1148
+ }
1149
+
1150
+ async getTemplateBuild(buildId: string): Promise<SandboxTemplateBuild> {
1151
+ const response = await this.http.get<WireEnvelope<SandboxTemplateBuild>>(
1152
+ `/sandbox-template-builds/${buildId}`,
1153
+ );
1154
+ return unwrap(response);
1155
+ }
1156
+ }
1157
+
1158
+ function toBase64(bytes: Uint8Array): string {
1159
+ const maybeBuffer = (
1160
+ globalThis as {
1161
+ Buffer?: {
1162
+ from(data: Uint8Array): { toString(encoding: string): string };
1163
+ };
1164
+ }
1165
+ ).Buffer;
1166
+ if (maybeBuffer) return maybeBuffer.from(bytes).toString("base64");
1167
+
1168
+ let binary = "";
1169
+ for (let index = 0; index < bytes.length; index += 1) {
1170
+ binary += String.fromCharCode(bytes[index]!);
1171
+ }
1172
+ return btoa(binary);
1173
+ }