@miosa/sdk 0.3.0 → 1.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.
- package/dist/index.d.ts +1209 -18
- package/dist/index.js +1698 -252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +44 -0
- package/src/index.ts +100 -0
- package/src/resources/api-keys.ts +5 -1
- package/src/resources/computer.ts +16 -0
- package/src/resources/cron-jobs.ts +7 -0
- package/src/resources/custom_domains.ts +7 -0
- package/src/resources/deployments.ts +13 -2
- 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/flat-custom-domains.ts +7 -0
- package/src/resources/functions.ts +7 -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/storage.ts +7 -0
- package/src/resources/tenant-events.ts +32 -0
- package/src/resources/tenant.ts +82 -3
- package/src/resources/volumes.ts +7 -0
- package/src/resources/webhooks.ts +55 -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/src/types.ts +7 -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,14 @@ 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";
|
|
47
|
+
import { Workspaces } from "./resources/workspaces.js";
|
|
41
48
|
import type { MiosaClientConfig } from "./types.js";
|
|
42
49
|
|
|
43
50
|
const DEFAULT_BASE_URL = "https://api.miosa.ai/api/v1";
|
|
@@ -58,6 +65,24 @@ const DEFAULT_MAX_RETRIES = 3;
|
|
|
58
65
|
* ```
|
|
59
66
|
*/
|
|
60
67
|
export class Miosa {
|
|
68
|
+
/** Workspace CRUD — create, list, get, update, delete, and sub-resource queries. */
|
|
69
|
+
readonly workspaces: Workspaces;
|
|
70
|
+
|
|
71
|
+
/** Per-workspace user roster — list, add, update role, remove. */
|
|
72
|
+
readonly workspaceMembers: WorkspaceMembers;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Workspace invite flow — create invite, list, revoke, preview, accept.
|
|
76
|
+
* Sending to an email already in the org adds the user directly.
|
|
77
|
+
*/
|
|
78
|
+
readonly workspaceInvites: WorkspaceInvites;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Org invite flow — create invite, list, revoke, preview, accept.
|
|
82
|
+
* Requires admin/owner role for write operations.
|
|
83
|
+
*/
|
|
84
|
+
readonly orgInvites: OrgInvites;
|
|
85
|
+
|
|
61
86
|
/** Current tenant plan, limits, and live usage counters. */
|
|
62
87
|
readonly tenant: Tenant;
|
|
63
88
|
|
|
@@ -183,6 +208,17 @@ export class Miosa {
|
|
|
183
208
|
/** Admin: fleet-wide snapshot index. */
|
|
184
209
|
readonly snapshotsStandalone: SnapshotsStandalone;
|
|
185
210
|
|
|
211
|
+
// ── Egress (security) namespaces ───────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/** Encrypted secret + OAuth credential vault (`/egress/secrets`). */
|
|
214
|
+
readonly secrets: EgressSecrets;
|
|
215
|
+
|
|
216
|
+
/** Egress allowlist + policies — host-level firewall (`/egress/policies`). */
|
|
217
|
+
readonly network: EgressNetwork;
|
|
218
|
+
|
|
219
|
+
/** Egress audit log — every outbound request, paginated query + tail. */
|
|
220
|
+
readonly audit: EgressAudit;
|
|
221
|
+
|
|
186
222
|
private readonly http: HttpClient;
|
|
187
223
|
|
|
188
224
|
constructor(config: MiosaClientConfig) {
|
|
@@ -206,6 +242,10 @@ export class Miosa {
|
|
|
206
242
|
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
207
243
|
});
|
|
208
244
|
|
|
245
|
+
this.workspaces = new Workspaces(this.http);
|
|
246
|
+
this.workspaceMembers = new WorkspaceMembers(this.http);
|
|
247
|
+
this.workspaceInvites = new WorkspaceInvites(this.http);
|
|
248
|
+
this.orgInvites = new OrgInvites(this.http);
|
|
209
249
|
this.tenant = new Tenant(this.http);
|
|
210
250
|
this.regions = new Regions(this.http);
|
|
211
251
|
this.settings = new Settings(this.http);
|
|
@@ -245,5 +285,9 @@ export class Miosa {
|
|
|
245
285
|
this.email = new Email(this.http);
|
|
246
286
|
this.builderSessions = new BuilderSessions(this.http);
|
|
247
287
|
this.snapshotsStandalone = new SnapshotsStandalone(this.http);
|
|
288
|
+
// Egress (security) namespaces
|
|
289
|
+
this.secrets = new EgressSecrets(this.http);
|
|
290
|
+
this.network = new EgressNetwork(this.http);
|
|
291
|
+
this.audit = new EgressAudit(this.http);
|
|
248
292
|
}
|
|
249
293
|
}
|
package/src/index.ts
CHANGED
|
@@ -245,6 +245,54 @@ export type {
|
|
|
245
245
|
ApiKeyCreateParams,
|
|
246
246
|
} from "./resources/api-keys.js";
|
|
247
247
|
|
|
248
|
+
// Workspace CRUD
|
|
249
|
+
export { Workspaces } from "./resources/workspaces.js";
|
|
250
|
+
export type {
|
|
251
|
+
WorkspaceId as TenantWorkspaceId,
|
|
252
|
+
WorkspaceData,
|
|
253
|
+
WorkspaceCreateParams,
|
|
254
|
+
WorkspaceUpdateParams,
|
|
255
|
+
WorkspaceComputerTemplateCreateParams,
|
|
256
|
+
} from "./resources/workspaces.js";
|
|
257
|
+
|
|
258
|
+
// Members & Invites
|
|
259
|
+
export { WorkspaceMembers } from "./resources/workspace-members.js";
|
|
260
|
+
export type {
|
|
261
|
+
WorkspaceRole,
|
|
262
|
+
WorkspaceMember,
|
|
263
|
+
WorkspaceMemberRecord,
|
|
264
|
+
AddWorkspaceMemberParams,
|
|
265
|
+
UpdateWorkspaceMemberRoleParams,
|
|
266
|
+
WorkspaceMemberListResponse,
|
|
267
|
+
WorkspaceMemberRecordResponse,
|
|
268
|
+
WorkspaceMemberDeleteResponse,
|
|
269
|
+
} from "./resources/workspace-members.js";
|
|
270
|
+
export { WorkspaceInvites } from "./resources/workspace-invites.js";
|
|
271
|
+
export type {
|
|
272
|
+
WorkspaceInvite,
|
|
273
|
+
WorkspaceInvitePreview,
|
|
274
|
+
CreateWorkspaceInviteParams,
|
|
275
|
+
CreateWorkspaceInviteResponse,
|
|
276
|
+
WorkspaceInviteCreatedResponse,
|
|
277
|
+
WorkspaceMemberAddedResponse,
|
|
278
|
+
WorkspaceInviteListResponse,
|
|
279
|
+
WorkspaceInviteRevokeResponse,
|
|
280
|
+
AcceptWorkspaceInviteResponse,
|
|
281
|
+
} from "./resources/workspace-invites.js";
|
|
282
|
+
export { OrgInvites } from "./resources/org-invites.js";
|
|
283
|
+
export type {
|
|
284
|
+
OrgRole,
|
|
285
|
+
OrgInvite,
|
|
286
|
+
OrgInviteCreated,
|
|
287
|
+
OrgInvitePreview,
|
|
288
|
+
TenantSummary,
|
|
289
|
+
CreateOrgInviteParams,
|
|
290
|
+
OrgInviteCreatedResponse,
|
|
291
|
+
OrgInviteListResponse,
|
|
292
|
+
OrgInviteRevokeResponse,
|
|
293
|
+
AcceptOrgInviteResponse,
|
|
294
|
+
} from "./resources/org-invites.js";
|
|
295
|
+
|
|
248
296
|
// P2 resources
|
|
249
297
|
export { Tenant } from "./resources/tenant.js";
|
|
250
298
|
export type { TenantPlan } from "./resources/tenant.js";
|
|
@@ -369,6 +417,58 @@ export {
|
|
|
369
417
|
SandboxTags,
|
|
370
418
|
} from "./resources/sandboxes.js";
|
|
371
419
|
|
|
420
|
+
// Egress (security) — secrets, network, audit
|
|
421
|
+
export {
|
|
422
|
+
EgressSecrets,
|
|
423
|
+
OAuthFlow,
|
|
424
|
+
SandboxSecrets,
|
|
425
|
+
ComputerSecrets,
|
|
426
|
+
} from "./resources/egressSecrets.js";
|
|
427
|
+
export type {
|
|
428
|
+
EgressSecretType,
|
|
429
|
+
EgressSecretScope,
|
|
430
|
+
EgressSecretData,
|
|
431
|
+
EgressBindingData,
|
|
432
|
+
OauthProvider,
|
|
433
|
+
SecretSetParams,
|
|
434
|
+
SecretListParams,
|
|
435
|
+
SecretRotateParams,
|
|
436
|
+
BindingCreateParams,
|
|
437
|
+
BindingListParams,
|
|
438
|
+
OauthConnectParams,
|
|
439
|
+
OauthStartResult,
|
|
440
|
+
OauthStatusResult,
|
|
441
|
+
} from "./resources/egressSecrets.js";
|
|
442
|
+
export {
|
|
443
|
+
EgressNetwork,
|
|
444
|
+
SandboxNetwork,
|
|
445
|
+
ComputerNetwork,
|
|
446
|
+
} from "./resources/egressNetwork.js";
|
|
447
|
+
export type {
|
|
448
|
+
EgressPolicyMode,
|
|
449
|
+
EgressRuleEffect,
|
|
450
|
+
EgressPolicyData,
|
|
451
|
+
EgressAllowlistRule,
|
|
452
|
+
EgressSuggestion,
|
|
453
|
+
AllowParams,
|
|
454
|
+
PolicyCreateParams,
|
|
455
|
+
PolicyUpdateParams,
|
|
456
|
+
ModeParams,
|
|
457
|
+
SuggestionsParams,
|
|
458
|
+
PolicyListParams,
|
|
459
|
+
RulesListParams,
|
|
460
|
+
} from "./resources/egressNetwork.js";
|
|
461
|
+
export {
|
|
462
|
+
EgressAudit,
|
|
463
|
+
SandboxAudit,
|
|
464
|
+
ComputerAudit,
|
|
465
|
+
} from "./resources/egressAudit.js";
|
|
466
|
+
export type {
|
|
467
|
+
EgressAuditEvent,
|
|
468
|
+
AuditListParams,
|
|
469
|
+
AuditTailParams,
|
|
470
|
+
} from "./resources/egressAudit.js";
|
|
471
|
+
|
|
372
472
|
// Error types
|
|
373
473
|
export {
|
|
374
474
|
MiosaError,
|
|
@@ -47,6 +47,9 @@ export interface ApiKeyCreateParams {
|
|
|
47
47
|
expires_at?: string;
|
|
48
48
|
expiresAt?: string;
|
|
49
49
|
idempotencyKey?: string;
|
|
50
|
+
/** Scope this key to a single workspace. Omit for a tenant-wide key. */
|
|
51
|
+
workspaceId?: string;
|
|
52
|
+
workspace_id?: string;
|
|
50
53
|
[key: string]: unknown;
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -100,10 +103,11 @@ export class ApiKeys {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
async create(params: ApiKeyCreateParams): Promise<ApiKeyCreateResult> {
|
|
103
|
-
const { idempotencyKey: ikey, expiresAt, ...rest } = params;
|
|
106
|
+
const { idempotencyKey: ikey, expiresAt, workspaceId, ...rest } = params;
|
|
104
107
|
const body = stripUndefined({
|
|
105
108
|
...rest,
|
|
106
109
|
expires_at: expiresAt ?? rest.expires_at,
|
|
110
|
+
workspace_id: workspaceId ?? rest.workspace_id,
|
|
107
111
|
});
|
|
108
112
|
const data = await this.http.request<unknown>("/api-keys", {
|
|
109
113
|
method: "POST",
|
|
@@ -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 {
|
|
@@ -55,6 +55,13 @@ export interface CronJobCreateParams {
|
|
|
55
55
|
name: string;
|
|
56
56
|
schedule: string;
|
|
57
57
|
idempotencyKey?: string;
|
|
58
|
+
// White-label attribution
|
|
59
|
+
externalWorkspaceId?: string;
|
|
60
|
+
external_workspace_id?: string;
|
|
61
|
+
externalUserId?: string;
|
|
62
|
+
external_user_id?: string;
|
|
63
|
+
externalProjectId?: string;
|
|
64
|
+
external_project_id?: string;
|
|
58
65
|
[key: string]: unknown;
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -27,6 +27,13 @@ export interface CustomDomainData {
|
|
|
27
27
|
|
|
28
28
|
export interface CustomDomainRegisterParams {
|
|
29
29
|
fqdn: string;
|
|
30
|
+
// White-label attribution
|
|
31
|
+
externalWorkspaceId?: string;
|
|
32
|
+
external_workspace_id?: string;
|
|
33
|
+
externalUserId?: string;
|
|
34
|
+
external_user_id?: string;
|
|
35
|
+
externalProjectId?: string;
|
|
36
|
+
external_project_id?: string;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
// ─── CustomDomains resource ──────────────────────────────────────────────────
|
|
@@ -95,7 +95,7 @@ export interface DeploymentData {
|
|
|
95
95
|
* with `source_sandbox_id` on the version row. Will become nullable.
|
|
96
96
|
*/
|
|
97
97
|
repo_url?: string;
|
|
98
|
-
repo_provider?: "github";
|
|
98
|
+
repo_provider?: "github" | "gitlab" | "bitbucket";
|
|
99
99
|
branch?: string;
|
|
100
100
|
build_command?: string | null;
|
|
101
101
|
run_command?: string | null;
|
|
@@ -278,6 +278,9 @@ export interface DeploymentCreateParams extends ExternalAttribution {
|
|
|
278
278
|
autoDeploy?: boolean;
|
|
279
279
|
database?: DeploymentDatabaseRequest;
|
|
280
280
|
metadata?: Record<string, unknown>;
|
|
281
|
+
/** Pin build and runtime VMs to a specific DC. One of: "us-west", "us-east", "us-mia". */
|
|
282
|
+
target_region?: string;
|
|
283
|
+
targetRegion?: string;
|
|
281
284
|
idempotencyKey?: string;
|
|
282
285
|
}
|
|
283
286
|
|
|
@@ -617,6 +620,7 @@ export class Deployments {
|
|
|
617
620
|
auto_deploy: params.autoDeploy ?? params.auto_deploy,
|
|
618
621
|
database: params.database,
|
|
619
622
|
metadata: params.metadata,
|
|
623
|
+
target_region: params.targetRegion ?? params.target_region,
|
|
620
624
|
...attributionBody(params),
|
|
621
625
|
});
|
|
622
626
|
const data = await this.http.request<unknown>("/deployments", {
|
|
@@ -735,9 +739,16 @@ export class Deployments {
|
|
|
735
739
|
return unwrap(data) as DeploymentBuildData;
|
|
736
740
|
}
|
|
737
741
|
|
|
738
|
-
async listEnv(
|
|
742
|
+
async listEnv(
|
|
743
|
+
deploymentId: string,
|
|
744
|
+
opts: { environment?: string } = {},
|
|
745
|
+
): Promise<Record<string, unknown>[]> {
|
|
746
|
+
const query = stripUndefined({ environment: opts.environment });
|
|
739
747
|
const data = await this.http.get<unknown>(
|
|
740
748
|
`/deployments/${deploymentId}/env`,
|
|
749
|
+
Object.keys(query).length
|
|
750
|
+
? (query as Record<string, string | number | boolean | undefined>)
|
|
751
|
+
: undefined,
|
|
741
752
|
);
|
|
742
753
|
return listItems<Record<string, unknown>>(data);
|
|
743
754
|
}
|
|
@@ -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
|
+
});
|