@miosa/sdk 1.2.3 → 1.2.5

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.
@@ -1,102 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import type { HttpClient } from "../http.js";
3
- import { DockerDeploy } from "./docker-deploy.js";
4
-
5
- const mockGet = vi.fn();
6
- const mockPost = vi.fn();
7
-
8
- function makeHttp(): HttpClient {
9
- const http = {} as HttpClient;
10
- http.get = mockGet;
11
- http.post = mockPost;
12
- return http;
13
- }
14
-
15
- beforeEach(() => {
16
- vi.clearAllMocks();
17
- });
18
-
19
- describe("DockerDeploy", () => {
20
- it("lists Docker Deploy hosts by workspace", async () => {
21
- mockGet.mockResolvedValue({
22
- data: [
23
- {
24
- id: "ddh_123",
25
- tenant_id: "ten_123",
26
- workspace_id: "ws_123",
27
- status: "bootstrapping",
28
- appliance_status: "starting",
29
- size: "medium",
30
- region: "us",
31
- },
32
- ],
33
- });
34
-
35
- const hosts = await new DockerDeploy(makeHttp()).listHosts({
36
- workspaceId: "ws_123",
37
- });
38
-
39
- expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts", {
40
- workspace_id: "ws_123",
41
- });
42
- expect(hosts[0]?.id).toBe("ddh_123");
43
- });
44
-
45
- it("ensures a workspace Docker Deploy host", async () => {
46
- mockPost.mockResolvedValue({
47
- host: {
48
- id: "ddh_123",
49
- tenant_id: "ten_123",
50
- workspace_id: "ws_123",
51
- status: "pending",
52
- appliance_status: "not_installed",
53
- size: "medium",
54
- region: "us",
55
- },
56
- queued: true,
57
- });
58
-
59
- const result = await new DockerDeploy(makeHttp()).ensureHost({
60
- workspaceId: "ws_123",
61
- });
62
-
63
- expect(mockPost).toHaveBeenCalledWith("/docker-deploy/hosts/ensure", {
64
- workspace_id: "ws_123",
65
- });
66
- expect(result.queued).toBe(true);
67
- expect(result.host.status).toBe("pending");
68
- });
69
-
70
- it("lists Docker Deploy templates", async () => {
71
- mockGet.mockResolvedValue({
72
- data: [
73
- {
74
- id: "nextjs-app",
75
- name: "Next.js app",
76
- description: "App router starter",
77
- },
78
- ],
79
- });
80
-
81
- const templates = await new DockerDeploy(makeHttp()).listTemplates();
82
-
83
- expect(mockGet).toHaveBeenCalledWith("/docker-deploy/templates");
84
- expect(templates[0]?.id).toBe("nextjs-app");
85
- });
86
-
87
- it("gets a Docker Deploy template", async () => {
88
- mockGet.mockResolvedValue({
89
- template: {
90
- id: "compose-full-stack",
91
- name: "Compose full stack",
92
- },
93
- });
94
-
95
- const template = await new DockerDeploy(makeHttp()).getTemplate("compose-full-stack");
96
-
97
- expect(mockGet).toHaveBeenCalledWith(
98
- "/docker-deploy/templates/compose-full-stack",
99
- );
100
- expect(template.id).toBe("compose-full-stack");
101
- });
102
- });
@@ -1,183 +0,0 @@
1
- import type { HttpClient } from "../http.js";
2
-
3
- export type DockerDeployHostId = string & {
4
- readonly __brand: "DockerDeployHostId";
5
- };
6
-
7
- export type DockerDeployHostStatus =
8
- | "pending"
9
- | "provisioning"
10
- | "bootstrapping"
11
- | "active"
12
- | "degraded"
13
- | "suspended"
14
- | "retired"
15
- | "error";
16
-
17
- export type DockerDeployApplianceStatus =
18
- | "not_installed"
19
- | "installing"
20
- | "starting"
21
- | "healthy"
22
- | "unhealthy"
23
- | "unknown";
24
-
25
- export interface DockerDeployHostData {
26
- id: DockerDeployHostId;
27
- tenant_id: string;
28
- workspace_id: string;
29
- external_workspace_id?: string | null;
30
- computer_id?: string | null;
31
- fleet_node_id?: string | null;
32
- status: DockerDeployHostStatus;
33
- size: string;
34
- region: string;
35
- portal_domain?: string | null;
36
- runtime_base_url?: string | null;
37
- agent_base_url?: string | null;
38
- appliance_image?: string | null;
39
- appliance_version?: string | null;
40
- appliance_status: DockerDeployApplianceStatus;
41
- agent_last_seen_at?: string | null;
42
- metadata?: Record<string, unknown>;
43
- created_at?: string;
44
- updated_at?: string;
45
- }
46
-
47
- export interface DockerDeployHostListParams {
48
- workspace_id?: string;
49
- workspaceId?: string;
50
- }
51
-
52
- export interface DockerDeployHostEnsureParams {
53
- workspace_id?: string;
54
- workspaceId?: string;
55
- external_workspace_id?: string;
56
- externalWorkspaceId?: string;
57
- }
58
-
59
- export interface DockerDeployHostListResponse {
60
- data?: DockerDeployHostData[];
61
- hosts?: DockerDeployHostData[];
62
- }
63
-
64
- export interface DockerDeployHostResponse {
65
- data?: DockerDeployHostData;
66
- host?: DockerDeployHostData;
67
- queued?: boolean;
68
- }
69
-
70
- export interface DockerDeployTemplate {
71
- id: string;
72
- name: string;
73
- description?: string;
74
- category?: string;
75
- runtime?: string;
76
- tags?: string[];
77
- metadata?: Record<string, unknown>;
78
- [key: string]: unknown;
79
- }
80
-
81
- export interface DockerDeployTemplateListResponse {
82
- data?: DockerDeployTemplate[];
83
- templates?: DockerDeployTemplate[];
84
- }
85
-
86
- export interface DockerDeployTemplateResponse {
87
- data?: DockerDeployTemplate;
88
- template?: DockerDeployTemplate;
89
- }
90
-
91
- function workspaceId(params?: DockerDeployHostListParams): string | undefined {
92
- return params?.workspace_id ?? params?.workspaceId;
93
- }
94
-
95
- function ensureBody(params: DockerDeployHostEnsureParams): Record<string, string> {
96
- const body: Record<string, string> = {};
97
- const id = params.workspace_id ?? params.workspaceId;
98
- const externalId = params.external_workspace_id ?? params.externalWorkspaceId;
99
- if (id) body.workspace_id = id;
100
- if (externalId) body.external_workspace_id = externalId;
101
- return body;
102
- }
103
-
104
- function unwrapHost(response: DockerDeployHostResponse): DockerDeployHostData {
105
- const host = response.data ?? response.host;
106
- if (!host) {
107
- throw new Error("Docker Deploy host response was empty.");
108
- }
109
- return host;
110
- }
111
-
112
- function unwrapTemplates(response: DockerDeployTemplateListResponse): DockerDeployTemplate[] {
113
- return response.data ?? response.templates ?? [];
114
- }
115
-
116
- function unwrapTemplate(response: DockerDeployTemplateResponse): DockerDeployTemplate {
117
- const template = response.data ?? response.template;
118
- if (!template) {
119
- throw new Error("Docker Deploy template response was empty.");
120
- }
121
- return template;
122
- }
123
-
124
- export class DockerDeploy {
125
- constructor(private readonly http: HttpClient) {}
126
-
127
- /**
128
- * List Docker Deploy appliance hosts scoped to the current tenant.
129
- *
130
- * Pass a workspace ID to inspect the dedicated always-on appliance machine
131
- * for one white-label workspace.
132
- */
133
- async listHosts(
134
- params: DockerDeployHostListParams = {},
135
- ): Promise<DockerDeployHostData[]> {
136
- const res = await this.http.get<DockerDeployHostListResponse>(
137
- "/docker-deploy/hosts",
138
- { workspace_id: workspaceId(params) },
139
- );
140
- return res.data ?? res.hosts ?? [];
141
- }
142
-
143
- /**
144
- * Ensure a workspace has its dedicated Docker Deploy appliance host.
145
- *
146
- * The host may still be `pending`, `provisioning`, or `bootstrapping` after
147
- * this call. Treat `status === "active"` and `appliance_status === "healthy"`
148
- * as the ready condition before sending app/container traffic to it.
149
- */
150
- async ensureHost(
151
- params: DockerDeployHostEnsureParams = {},
152
- ): Promise<{ host: DockerDeployHostData; queued: boolean }> {
153
- const res = await this.http.post<DockerDeployHostResponse>(
154
- "/docker-deploy/hosts/ensure",
155
- ensureBody(params),
156
- );
157
- return { host: unwrapHost(res), queued: res.queued ?? false };
158
- }
159
-
160
- /** Fetch one Docker Deploy host by ID. */
161
- async getHost(hostId: string): Promise<DockerDeployHostData> {
162
- const res = await this.http.get<DockerDeployHostResponse>(
163
- `/docker-deploy/hosts/${hostId}`,
164
- );
165
- return unwrapHost(res);
166
- }
167
-
168
- /** List Docker Deploy starter templates. */
169
- async listTemplates(): Promise<DockerDeployTemplate[]> {
170
- const res = await this.http.get<DockerDeployTemplateListResponse>(
171
- "/docker-deploy/templates",
172
- );
173
- return unwrapTemplates(res);
174
- }
175
-
176
- /** Fetch one Docker Deploy starter template by ID. */
177
- async getTemplate(templateId: string): Promise<DockerDeployTemplate> {
178
- const res = await this.http.get<DockerDeployTemplateResponse>(
179
- `/docker-deploy/templates/${encodeURIComponent(templateId)}`,
180
- );
181
- return unwrapTemplate(res);
182
- }
183
- }
@@ -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
- });