@miosa/sdk 1.1.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 +26 -229
- package/dist/index.js +25 -276
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +0 -5
- package/src/index.ts +0 -10
- package/src/resources/admin.ts +11 -0
- package/src/resources/api-keys.ts +17 -5
- package/src/resources/cron-jobs.ts +0 -7
- package/src/resources/custom_domains.ts +0 -7
- package/src/resources/deployments.ts +2 -13
- package/src/resources/flat-custom-domains.ts +0 -7
- package/src/resources/functions.ts +0 -7
- package/src/resources/governance.test.ts +355 -0
- package/src/resources/governance.ts +528 -0
- package/src/resources/storage.ts +0 -7
- package/src/resources/tenant.ts +3 -82
- package/src/resources/volumes.ts +0 -7
- package/src/resources/webhooks.ts +0 -55
- package/src/types.ts +0 -7
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -44,7 +44,6 @@ import { Volumes } from "./resources/volumes.js";
|
|
|
44
44
|
import { Webhooks } from "./resources/webhooks.js";
|
|
45
45
|
import { WorkspaceInvites } from "./resources/workspace-invites.js";
|
|
46
46
|
import { WorkspaceMembers } from "./resources/workspace-members.js";
|
|
47
|
-
import { Workspaces } from "./resources/workspaces.js";
|
|
48
47
|
import type { MiosaClientConfig } from "./types.js";
|
|
49
48
|
|
|
50
49
|
const DEFAULT_BASE_URL = "https://api.miosa.ai/api/v1";
|
|
@@ -65,9 +64,6 @@ const DEFAULT_MAX_RETRIES = 3;
|
|
|
65
64
|
* ```
|
|
66
65
|
*/
|
|
67
66
|
export class Miosa {
|
|
68
|
-
/** Workspace CRUD — create, list, get, update, delete, and sub-resource queries. */
|
|
69
|
-
readonly workspaces: Workspaces;
|
|
70
|
-
|
|
71
67
|
/** Per-workspace user roster — list, add, update role, remove. */
|
|
72
68
|
readonly workspaceMembers: WorkspaceMembers;
|
|
73
69
|
|
|
@@ -242,7 +238,6 @@ export class Miosa {
|
|
|
242
238
|
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
243
239
|
});
|
|
244
240
|
|
|
245
|
-
this.workspaces = new Workspaces(this.http);
|
|
246
241
|
this.workspaceMembers = new WorkspaceMembers(this.http);
|
|
247
242
|
this.workspaceInvites = new WorkspaceInvites(this.http);
|
|
248
243
|
this.orgInvites = new OrgInvites(this.http);
|
package/src/index.ts
CHANGED
|
@@ -245,16 +245,6 @@ 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
248
|
// Members & Invites
|
|
259
249
|
export { WorkspaceMembers } from "./resources/workspace-members.js";
|
|
260
250
|
export type {
|
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
|
}
|
|
@@ -47,9 +47,6 @@ 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;
|
|
53
50
|
[key: string]: unknown;
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -103,11 +100,10 @@ export class ApiKeys {
|
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
async create(params: ApiKeyCreateParams): Promise<ApiKeyCreateResult> {
|
|
106
|
-
const { idempotencyKey: ikey, expiresAt,
|
|
103
|
+
const { idempotencyKey: ikey, expiresAt, ...rest } = params;
|
|
107
104
|
const body = stripUndefined({
|
|
108
105
|
...rest,
|
|
109
106
|
expires_at: expiresAt ?? rest.expires_at,
|
|
110
|
-
workspace_id: workspaceId ?? rest.workspace_id,
|
|
111
107
|
});
|
|
112
108
|
const data = await this.http.request<unknown>("/api-keys", {
|
|
113
109
|
method: "POST",
|
|
@@ -117,6 +113,22 @@ export class ApiKeys {
|
|
|
117
113
|
return unwrap<ApiKeyCreateResult>(data);
|
|
118
114
|
}
|
|
119
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
|
+
|
|
120
132
|
async delete(keyId: string): Promise<void> {
|
|
121
133
|
await this.http.delete<unknown>(`/api-keys/${keyId}`);
|
|
122
134
|
}
|
|
@@ -55,13 +55,6 @@ 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;
|
|
65
58
|
[key: string]: unknown;
|
|
66
59
|
}
|
|
67
60
|
|
|
@@ -27,13 +27,6 @@ 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;
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
// ─── 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";
|
|
99
99
|
branch?: string;
|
|
100
100
|
build_command?: string | null;
|
|
101
101
|
run_command?: string | null;
|
|
@@ -278,9 +278,6 @@ 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;
|
|
284
281
|
idempotencyKey?: string;
|
|
285
282
|
}
|
|
286
283
|
|
|
@@ -620,7 +617,6 @@ export class Deployments {
|
|
|
620
617
|
auto_deploy: params.autoDeploy ?? params.auto_deploy,
|
|
621
618
|
database: params.database,
|
|
622
619
|
metadata: params.metadata,
|
|
623
|
-
target_region: params.targetRegion ?? params.target_region,
|
|
624
620
|
...attributionBody(params),
|
|
625
621
|
});
|
|
626
622
|
const data = await this.http.request<unknown>("/deployments", {
|
|
@@ -739,16 +735,9 @@ export class Deployments {
|
|
|
739
735
|
return unwrap(data) as DeploymentBuildData;
|
|
740
736
|
}
|
|
741
737
|
|
|
742
|
-
async listEnv(
|
|
743
|
-
deploymentId: string,
|
|
744
|
-
opts: { environment?: string } = {},
|
|
745
|
-
): Promise<Record<string, unknown>[]> {
|
|
746
|
-
const query = stripUndefined({ environment: opts.environment });
|
|
738
|
+
async listEnv(deploymentId: string): Promise<Record<string, unknown>[]> {
|
|
747
739
|
const data = await this.http.get<unknown>(
|
|
748
740
|
`/deployments/${deploymentId}/env`,
|
|
749
|
-
Object.keys(query).length
|
|
750
|
-
? (query as Record<string, string | number | boolean | undefined>)
|
|
751
|
-
: undefined,
|
|
752
741
|
);
|
|
753
742
|
return listItems<Record<string, unknown>>(data);
|
|
754
743
|
}
|
|
@@ -47,13 +47,6 @@ export interface CustomDomainCreateParams {
|
|
|
47
47
|
redirect_policy?: "none" | "www_to_apex" | "apex_to_www";
|
|
48
48
|
redirectPolicy?: "none" | "www_to_apex" | "apex_to_www";
|
|
49
49
|
idempotencyKey?: string;
|
|
50
|
-
// White-label attribution
|
|
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
50
|
[key: string]: unknown;
|
|
58
51
|
}
|
|
59
52
|
|
|
@@ -46,13 +46,6 @@ export interface FunctionCreateParams {
|
|
|
46
46
|
timeoutSec?: number;
|
|
47
47
|
env?: Record<string, string>;
|
|
48
48
|
idempotencyKey?: string;
|
|
49
|
-
// White-label attribution
|
|
50
|
-
externalWorkspaceId?: string;
|
|
51
|
-
external_workspace_id?: string;
|
|
52
|
-
externalUserId?: string;
|
|
53
|
-
external_user_id?: string;
|
|
54
|
-
externalProjectId?: string;
|
|
55
|
-
external_project_id?: string;
|
|
56
49
|
[key: string]: unknown;
|
|
57
50
|
}
|
|
58
51
|
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { HttpClient } from "../http.js";
|
|
3
|
+
import {
|
|
4
|
+
BillingResource,
|
|
5
|
+
BulkResource,
|
|
6
|
+
ExternalUsers,
|
|
7
|
+
GovernanceTenant,
|
|
8
|
+
GovernanceWorkspaces,
|
|
9
|
+
} from "./governance.js";
|
|
10
|
+
import { ApiKeys } from "./api-keys.js";
|
|
11
|
+
import { Admin } from "./admin.js";
|
|
12
|
+
|
|
13
|
+
const mockGet = vi.fn();
|
|
14
|
+
const mockPost = vi.fn();
|
|
15
|
+
const mockPut = vi.fn();
|
|
16
|
+
const mockPatch = vi.fn();
|
|
17
|
+
const mockDelete = vi.fn();
|
|
18
|
+
const mockStream = vi.fn();
|
|
19
|
+
const mockRequest = vi.fn();
|
|
20
|
+
|
|
21
|
+
function makeHttp(): HttpClient {
|
|
22
|
+
return {
|
|
23
|
+
get: mockGet,
|
|
24
|
+
post: mockPost,
|
|
25
|
+
put: mockPut,
|
|
26
|
+
patch: mockPatch,
|
|
27
|
+
delete: mockDelete,
|
|
28
|
+
request: mockRequest,
|
|
29
|
+
stream: mockStream,
|
|
30
|
+
} as unknown as HttpClient;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.resetAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── GovernanceTenant.policy ───────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe("GovernanceTenant.policy", () => {
|
|
40
|
+
it("get calls GET /tenant/policy and unwraps data", async () => {
|
|
41
|
+
mockGet.mockResolvedValue({ data: { quotas: { max_sandboxes: 10 } } });
|
|
42
|
+
const t = new GovernanceTenant(makeHttp());
|
|
43
|
+
const result = await t.policy.get();
|
|
44
|
+
expect(mockGet).toHaveBeenCalledWith("/tenant/policy");
|
|
45
|
+
expect((result as Record<string, unknown>).quotas).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("set calls PUT /tenant/policy with body", async () => {
|
|
49
|
+
mockPut.mockResolvedValue({ data: { quotas: { max_sandboxes: 5 } } });
|
|
50
|
+
const t = new GovernanceTenant(makeHttp());
|
|
51
|
+
await t.policy.set({ quotas: { max_sandboxes: 5 } });
|
|
52
|
+
expect(mockPut).toHaveBeenCalledWith("/tenant/policy", {
|
|
53
|
+
quotas: { max_sandboxes: 5 },
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("delete calls DELETE /tenant/policy", async () => {
|
|
58
|
+
mockDelete.mockResolvedValue(null);
|
|
59
|
+
const t = new GovernanceTenant(makeHttp());
|
|
60
|
+
await t.policy.delete();
|
|
61
|
+
expect(mockDelete).toHaveBeenCalledWith("/tenant/policy");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── GovernanceTenant.members ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("GovernanceTenant.members", () => {
|
|
68
|
+
it("list returns members array", async () => {
|
|
69
|
+
mockGet.mockResolvedValue({
|
|
70
|
+
data: [{ id: "m1", email: "a@b.com", role: "admin" }],
|
|
71
|
+
});
|
|
72
|
+
const t = new GovernanceTenant(makeHttp());
|
|
73
|
+
const members = await t.members.list();
|
|
74
|
+
expect(members).toHaveLength(1);
|
|
75
|
+
expect(members[0].role).toBe("admin");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("invite posts email and role", async () => {
|
|
79
|
+
mockPost.mockResolvedValue({ data: { id: "m2", role: "developer" } });
|
|
80
|
+
const t = new GovernanceTenant(makeHttp());
|
|
81
|
+
const result = await t.members.invite("x@y.com", "developer");
|
|
82
|
+
expect(mockPost).toHaveBeenCalledWith("/tenant/members", {
|
|
83
|
+
email: "x@y.com",
|
|
84
|
+
role: "developer",
|
|
85
|
+
});
|
|
86
|
+
expect(result.role).toBe("developer");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("updateRole patches member role", async () => {
|
|
90
|
+
mockPatch.mockResolvedValue({ data: { id: "m1", role: "admin" } });
|
|
91
|
+
const t = new GovernanceTenant(makeHttp());
|
|
92
|
+
await t.members.updateRole("m1", "admin");
|
|
93
|
+
expect(mockPatch).toHaveBeenCalledWith("/tenant/members/m1/role", {
|
|
94
|
+
role: "admin",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("remove deletes member", async () => {
|
|
99
|
+
mockDelete.mockResolvedValue(null);
|
|
100
|
+
const t = new GovernanceTenant(makeHttp());
|
|
101
|
+
await t.members.remove("m1");
|
|
102
|
+
expect(mockDelete).toHaveBeenCalledWith("/tenant/members/m1");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("transferOwnership posts new_owner_user_id", async () => {
|
|
106
|
+
mockPost.mockResolvedValue({ data: { transferred: true } });
|
|
107
|
+
const t = new GovernanceTenant(makeHttp());
|
|
108
|
+
await t.members.transferOwnership("user_new");
|
|
109
|
+
expect(mockPost).toHaveBeenCalledWith("/tenant/transfer-ownership", {
|
|
110
|
+
new_owner_user_id: "user_new",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── GovernanceWorkspaces ──────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe("GovernanceWorkspaces", () => {
|
|
118
|
+
it("list returns workspace array", async () => {
|
|
119
|
+
mockGet.mockResolvedValue({ data: [{ id: "ws_1", name: "alpha" }] });
|
|
120
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
121
|
+
const items = await gw.list();
|
|
122
|
+
expect(items).toHaveLength(1);
|
|
123
|
+
expect(items[0].id).toBe("ws_1");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("create posts name", async () => {
|
|
127
|
+
mockPost.mockResolvedValue({ data: { id: "ws_2", name: "beta" } });
|
|
128
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
129
|
+
await gw.create("beta");
|
|
130
|
+
expect(mockPost).toHaveBeenCalledWith("/workspaces", { name: "beta" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("workspace(id).policy.get calls correct path", async () => {
|
|
134
|
+
mockGet.mockResolvedValue({ data: { quotas: {} } });
|
|
135
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
136
|
+
await gw.workspace("ws_1").policy.get();
|
|
137
|
+
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws_1/policy");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("workspace(id).policy.set calls PUT", async () => {
|
|
141
|
+
mockPut.mockResolvedValue({ data: {} });
|
|
142
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
143
|
+
await gw.workspace("ws_1").policy.set({ quotas: { max_sandboxes: 3 } });
|
|
144
|
+
expect(mockPut).toHaveBeenCalledWith("/workspaces/ws_1/policy", {
|
|
145
|
+
quotas: { max_sandboxes: 3 },
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("workspace(id).members.list calls correct path", async () => {
|
|
150
|
+
mockGet.mockResolvedValue({ data: [{ id: "m1", role: "developer" }] });
|
|
151
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
152
|
+
const members = await gw.workspace("ws_1").members.list();
|
|
153
|
+
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws_1/members");
|
|
154
|
+
expect(members[0].role).toBe("developer");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("workspace(id).members.invite posts correctly", async () => {
|
|
158
|
+
mockPost.mockResolvedValue({ data: { id: "m2" } });
|
|
159
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
160
|
+
await gw.workspace("ws_1").members.invite("bob@acme.com", "viewer");
|
|
161
|
+
expect(mockPost).toHaveBeenCalledWith("/workspaces/ws_1/members", {
|
|
162
|
+
email: "bob@acme.com",
|
|
163
|
+
role: "viewer",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("workspace(id).transfer posts resource_ids and target", async () => {
|
|
168
|
+
mockPost.mockResolvedValue({ data: { transferred: 2 } });
|
|
169
|
+
const gw = new GovernanceWorkspaces(makeHttp());
|
|
170
|
+
await gw.workspace("ws_1").transfer(["sbx_1", "sbx_2"], "ws_2");
|
|
171
|
+
expect(mockPost).toHaveBeenCalledWith("/workspaces/ws_1/transfer", {
|
|
172
|
+
resource_ids: ["sbx_1", "sbx_2"],
|
|
173
|
+
target_workspace_id: "ws_2",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── ExternalUsers.policy ──────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe("ExternalUsers.policy", () => {
|
|
181
|
+
it("get calls correct path", async () => {
|
|
182
|
+
mockGet.mockResolvedValue({ data: { lifecycle: {} } });
|
|
183
|
+
const eu = new ExternalUsers(makeHttp());
|
|
184
|
+
await eu.call("alice").policy.get();
|
|
185
|
+
expect(mockGet).toHaveBeenCalledWith("/external-users/alice/policy");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("effective returns typed EffectivePolicyDoc with source fields", async () => {
|
|
189
|
+
mockGet.mockResolvedValue({
|
|
190
|
+
lifecycle: {
|
|
191
|
+
default_idle_timeout_sec: { value: 600, source: "user" },
|
|
192
|
+
default_timeout_sec: { value: 86400, source: "tenant" },
|
|
193
|
+
},
|
|
194
|
+
quotas: {
|
|
195
|
+
max_sandboxes: { value: 5, source: "workspace" },
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const eu = new ExternalUsers(makeHttp());
|
|
199
|
+
const eff = await eu.call("alice").policy.effective();
|
|
200
|
+
expect(eff.lifecycle["default_idle_timeout_sec"]?.value).toBe(600);
|
|
201
|
+
expect(eff.lifecycle["default_idle_timeout_sec"]?.source).toBe("user");
|
|
202
|
+
expect(eff.quotas["max_sandboxes"]?.value).toBe(5);
|
|
203
|
+
expect(eff.quotas["max_sandboxes"]?.source).toBe("workspace");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── BulkResource ──────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
describe("BulkResource", () => {
|
|
210
|
+
it("sandboxes.pause posts ids", async () => {
|
|
211
|
+
mockPost.mockResolvedValue({ queued: 3, job_id: "job_1" });
|
|
212
|
+
const bulk = new BulkResource(makeHttp());
|
|
213
|
+
const result = await bulk.sandboxes.pause({
|
|
214
|
+
ids: ["sbx_1", "sbx_2", "sbx_3"],
|
|
215
|
+
});
|
|
216
|
+
expect(mockPost).toHaveBeenCalledWith("/bulk/sandboxes/pause", {
|
|
217
|
+
ids: ["sbx_1", "sbx_2", "sbx_3"],
|
|
218
|
+
});
|
|
219
|
+
expect(result.job_id).toBe("job_1");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("sandboxes.destroy posts filter", async () => {
|
|
223
|
+
mockPost.mockResolvedValue({ queued: 5, job_id: "job_2" });
|
|
224
|
+
const bulk = new BulkResource(makeHttp());
|
|
225
|
+
const result = await bulk.sandboxes.destroy({ filter: { state: "idle" } });
|
|
226
|
+
expect(mockPost).toHaveBeenCalledWith("/bulk/sandboxes/destroy", {
|
|
227
|
+
filter: { state: "idle" },
|
|
228
|
+
});
|
|
229
|
+
expect(result.queued).toBe(5);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("policy.apply posts tier + ids + policy", async () => {
|
|
233
|
+
mockPost.mockResolvedValue({ queued: 10, job_id: "job_3" });
|
|
234
|
+
const bulk = new BulkResource(makeHttp());
|
|
235
|
+
await bulk.policy.apply({
|
|
236
|
+
tier: "external_user",
|
|
237
|
+
idsOrFilter: ["u1", "u2"],
|
|
238
|
+
policy: { quotas: { max_sandboxes: 3 } },
|
|
239
|
+
});
|
|
240
|
+
expect(mockPost).toHaveBeenCalledWith("/bulk/policy/apply", {
|
|
241
|
+
tier: "external_user",
|
|
242
|
+
ids: ["u1", "u2"],
|
|
243
|
+
policy: { quotas: { max_sandboxes: 3 } },
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("jobs.get returns job status", async () => {
|
|
248
|
+
mockGet.mockResolvedValue({
|
|
249
|
+
data: { id: "job_1", status: "completed", processed: 3 },
|
|
250
|
+
});
|
|
251
|
+
const bulk = new BulkResource(makeHttp());
|
|
252
|
+
const job = await bulk.jobs.get("job_1");
|
|
253
|
+
expect(job.status).toBe("completed");
|
|
254
|
+
expect(job.processed).toBe(3);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── BillingResource ───────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe("BillingResource", () => {
|
|
261
|
+
it("invoices.list returns array", async () => {
|
|
262
|
+
mockGet.mockResolvedValue({
|
|
263
|
+
data: [{ id: "inv_1", amount: 5000 }],
|
|
264
|
+
});
|
|
265
|
+
const billing = new BillingResource(makeHttp());
|
|
266
|
+
const invoices = await billing.invoices.list();
|
|
267
|
+
expect(invoices[0].id).toBe("inv_1");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("invoices.get returns single invoice", async () => {
|
|
271
|
+
mockGet.mockResolvedValue({
|
|
272
|
+
data: { id: "inv_1", amount: 5000, line_items: [] },
|
|
273
|
+
});
|
|
274
|
+
const billing = new BillingResource(makeHttp());
|
|
275
|
+
const inv = await billing.invoices.get("inv_1");
|
|
276
|
+
expect(inv.id).toBe("inv_1");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("paymentMethods returns list", async () => {
|
|
280
|
+
mockGet.mockResolvedValue({
|
|
281
|
+
data: [{ id: "pm_1", brand: "visa", last4: "4242" }],
|
|
282
|
+
});
|
|
283
|
+
const billing = new BillingResource(makeHttp());
|
|
284
|
+
const methods = await billing.paymentMethods();
|
|
285
|
+
expect(methods[0].brand).toBe("visa");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("upcoming returns preview", async () => {
|
|
289
|
+
mockGet.mockResolvedValue({
|
|
290
|
+
data: { amount: 1200, period_end: "2026-06-01" },
|
|
291
|
+
});
|
|
292
|
+
const billing = new BillingResource(makeHttp());
|
|
293
|
+
const upcoming = await billing.upcoming();
|
|
294
|
+
expect((upcoming as Record<string, unknown>).amount).toBe(1200);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── ApiKeys.createScoped ──────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
describe("ApiKeys.createScoped", () => {
|
|
301
|
+
it("posts to /api-keys/scoped with correct body", async () => {
|
|
302
|
+
mockPost.mockResolvedValue({
|
|
303
|
+
data: { id: "key_1", token: "msk_l2_abc" },
|
|
304
|
+
});
|
|
305
|
+
const keys = new ApiKeys(makeHttp());
|
|
306
|
+
const result = await keys.createScoped({
|
|
307
|
+
externalUserId: "alice-42",
|
|
308
|
+
scopes: ["sandboxes:read", "sandboxes:exec"],
|
|
309
|
+
expiresAt: "2026-12-31T00:00:00Z",
|
|
310
|
+
});
|
|
311
|
+
expect(mockPost).toHaveBeenCalledWith("/api-keys/scoped", {
|
|
312
|
+
external_user_id: "alice-42",
|
|
313
|
+
scopes: ["sandboxes:read", "sandboxes:exec"],
|
|
314
|
+
expires_at: "2026-12-31T00:00:00Z",
|
|
315
|
+
});
|
|
316
|
+
expect(result.token).toBe("msk_l2_abc");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("omits expires_at when not provided", async () => {
|
|
320
|
+
mockPost.mockResolvedValue({ data: { id: "key_2" } });
|
|
321
|
+
const keys = new ApiKeys(makeHttp());
|
|
322
|
+
await keys.createScoped({
|
|
323
|
+
externalUserId: "bob",
|
|
324
|
+
scopes: ["sandboxes:read"],
|
|
325
|
+
});
|
|
326
|
+
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
|
|
327
|
+
expect(body["expires_at"]).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ── Admin.impersonate ─────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
describe("Admin.impersonate", () => {
|
|
334
|
+
it("posts to /admin/impersonate with external_user_id and ttl_sec", async () => {
|
|
335
|
+
mockPost.mockResolvedValue({
|
|
336
|
+
token: "msi_abc",
|
|
337
|
+
expires_at: "2026-06-01T00:00:00Z",
|
|
338
|
+
});
|
|
339
|
+
const admin = new Admin(makeHttp());
|
|
340
|
+
const result = await admin.impersonate("alice-42", { ttlSec: 1800 });
|
|
341
|
+
expect(mockPost).toHaveBeenCalledWith("/admin/impersonate", {
|
|
342
|
+
external_user_id: "alice-42",
|
|
343
|
+
ttl_sec: 1800,
|
|
344
|
+
});
|
|
345
|
+
expect(result.token).toBe("msi_abc");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("defaults ttl_sec to 3600", async () => {
|
|
349
|
+
mockPost.mockResolvedValue({ token: "msi_xyz" });
|
|
350
|
+
const admin = new Admin(makeHttp());
|
|
351
|
+
await admin.impersonate("bob");
|
|
352
|
+
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
|
|
353
|
+
expect(body["ttl_sec"]).toBe(3600);
|
|
354
|
+
});
|
|
355
|
+
});
|