@miosa/sdk 1.2.0 → 1.2.1
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/README.md +1 -1
- package/dist/index.d.ts +10 -18
- package/dist/index.js +8 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/resources/admin.ts +0 -11
- package/src/resources/api-keys.ts +0 -16
- package/src/resources/computer.ts +9 -4
- package/src/resources/custom_domains.ts +1 -1
- package/src/resources/sandboxes.test.ts +10 -7
- package/src/types.ts +5 -2
- package/src/resources/governance.test.ts +0 -355
- package/src/resources/governance.ts +0 -528
- package/src/resources/phase1.test.ts +0 -187
- package/src/resources/quotas.ts +0 -77
- package/src/resources/sandbox-processes.ts +0 -112
- package/src/resources/sandbox-shares.ts +0 -83
- package/src/resources/tenant-events.ts +0 -32
- package/src/resources/workspaces.ts +0 -285
package/package.json
CHANGED
package/src/resources/admin.ts
CHANGED
|
@@ -345,15 +345,4 @@ 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
|
-
}
|
|
359
348
|
}
|
|
@@ -113,22 +113,6 @@ 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
|
-
|
|
132
116
|
async delete(keyId: string): Promise<void> {
|
|
133
117
|
await this.http.delete<unknown>(`/api-keys/${keyId}`);
|
|
134
118
|
}
|
|
@@ -176,21 +176,26 @@ export class Computer {
|
|
|
176
176
|
* ```ts
|
|
177
177
|
* await computer.exec.bash("npm run dev &");
|
|
178
178
|
* const url = computer.previewUrl(3000);
|
|
179
|
-
* // => https://3000-<slug>.sandbox
|
|
179
|
+
* // => https://3000-<slug>.sandbox.<tenant-domain>
|
|
180
180
|
* ```
|
|
181
181
|
*
|
|
182
182
|
* Works for any TCP HTTP listener. Public (no auth required); anyone with
|
|
183
183
|
* the URL can see it. Served over the ingress proxy so it inherits the
|
|
184
|
-
*
|
|
184
|
+
* tenant's white-label preview domain — no per-sandbox certs to manage.
|
|
185
185
|
*/
|
|
186
186
|
previewUrl(port: number, path: string = "/"): string {
|
|
187
187
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
188
|
-
return `https://${port}-${this.slug}.sandbox.
|
|
188
|
+
return `https://${port}-${this.slug}.sandbox.${this.previewDomain}${p}`;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/** Root preview URL — serves whatever is on the default app port. */
|
|
192
192
|
get publicUrl(): string {
|
|
193
|
-
return `https://${this.slug}.sandbox.
|
|
193
|
+
return `https://${this.slug}.sandbox.${this.previewDomain}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Tenant's preview/base domain, white-label aware. */
|
|
197
|
+
get previewDomain(): string {
|
|
198
|
+
return this.data.preview_domain || "miosa.app";
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -41,7 +41,7 @@ export interface CustomDomainRegisterParams {
|
|
|
41
41
|
* // 1. Register the domain
|
|
42
42
|
* const domain = await computer.domains.register("app.example.com");
|
|
43
43
|
* console.log(domain.instructions);
|
|
44
|
-
* // => "Add a CNAME record: app.example.com → <slug>.sandbox
|
|
44
|
+
* // => "Add a CNAME record: app.example.com → <slug>.sandbox.<tenant-domain>"
|
|
45
45
|
*
|
|
46
46
|
* // 2. Add the CNAME in your DNS registrar, then...
|
|
47
47
|
*
|
|
@@ -393,10 +393,10 @@ describe("Sandbox handle", () => {
|
|
|
393
393
|
it("pause/resume/deploy use native sandbox lifecycle endpoints", async () => {
|
|
394
394
|
mockPost
|
|
395
395
|
.mockResolvedValueOnce({ data: sandboxData({ state: "paused" }) })
|
|
396
|
-
.mockResolvedValueOnce({ data: sandboxData({ state: "running" }) })
|
|
397
|
-
|
|
396
|
+
.mockResolvedValueOnce({ data: sandboxData({ state: "running" }) });
|
|
397
|
+
mockRequest.mockResolvedValueOnce({
|
|
398
398
|
data: { deployment_id: "dep_1", url: "https://app.miosa.app" },
|
|
399
|
-
|
|
399
|
+
});
|
|
400
400
|
|
|
401
401
|
const sandbox = new Sandbox(makeHttp(), sandboxData());
|
|
402
402
|
await sandbox.pause();
|
|
@@ -413,10 +413,13 @@ describe("Sandbox handle", () => {
|
|
|
413
413
|
"/sandboxes/sbx_123/resume",
|
|
414
414
|
{},
|
|
415
415
|
);
|
|
416
|
-
expect(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
expect(mockRequest).toHaveBeenCalledWith("/sandboxes/sbx_123/deploy", {
|
|
417
|
+
method: "POST",
|
|
418
|
+
body: {
|
|
419
|
+
name: "site",
|
|
420
|
+
output_path: "/workspace/dist",
|
|
421
|
+
custom_domain: "example.com",
|
|
422
|
+
},
|
|
420
423
|
});
|
|
421
424
|
expect(deployment.deployment_id).toBe("dep_1");
|
|
422
425
|
});
|
package/src/types.ts
CHANGED
|
@@ -78,7 +78,8 @@ export interface ComputerData {
|
|
|
78
78
|
id: ComputerId;
|
|
79
79
|
name: string;
|
|
80
80
|
/**
|
|
81
|
-
* URL-safe identifier used in preview URLs:
|
|
81
|
+
* URL-safe identifier used in preview URLs:
|
|
82
|
+
* `https://{port}-{slug}.sandbox.{preview_domain}`.
|
|
82
83
|
* Falls back to the computer id when no slug is assigned.
|
|
83
84
|
*/
|
|
84
85
|
slug: string;
|
|
@@ -90,8 +91,10 @@ export interface ComputerData {
|
|
|
90
91
|
metadata: Record<string, string>;
|
|
91
92
|
/** Controls who can access the HTTP preview URL. Defaults to `"public"`. */
|
|
92
93
|
visibility: ComputerVisibility;
|
|
93
|
-
/** Public ingress root, e.g. `https://<slug>.sandbox
|
|
94
|
+
/** Public ingress root, e.g. `https://<slug>.sandbox.<preview_domain>`. */
|
|
94
95
|
sandbox_url?: string;
|
|
96
|
+
/** Tenant's white-label preview/base domain, e.g. `cliniciq.com`. */
|
|
97
|
+
preview_domain?: string;
|
|
95
98
|
/** KasmVNC URL for desktop templates. */
|
|
96
99
|
desktop_url?: string;
|
|
97
100
|
created_at: string;
|
|
@@ -1,355 +0,0 @@
|
|
|
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
|
-
});
|