@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/dist/index.d.ts +968 -6
- package/dist/index.js +1341 -236
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +39 -0
- package/src/index.ts +90 -0
- package/src/resources/admin.ts +11 -0
- package/src/resources/api-keys.ts +16 -0
- package/src/resources/computer.ts +16 -0
- package/src/resources/egress.test.ts +318 -0
- package/src/resources/egressAudit.ts +245 -0
- package/src/resources/egressNetwork.ts +450 -0
- package/src/resources/egressSecrets.ts +577 -0
- package/src/resources/governance.test.ts +355 -0
- package/src/resources/governance.ts +528 -0
- package/src/resources/org-invites.ts +189 -0
- package/src/resources/phase1.test.ts +187 -0
- package/src/resources/quotas.ts +77 -0
- package/src/resources/sandbox-processes.ts +112 -0
- package/src/resources/sandbox-shares.ts +83 -0
- package/src/resources/sandboxes.ts +239 -10
- package/src/resources/tenant-events.ts +32 -0
- package/src/resources/workspace-invites.ts +188 -0
- package/src/resources/workspace-members.test.ts +121 -0
- package/src/resources/workspace-members.ts +143 -0
- package/src/resources/workspaces.ts +285 -0
package/package.json
CHANGED
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,
|
package/src/resources/admin.ts
CHANGED
|
@@ -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
|
+
});
|