@miosa/sdk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miosa/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "TypeScript SDK for the MIOSA API — cloud VM desktop infrastructure for AI agents",
5
5
  "license": "MIT",
6
6
  "author": "MIOSA <hello@miosa.ai>",
package/src/client.ts CHANGED
@@ -15,6 +15,9 @@ import { CronJobs } from "./resources/cron-jobs.js";
15
15
  import { Dashboard } from "./resources/dashboard.js";
16
16
  import { Databases } from "./resources/databases.js";
17
17
  import { Deployments } from "./resources/deployments.js";
18
+ import { EgressAudit } from "./resources/egressAudit.js";
19
+ import { EgressNetwork } from "./resources/egressNetwork.js";
20
+ import { EgressSecrets } from "./resources/egressSecrets.js";
18
21
  import { Email } from "./resources/email.js";
19
22
  import { Embeddings } from "./resources/embeddings.js";
20
23
  import { ExternalKeys } from "./resources/external-keys.js";
@@ -34,10 +37,13 @@ import { SandboxTemplates } from "./resources/sandbox-templates.js";
34
37
  import { Settings } from "./resources/settings.js";
35
38
  import { SnapshotsStandalone } from "./resources/snapshots-standalone.js";
36
39
  import { Storage } from "./resources/storage.js";
40
+ import { OrgInvites } from "./resources/org-invites.js";
37
41
  import { Tenant } from "./resources/tenant.js";
38
42
  import { Usage } from "./resources/usage.js";
39
43
  import { Volumes } from "./resources/volumes.js";
40
44
  import { Webhooks } from "./resources/webhooks.js";
45
+ import { WorkspaceInvites } from "./resources/workspace-invites.js";
46
+ import { WorkspaceMembers } from "./resources/workspace-members.js";
41
47
  import type { MiosaClientConfig } from "./types.js";
42
48
 
43
49
  const DEFAULT_BASE_URL = "https://api.miosa.ai/api/v1";
@@ -58,6 +64,21 @@ const DEFAULT_MAX_RETRIES = 3;
58
64
  * ```
59
65
  */
60
66
  export class Miosa {
67
+ /** Per-workspace user roster — list, add, update role, remove. */
68
+ readonly workspaceMembers: WorkspaceMembers;
69
+
70
+ /**
71
+ * Workspace invite flow — create invite, list, revoke, preview, accept.
72
+ * Sending to an email already in the org adds the user directly.
73
+ */
74
+ readonly workspaceInvites: WorkspaceInvites;
75
+
76
+ /**
77
+ * Org invite flow — create invite, list, revoke, preview, accept.
78
+ * Requires admin/owner role for write operations.
79
+ */
80
+ readonly orgInvites: OrgInvites;
81
+
61
82
  /** Current tenant plan, limits, and live usage counters. */
62
83
  readonly tenant: Tenant;
63
84
 
@@ -183,6 +204,17 @@ export class Miosa {
183
204
  /** Admin: fleet-wide snapshot index. */
184
205
  readonly snapshotsStandalone: SnapshotsStandalone;
185
206
 
207
+ // ── Egress (security) namespaces ───────────────────────────────────────────
208
+
209
+ /** Encrypted secret + OAuth credential vault (`/egress/secrets`). */
210
+ readonly secrets: EgressSecrets;
211
+
212
+ /** Egress allowlist + policies — host-level firewall (`/egress/policies`). */
213
+ readonly network: EgressNetwork;
214
+
215
+ /** Egress audit log — every outbound request, paginated query + tail. */
216
+ readonly audit: EgressAudit;
217
+
186
218
  private readonly http: HttpClient;
187
219
 
188
220
  constructor(config: MiosaClientConfig) {
@@ -206,6 +238,9 @@ export class Miosa {
206
238
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
207
239
  });
208
240
 
241
+ this.workspaceMembers = new WorkspaceMembers(this.http);
242
+ this.workspaceInvites = new WorkspaceInvites(this.http);
243
+ this.orgInvites = new OrgInvites(this.http);
209
244
  this.tenant = new Tenant(this.http);
210
245
  this.regions = new Regions(this.http);
211
246
  this.settings = new Settings(this.http);
@@ -245,5 +280,9 @@ export class Miosa {
245
280
  this.email = new Email(this.http);
246
281
  this.builderSessions = new BuilderSessions(this.http);
247
282
  this.snapshotsStandalone = new SnapshotsStandalone(this.http);
283
+ // Egress (security) namespaces
284
+ this.secrets = new EgressSecrets(this.http);
285
+ this.network = new EgressNetwork(this.http);
286
+ this.audit = new EgressAudit(this.http);
248
287
  }
249
288
  }
package/src/index.ts CHANGED
@@ -245,6 +245,44 @@ export type {
245
245
  ApiKeyCreateParams,
246
246
  } from "./resources/api-keys.js";
247
247
 
248
+ // Members & Invites
249
+ export { WorkspaceMembers } from "./resources/workspace-members.js";
250
+ export type {
251
+ WorkspaceRole,
252
+ WorkspaceMember,
253
+ WorkspaceMemberRecord,
254
+ AddWorkspaceMemberParams,
255
+ UpdateWorkspaceMemberRoleParams,
256
+ WorkspaceMemberListResponse,
257
+ WorkspaceMemberRecordResponse,
258
+ WorkspaceMemberDeleteResponse,
259
+ } from "./resources/workspace-members.js";
260
+ export { WorkspaceInvites } from "./resources/workspace-invites.js";
261
+ export type {
262
+ WorkspaceInvite,
263
+ WorkspaceInvitePreview,
264
+ CreateWorkspaceInviteParams,
265
+ CreateWorkspaceInviteResponse,
266
+ WorkspaceInviteCreatedResponse,
267
+ WorkspaceMemberAddedResponse,
268
+ WorkspaceInviteListResponse,
269
+ WorkspaceInviteRevokeResponse,
270
+ AcceptWorkspaceInviteResponse,
271
+ } from "./resources/workspace-invites.js";
272
+ export { OrgInvites } from "./resources/org-invites.js";
273
+ export type {
274
+ OrgRole,
275
+ OrgInvite,
276
+ OrgInviteCreated,
277
+ OrgInvitePreview,
278
+ TenantSummary,
279
+ CreateOrgInviteParams,
280
+ OrgInviteCreatedResponse,
281
+ OrgInviteListResponse,
282
+ OrgInviteRevokeResponse,
283
+ AcceptOrgInviteResponse,
284
+ } from "./resources/org-invites.js";
285
+
248
286
  // P2 resources
249
287
  export { Tenant } from "./resources/tenant.js";
250
288
  export type { TenantPlan } from "./resources/tenant.js";
@@ -369,6 +407,58 @@ export {
369
407
  SandboxTags,
370
408
  } from "./resources/sandboxes.js";
371
409
 
410
+ // Egress (security) — secrets, network, audit
411
+ export {
412
+ EgressSecrets,
413
+ OAuthFlow,
414
+ SandboxSecrets,
415
+ ComputerSecrets,
416
+ } from "./resources/egressSecrets.js";
417
+ export type {
418
+ EgressSecretType,
419
+ EgressSecretScope,
420
+ EgressSecretData,
421
+ EgressBindingData,
422
+ OauthProvider,
423
+ SecretSetParams,
424
+ SecretListParams,
425
+ SecretRotateParams,
426
+ BindingCreateParams,
427
+ BindingListParams,
428
+ OauthConnectParams,
429
+ OauthStartResult,
430
+ OauthStatusResult,
431
+ } from "./resources/egressSecrets.js";
432
+ export {
433
+ EgressNetwork,
434
+ SandboxNetwork,
435
+ ComputerNetwork,
436
+ } from "./resources/egressNetwork.js";
437
+ export type {
438
+ EgressPolicyMode,
439
+ EgressRuleEffect,
440
+ EgressPolicyData,
441
+ EgressAllowlistRule,
442
+ EgressSuggestion,
443
+ AllowParams,
444
+ PolicyCreateParams,
445
+ PolicyUpdateParams,
446
+ ModeParams,
447
+ SuggestionsParams,
448
+ PolicyListParams,
449
+ RulesListParams,
450
+ } from "./resources/egressNetwork.js";
451
+ export {
452
+ EgressAudit,
453
+ SandboxAudit,
454
+ ComputerAudit,
455
+ } from "./resources/egressAudit.js";
456
+ export type {
457
+ EgressAuditEvent,
458
+ AuditListParams,
459
+ AuditTailParams,
460
+ } from "./resources/egressAudit.js";
461
+
372
462
  // Error types
373
463
  export {
374
464
  MiosaError,
@@ -345,4 +345,15 @@ export class Admin {
345
345
  model_id: modelId,
346
346
  });
347
347
  }
348
+
349
+ /** POST /api/v1/admin/impersonate — returns {token, expires_at}. */
350
+ impersonate(
351
+ externalUserId: string,
352
+ options: { ttlSec?: number } = {},
353
+ ): Promise<{ token: string; expires_at: string }> {
354
+ return this.http.post("/admin/impersonate", {
355
+ external_user_id: externalUserId,
356
+ ttl_sec: options.ttlSec ?? 3600,
357
+ });
358
+ }
348
359
  }
@@ -113,6 +113,22 @@ export class ApiKeys {
113
113
  return unwrap<ApiKeyCreateResult>(data);
114
114
  }
115
115
 
116
+ /** POST /api/v1/api-keys/scoped — L2 delegation token bound to one external user. */
117
+ async createScoped(params: {
118
+ externalUserId: string;
119
+ scopes: string[];
120
+ expiresAt?: string;
121
+ }): Promise<ApiKeyCreateResult> {
122
+ const body = stripUndefined({
123
+ external_user_id: params.externalUserId,
124
+ scopes: params.scopes,
125
+ expires_at: params.expiresAt,
126
+ });
127
+ return unwrap<ApiKeyCreateResult>(
128
+ await this.http.post<unknown>("/api-keys/scoped", body),
129
+ );
130
+ }
131
+
116
132
  async delete(keyId: string): Promise<void> {
117
133
  await this.http.delete<unknown>(`/api-keys/${keyId}`);
118
134
  }
@@ -10,6 +10,9 @@ import { ComputerTerminal } from "./computer-terminal.js";
10
10
  import { ComputerVolumes } from "./computer-volumes.js";
11
11
  import { CustomDomains } from "./custom_domains.js";
12
12
  import { Desktop } from "./desktop.js";
13
+ import { ComputerAudit } from "./egressAudit.js";
14
+ import { ComputerNetwork } from "./egressNetwork.js";
15
+ import { ComputerSecrets } from "./egressSecrets.js";
13
16
  import { Events } from "./events.js";
14
17
  import { Exec } from "./exec.js";
15
18
  import { Files } from "./files.js";
@@ -112,6 +115,15 @@ export class Computer {
112
115
  /** Volume attachment — list, attach, detach. */
113
116
  readonly volumes: ComputerVolumes;
114
117
 
118
+ /** Encrypted secrets + OAuth credentials scoped to this computer. */
119
+ readonly secrets: ComputerSecrets;
120
+
121
+ /** Egress allowlist + policies scoped to this computer. */
122
+ readonly network: ComputerNetwork;
123
+
124
+ /** Egress audit log + live tail scoped to this computer. */
125
+ readonly audit: ComputerAudit;
126
+
115
127
  private readonly http: HttpClient;
116
128
 
117
129
  constructor(http: HttpClient, data: ComputerData) {
@@ -134,6 +146,10 @@ export class Computer {
134
146
  this.logs = new ComputerLogs(http, id);
135
147
  this.ports = new ComputerPorts(http, id);
136
148
  this.volumes = new ComputerVolumes(http, id);
149
+ // Egress (security) namespaces — pre-scoped to this computer id.
150
+ this.secrets = new ComputerSecrets(http, id);
151
+ this.network = new ComputerNetwork(http, id);
152
+ this.audit = new ComputerAudit(http, id);
137
153
  }
138
154
 
139
155
  get id(): ComputerId {
@@ -0,0 +1,318 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { HttpClient } from "../http.js";
3
+ import { EgressAudit, SandboxAudit } from "./egressAudit.js";
4
+ import { EgressNetwork, SandboxNetwork } from "./egressNetwork.js";
5
+ import { EgressSecrets, OAuthFlow, SandboxSecrets } from "./egressSecrets.js";
6
+
7
+ const mockGet = vi.fn();
8
+ const mockPost = vi.fn();
9
+ const mockPatch = vi.fn();
10
+ const mockDelete = vi.fn();
11
+ const mockRequest = vi.fn();
12
+ const mockStream = vi.fn();
13
+ const mockGetBinary = vi.fn();
14
+
15
+ function makeHttp(): HttpClient {
16
+ const http = {} as HttpClient;
17
+ http.get = mockGet;
18
+ http.post = mockPost;
19
+ http.patch = mockPatch;
20
+ http.delete = mockDelete;
21
+ http.request = mockRequest;
22
+ http.stream = mockStream;
23
+ http.getBinary = mockGetBinary;
24
+ return http;
25
+ }
26
+
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ // ─── client.secrets ──────────────────────────────────────────────────────────
32
+
33
+ describe("EgressSecrets", () => {
34
+ it("set() POSTs to /egress/secrets with the right body", async () => {
35
+ mockPost.mockResolvedValue({
36
+ data: { id: "sec_1", name: "OPENAI_API_KEY" },
37
+ });
38
+
39
+ const client = new EgressSecrets(makeHttp());
40
+ const secret = await client.set({
41
+ name: "OPENAI_API_KEY",
42
+ value: "sk-abc123",
43
+ type: "api_key",
44
+ scope: "user",
45
+ exposeAsEnv: "OPENAI_API_KEY",
46
+ });
47
+
48
+ expect(mockPost).toHaveBeenCalledWith("/egress/secrets", {
49
+ name: "OPENAI_API_KEY",
50
+ value: "sk-abc123",
51
+ type: "api_key",
52
+ scope: "user",
53
+ expose_as_env: "OPENAI_API_KEY",
54
+ });
55
+ expect(secret.id).toBe("sec_1");
56
+ });
57
+
58
+ it("list({ scope: 'user' }) GETs /egress/secrets?scope=user", async () => {
59
+ mockGet.mockResolvedValue({ data: [{ id: "sec_1" }, { id: "sec_2" }] });
60
+
61
+ const result = await new EgressSecrets(makeHttp()).list({ scope: "user" });
62
+
63
+ expect(mockGet).toHaveBeenCalledWith("/egress/secrets", { scope: "user" });
64
+ expect(result).toHaveLength(2);
65
+ });
66
+
67
+ it("get() calls /egress/secrets/:id", async () => {
68
+ mockGet.mockResolvedValue({ data: { id: "sec_1" } });
69
+ await new EgressSecrets(makeHttp()).get("sec_1");
70
+ expect(mockGet).toHaveBeenCalledWith("/egress/secrets/sec_1");
71
+ });
72
+
73
+ it("rotate() PATCHes /egress/secrets/:id with new value", async () => {
74
+ mockPatch.mockResolvedValue({ data: { id: "sec_1" } });
75
+ await new EgressSecrets(makeHttp()).rotate("sec_1", {
76
+ newValue: "sk-new",
77
+ expiresAt: "2027-01-01T00:00:00Z",
78
+ });
79
+ expect(mockPatch).toHaveBeenCalledWith("/egress/secrets/sec_1", {
80
+ value: "sk-new",
81
+ expires_at: "2027-01-01T00:00:00Z",
82
+ });
83
+ });
84
+
85
+ it("delete() DELETEs /egress/secrets/:id", async () => {
86
+ mockDelete.mockResolvedValue(undefined);
87
+ await new EgressSecrets(makeHttp()).delete("sec_1");
88
+ expect(mockDelete).toHaveBeenCalledWith("/egress/secrets/sec_1");
89
+ });
90
+
91
+ it("connect() returns an OAuthFlow with authorizeUrl + state", async () => {
92
+ mockPost.mockResolvedValue({
93
+ data: {
94
+ authorize_url: "https://github.com/login/oauth/authorize?state=xyz",
95
+ state: "xyz",
96
+ },
97
+ });
98
+
99
+ const flow = await new EgressSecrets(makeHttp()).connect({
100
+ provider: "github",
101
+ exposeAsEnv: "GITHUB_TOKEN",
102
+ });
103
+
104
+ expect(mockPost).toHaveBeenCalledWith("/egress/oauth/start", {
105
+ provider: "github",
106
+ expose_as_env: "GITHUB_TOKEN",
107
+ });
108
+ expect(flow).toBeInstanceOf(OAuthFlow);
109
+ expect(flow.authorizeUrl).toBe(
110
+ "https://github.com/login/oauth/authorize?state=xyz",
111
+ );
112
+ expect(flow.state).toBe("xyz");
113
+ });
114
+
115
+ it("OAuthFlow.waitForCompletion polls /egress/oauth/status", async () => {
116
+ mockPost.mockResolvedValue({
117
+ data: { authorize_url: "https://example.com", state: "xyz" },
118
+ });
119
+ mockGet.mockResolvedValueOnce({
120
+ data: { status: "completed", secret_id: "sec_new" },
121
+ });
122
+
123
+ const flow = await new EgressSecrets(makeHttp()).connect({
124
+ provider: "github",
125
+ });
126
+ const result = await flow.waitForCompletion({
127
+ timeoutSec: 2,
128
+ pollIntervalMs: 1,
129
+ });
130
+
131
+ expect(mockGet).toHaveBeenCalledWith("/egress/oauth/status", {
132
+ state: "xyz",
133
+ });
134
+ expect(result.status).toBe("completed");
135
+ });
136
+
137
+ it("createBinding() POSTs to /egress/bindings", async () => {
138
+ mockPost.mockResolvedValue({ data: { id: "bnd_1" } });
139
+ await new EgressSecrets(makeHttp()).createBinding({
140
+ secretId: "sec_1",
141
+ resourceId: "sbx_1",
142
+ resourceType: "sandbox",
143
+ exposeAsEnv: "OPENAI_API_KEY",
144
+ });
145
+ expect(mockPost).toHaveBeenCalledWith("/egress/bindings", {
146
+ secret_id: "sec_1",
147
+ resource_id: "sbx_1",
148
+ resource_type: "sandbox",
149
+ expose_as_env: "OPENAI_API_KEY",
150
+ });
151
+ });
152
+
153
+ it("providers() GETs /egress/oauth/providers", async () => {
154
+ mockGet.mockResolvedValue({
155
+ data: [{ name: "github" }, { name: "slack" }],
156
+ });
157
+ const out = await new EgressSecrets(makeHttp()).providers();
158
+ expect(mockGet).toHaveBeenCalledWith("/egress/oauth/providers");
159
+ expect(out).toHaveLength(2);
160
+ });
161
+ });
162
+
163
+ // ─── client.network ──────────────────────────────────────────────────────────
164
+
165
+ describe("EgressNetwork", () => {
166
+ it("allow() POSTs to /egress/allowlist with effect=allow", async () => {
167
+ mockPost.mockResolvedValue({ data: { id: "rul_1" } });
168
+ await new EgressNetwork(makeHttp()).allow("api.openai.com", {
169
+ methods: ["GET", "POST"],
170
+ pathGlob: "/v1/*",
171
+ });
172
+ expect(mockPost).toHaveBeenCalledWith("/egress/allowlist", {
173
+ host: "api.openai.com",
174
+ effect: "allow",
175
+ methods: ["GET", "POST"],
176
+ path_glob: "/v1/*",
177
+ });
178
+ });
179
+
180
+ it("deny() POSTs with effect=deny", async () => {
181
+ mockPost.mockResolvedValue({ data: { id: "rul_2" } });
182
+ await new EgressNetwork(makeHttp()).deny("169.254.169.254");
183
+ expect(mockPost).toHaveBeenCalledWith("/egress/allowlist", {
184
+ host: "169.254.169.254",
185
+ effect: "deny",
186
+ });
187
+ });
188
+
189
+ it("lockdown() PATCHes /egress/policies with mode=enforce", async () => {
190
+ mockPatch.mockResolvedValue({ data: {} });
191
+ await new EgressNetwork(makeHttp()).lockdown();
192
+ expect(mockPatch).toHaveBeenCalledWith("/egress/policies", {
193
+ mode: "enforce",
194
+ });
195
+ });
196
+
197
+ it("observe() PATCHes /egress/policies with mode=audit_only", async () => {
198
+ mockPatch.mockResolvedValue({ data: {} });
199
+ await new EgressNetwork(makeHttp()).observe();
200
+ expect(mockPatch).toHaveBeenCalledWith("/egress/policies", {
201
+ mode: "audit_only",
202
+ });
203
+ });
204
+
205
+ it("suggestions() GETs /egress/audit/suggestions with resource_id", async () => {
206
+ mockGet.mockResolvedValue({ data: [] });
207
+ await new EgressNetwork(makeHttp()).suggestions({
208
+ resourceId: "sbx_1",
209
+ since: "1d",
210
+ });
211
+ expect(mockGet).toHaveBeenCalledWith("/egress/audit/suggestions", {
212
+ resource_id: "sbx_1",
213
+ since: "1d",
214
+ });
215
+ });
216
+
217
+ it("policies() GETs /egress/policies", async () => {
218
+ mockGet.mockResolvedValue({ data: [{ id: "pol_1" }] });
219
+ const out = await new EgressNetwork(makeHttp()).policies();
220
+ expect(mockGet).toHaveBeenCalledWith("/egress/policies", {});
221
+ expect(out).toHaveLength(1);
222
+ });
223
+
224
+ it("removeRule() DELETEs /egress/allowlist/:id", async () => {
225
+ mockDelete.mockResolvedValue(undefined);
226
+ await new EgressNetwork(makeHttp()).removeRule("rul_1");
227
+ expect(mockDelete).toHaveBeenCalledWith("/egress/allowlist/rul_1");
228
+ });
229
+ });
230
+
231
+ // ─── client.audit ────────────────────────────────────────────────────────────
232
+
233
+ describe("EgressAudit", () => {
234
+ it("list() GETs /egress/audit", async () => {
235
+ mockGet.mockResolvedValue({
236
+ data: [
237
+ { id: "evt_1", host: "api.openai.com" },
238
+ { id: "evt_2", host: "github.com" },
239
+ ],
240
+ });
241
+ const out = await new EgressAudit(makeHttp()).list();
242
+ expect(mockGet).toHaveBeenCalledWith("/egress/audit", {});
243
+ expect(out).toHaveLength(2);
244
+ });
245
+
246
+ it("list() passes filters as query params", async () => {
247
+ mockGet.mockResolvedValue({ data: [] });
248
+ await new EgressAudit(makeHttp()).list({
249
+ resourceId: "sbx_1",
250
+ host: "api.openai.com",
251
+ action: "denied",
252
+ limit: 50,
253
+ });
254
+ expect(mockGet).toHaveBeenCalledWith("/egress/audit", {
255
+ resource_id: "sbx_1",
256
+ host: "api.openai.com",
257
+ action: "denied",
258
+ limit: 50,
259
+ });
260
+ });
261
+
262
+ it("get() returns a single event", async () => {
263
+ mockGet.mockResolvedValue({ data: { id: "evt_1" } });
264
+ const out = await new EgressAudit(makeHttp()).get("evt_1");
265
+ expect(mockGet).toHaveBeenCalledWith("/egress/audit/evt_1");
266
+ expect(out.id).toBe("evt_1");
267
+ });
268
+ });
269
+
270
+ // ─── sandbox.* scoped namespaces ─────────────────────────────────────────────
271
+
272
+ describe("Sandbox-scoped egress", () => {
273
+ it("sandbox.secrets.set() injects resource_id + resource_type", async () => {
274
+ mockPost.mockResolvedValue({ data: { id: "sec_1" } });
275
+ await new SandboxSecrets(makeHttp(), "sbx_abc").set({
276
+ name: "OPENAI_API_KEY",
277
+ value: "sk-abc",
278
+ });
279
+ expect(mockPost).toHaveBeenCalledWith("/egress/secrets", {
280
+ name: "OPENAI_API_KEY",
281
+ value: "sk-abc",
282
+ type: "api_key",
283
+ scope: "user",
284
+ resource_id: "sbx_abc",
285
+ resource_type: "sandbox",
286
+ });
287
+ });
288
+
289
+ it("sandbox.audit.list() includes resource_id in query params", async () => {
290
+ mockGet.mockResolvedValue({ data: [] });
291
+ await new SandboxAudit(makeHttp(), "sbx_abc").list();
292
+ expect(mockGet).toHaveBeenCalledWith("/egress/audit", {
293
+ resource_id: "sbx_abc",
294
+ resource_type: "sandbox",
295
+ });
296
+ });
297
+
298
+ it("sandbox.network.allow() scopes the rule to the sandbox", async () => {
299
+ mockPost.mockResolvedValue({ data: { id: "rul_1" } });
300
+ await new SandboxNetwork(makeHttp(), "sbx_abc").allow("api.openai.com");
301
+ expect(mockPost).toHaveBeenCalledWith("/egress/allowlist", {
302
+ host: "api.openai.com",
303
+ effect: "allow",
304
+ resource_id: "sbx_abc",
305
+ resource_type: "sandbox",
306
+ });
307
+ });
308
+
309
+ it("sandbox.network.lockdown() scopes the policy patch", async () => {
310
+ mockPatch.mockResolvedValue({ data: {} });
311
+ await new SandboxNetwork(makeHttp(), "sbx_abc").lockdown();
312
+ expect(mockPatch).toHaveBeenCalledWith("/egress/policies", {
313
+ mode: "enforce",
314
+ resource_id: "sbx_abc",
315
+ resource_type: "sandbox",
316
+ });
317
+ });
318
+ });