@miosa/sdk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Phase 6 governance resources — policy, members, workspaces, bulk ops,
3
+ * billing, impersonation, and scoped API keys.
4
+ */
5
+
6
+ import type { HttpClient } from "../http.js";
7
+
8
+ // ── Types ──────────────────────────────────────────────────────────────────────
9
+
10
+ export type PolicyDoc = Record<string, unknown>;
11
+
12
+ export interface BulkJobResponse {
13
+ queued: number;
14
+ job_id: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export interface BulkJobStatus {
19
+ id: string;
20
+ status: "queued" | "running" | "completed" | "failed";
21
+ processed?: number;
22
+ errors?: unknown[];
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export interface MemberRecord {
27
+ id: string;
28
+ email?: string;
29
+ role: string;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface Invoice {
34
+ id: string;
35
+ amount: number;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface PaymentMethod {
40
+ id: string;
41
+ brand: string;
42
+ last4: string;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ // ── Effective policy typed accessors ─────────────────────────────────────────
47
+
48
+ export interface EffectivePolicyField<T = unknown> {
49
+ value: T;
50
+ source: "user" | "workspace" | "tenant" | "platform";
51
+ }
52
+
53
+ export interface EffectiveLifecycle {
54
+ default_idle_timeout_sec?: EffectivePolicyField<number>;
55
+ default_timeout_sec?: EffectivePolicyField<number>;
56
+ default_always_on?: EffectivePolicyField<boolean>;
57
+ [key: string]: EffectivePolicyField | undefined;
58
+ }
59
+
60
+ export interface EffectiveQuotas {
61
+ max_sandboxes?: EffectivePolicyField<number>;
62
+ max_concurrent_sandboxes?: EffectivePolicyField<number>;
63
+ max_computers?: EffectivePolicyField<number>;
64
+ [key: string]: EffectivePolicyField | undefined;
65
+ }
66
+
67
+ export interface EffectivePolicyDoc {
68
+ lifecycle: Record<string, EffectivePolicyField>;
69
+ quotas: Record<string, EffectivePolicyField>;
70
+ sizing?: Record<string, EffectivePolicyField>;
71
+ features?: Record<string, EffectivePolicyField>;
72
+ egress?: Record<string, EffectivePolicyField>;
73
+ [key: string]: Record<string, EffectivePolicyField> | undefined;
74
+ }
75
+
76
+ // ── Policy sub-resources ──────────────────────────────────────────────────────
77
+
78
+ export class TenantPolicy {
79
+ constructor(private readonly http: HttpClient) {}
80
+
81
+ async get(): Promise<PolicyDoc> {
82
+ const r = await this.http.get<{ data?: PolicyDoc }>("/tenant/policy");
83
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
84
+ }
85
+
86
+ async set(policy: PolicyDoc): Promise<PolicyDoc> {
87
+ const r = await this.http.put<{ data?: PolicyDoc }>(
88
+ "/tenant/policy",
89
+ policy,
90
+ );
91
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
92
+ }
93
+
94
+ async delete(): Promise<void> {
95
+ await this.http.delete<unknown>("/tenant/policy");
96
+ }
97
+ }
98
+
99
+ export class WorkspacePolicyResource {
100
+ constructor(
101
+ private readonly http: HttpClient,
102
+ private readonly workspaceId: string,
103
+ ) {}
104
+
105
+ async get(): Promise<PolicyDoc> {
106
+ const r = await this.http.get<{ data?: PolicyDoc }>(
107
+ `/workspaces/${this.workspaceId}/policy`,
108
+ );
109
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
110
+ }
111
+
112
+ async set(policy: PolicyDoc): Promise<PolicyDoc> {
113
+ const r = await this.http.put<{ data?: PolicyDoc }>(
114
+ `/workspaces/${this.workspaceId}/policy`,
115
+ policy,
116
+ );
117
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
118
+ }
119
+
120
+ async delete(): Promise<void> {
121
+ await this.http.delete<unknown>(`/workspaces/${this.workspaceId}/policy`);
122
+ }
123
+ }
124
+
125
+ export class ExternalUserPolicyResource {
126
+ constructor(
127
+ private readonly http: HttpClient,
128
+ private readonly userId: string,
129
+ ) {}
130
+
131
+ async get(): Promise<PolicyDoc> {
132
+ const r = await this.http.get<{ data?: PolicyDoc }>(
133
+ `/external-users/${this.userId}/policy`,
134
+ );
135
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
136
+ }
137
+
138
+ async set(policy: PolicyDoc): Promise<PolicyDoc> {
139
+ const r = await this.http.put<{ data?: PolicyDoc }>(
140
+ `/external-users/${this.userId}/policy`,
141
+ policy,
142
+ );
143
+ return (r as { data?: PolicyDoc }).data ?? (r as PolicyDoc);
144
+ }
145
+
146
+ async delete(): Promise<void> {
147
+ await this.http.delete<unknown>(`/external-users/${this.userId}/policy`);
148
+ }
149
+
150
+ async effective(): Promise<EffectivePolicyDoc> {
151
+ const r = await this.http.get<unknown>(
152
+ `/external-users/${this.userId}/effective-policy`,
153
+ );
154
+ const data =
155
+ (r as { data?: EffectivePolicyDoc }).data ?? (r as EffectivePolicyDoc);
156
+ return data as EffectivePolicyDoc;
157
+ }
158
+ }
159
+
160
+ // ── ExternalUsers proxy ───────────────────────────────────────────────────────
161
+
162
+ export class ExternalUserProxy {
163
+ readonly policy: ExternalUserPolicyResource;
164
+
165
+ constructor(http: HttpClient, userId: string) {
166
+ this.policy = new ExternalUserPolicyResource(http, userId);
167
+ }
168
+ }
169
+
170
+ export class ExternalUsers {
171
+ constructor(private readonly http: HttpClient) {}
172
+
173
+ call(externalUserId: string): ExternalUserProxy {
174
+ return new ExternalUserProxy(this.http, externalUserId);
175
+ }
176
+ }
177
+
178
+ // ── Tenant members ─────────────────────────────────────────────────────────────
179
+
180
+ export class TenantMembersResource {
181
+ constructor(private readonly http: HttpClient) {}
182
+
183
+ async list(): Promise<MemberRecord[]> {
184
+ const r = await this.http.get<{ data?: MemberRecord[] }>("/tenant/members");
185
+ return (r as { data?: MemberRecord[] }).data ?? (r as MemberRecord[]) ?? [];
186
+ }
187
+
188
+ async invite(email: string, role: string): Promise<MemberRecord> {
189
+ const r = await this.http.post<{ data?: MemberRecord }>("/tenant/members", {
190
+ email,
191
+ role,
192
+ });
193
+ return (r as { data?: MemberRecord }).data ?? (r as MemberRecord);
194
+ }
195
+
196
+ async updateRole(memberId: string, role: string): Promise<MemberRecord> {
197
+ const r = await this.http.patch<{ data?: MemberRecord }>(
198
+ `/tenant/members/${memberId}/role`,
199
+ { role },
200
+ );
201
+ return (r as { data?: MemberRecord }).data ?? (r as MemberRecord);
202
+ }
203
+
204
+ async remove(memberId: string): Promise<void> {
205
+ await this.http.delete<unknown>(`/tenant/members/${memberId}`);
206
+ }
207
+
208
+ async transferOwnership(
209
+ newOwnerUserId: string,
210
+ ): Promise<Record<string, unknown>> {
211
+ const r = await this.http.post<{ data?: Record<string, unknown> }>(
212
+ "/tenant/transfer-ownership",
213
+ { new_owner_user_id: newOwnerUserId },
214
+ );
215
+ return (
216
+ (r as { data?: Record<string, unknown> }).data ??
217
+ (r as Record<string, unknown>)
218
+ );
219
+ }
220
+ }
221
+
222
+ // ── Tenant events ─────────────────────────────────────────────────────────────
223
+
224
+ export interface AdminStreamEvent {
225
+ _event_type?: string;
226
+ [key: string]: unknown;
227
+ }
228
+
229
+ export class TenantEventStreamResource {
230
+ constructor(private readonly http: HttpClient) {}
231
+
232
+ stream(options?: {
233
+ types?: string[];
234
+ scope?: string;
235
+ }): AsyncIterableIterator<AdminStreamEvent> {
236
+ const params = new URLSearchParams();
237
+ if (options?.types?.length) params.set("types", options.types.join(","));
238
+ if (options?.scope) params.set("scope", options.scope);
239
+ const qs = params.toString() ? `?${params.toString()}` : "";
240
+ return this.http.stream<AdminStreamEvent>(`/tenant/events/stream${qs}`);
241
+ }
242
+ }
243
+
244
+ // ── GovernanceTenant ──────────────────────────────────────────────────────────
245
+
246
+ export class GovernanceTenant {
247
+ readonly policy: TenantPolicy;
248
+ readonly members: TenantMembersResource;
249
+ readonly events: TenantEventStreamResource;
250
+
251
+ constructor(http: HttpClient) {
252
+ this.policy = new TenantPolicy(http);
253
+ this.members = new TenantMembersResource(http);
254
+ this.events = new TenantEventStreamResource(http);
255
+ }
256
+
257
+ async current(): Promise<Record<string, unknown>> {
258
+ return (await this.policy["http" as never]) as never;
259
+ }
260
+ }
261
+
262
+ // ── Workspace sub-resources ────────────────────────────────────────────────────
263
+
264
+ export class WorkspaceMembersResource {
265
+ constructor(
266
+ private readonly http: HttpClient,
267
+ private readonly workspaceId: string,
268
+ ) {}
269
+
270
+ async list(): Promise<MemberRecord[]> {
271
+ const r = await this.http.get<{ data?: MemberRecord[] }>(
272
+ `/workspaces/${this.workspaceId}/members`,
273
+ );
274
+ return (r as { data?: MemberRecord[] }).data ?? (r as MemberRecord[]) ?? [];
275
+ }
276
+
277
+ async invite(email: string, role: string): Promise<MemberRecord> {
278
+ const r = await this.http.post<{ data?: MemberRecord }>(
279
+ `/workspaces/${this.workspaceId}/members`,
280
+ { email, role },
281
+ );
282
+ return (r as { data?: MemberRecord }).data ?? (r as MemberRecord);
283
+ }
284
+
285
+ async updateRole(memberId: string, role: string): Promise<MemberRecord> {
286
+ const r = await this.http.patch<{ data?: MemberRecord }>(
287
+ `/workspaces/${this.workspaceId}/members/${memberId}/role`,
288
+ { role },
289
+ );
290
+ return (r as { data?: MemberRecord }).data ?? (r as MemberRecord);
291
+ }
292
+
293
+ async remove(memberId: string): Promise<void> {
294
+ await this.http.delete<unknown>(
295
+ `/workspaces/${this.workspaceId}/members/${memberId}`,
296
+ );
297
+ }
298
+ }
299
+
300
+ export class WorkspaceProxy {
301
+ readonly policy: WorkspacePolicyResource;
302
+ readonly members: WorkspaceMembersResource;
303
+
304
+ constructor(
305
+ private readonly http: HttpClient,
306
+ private readonly workspaceId: string,
307
+ ) {
308
+ this.policy = new WorkspacePolicyResource(http, workspaceId);
309
+ this.members = new WorkspaceMembersResource(http, workspaceId);
310
+ }
311
+
312
+ async transfer(
313
+ resourceIds: string[],
314
+ targetWorkspaceId: string,
315
+ ): Promise<Record<string, unknown>> {
316
+ const r = await this.http.post<{ data?: Record<string, unknown> }>(
317
+ `/workspaces/${this.workspaceId}/transfer`,
318
+ { resource_ids: resourceIds, target_workspace_id: targetWorkspaceId },
319
+ );
320
+ return (
321
+ (r as { data?: Record<string, unknown> }).data ??
322
+ (r as Record<string, unknown>)
323
+ );
324
+ }
325
+ }
326
+
327
+ // ── GovernanceWorkspaces ──────────────────────────────────────────────────────
328
+
329
+ export class GovernanceWorkspaces {
330
+ constructor(private readonly http: HttpClient) {}
331
+
332
+ /** client.workspaces("ws_id") → proxy with .policy, .members, .transfer() */
333
+ workspace(workspaceId: string): WorkspaceProxy {
334
+ return new WorkspaceProxy(this.http, workspaceId);
335
+ }
336
+
337
+ async list(): Promise<Record<string, unknown>[]> {
338
+ const r = await this.http.get<{ data?: Record<string, unknown>[] }>(
339
+ "/workspaces",
340
+ );
341
+ const items =
342
+ (r as { data?: Record<string, unknown>[] }).data ??
343
+ (r as Record<string, unknown>[]);
344
+ return Array.isArray(items) ? items : [];
345
+ }
346
+
347
+ async create(
348
+ name: string,
349
+ opts?: { description?: string; metadata?: Record<string, unknown> },
350
+ ): Promise<Record<string, unknown>> {
351
+ const r = await this.http.post<{ data?: Record<string, unknown> }>(
352
+ "/workspaces",
353
+ { name, ...opts },
354
+ );
355
+ return (
356
+ (r as { data?: Record<string, unknown> }).data ??
357
+ (r as Record<string, unknown>)
358
+ );
359
+ }
360
+
361
+ async get(workspaceId: string): Promise<Record<string, unknown>> {
362
+ const r = await this.http.get<{ data?: Record<string, unknown> }>(
363
+ `/workspaces/${workspaceId}`,
364
+ );
365
+ return (
366
+ (r as { data?: Record<string, unknown> }).data ??
367
+ (r as Record<string, unknown>)
368
+ );
369
+ }
370
+
371
+ async update(
372
+ workspaceId: string,
373
+ fields: Record<string, unknown>,
374
+ ): Promise<Record<string, unknown>> {
375
+ const r = await this.http.patch<{ data?: Record<string, unknown> }>(
376
+ `/workspaces/${workspaceId}`,
377
+ fields,
378
+ );
379
+ return (
380
+ (r as { data?: Record<string, unknown> }).data ??
381
+ (r as Record<string, unknown>)
382
+ );
383
+ }
384
+
385
+ async delete(workspaceId: string): Promise<void> {
386
+ await this.http.delete<unknown>(`/workspaces/${workspaceId}`);
387
+ }
388
+ }
389
+
390
+ // ── Bulk ops ──────────────────────────────────────────────────────────────────
391
+
392
+ function bulkBody(opts: {
393
+ ids?: string[];
394
+ filter?: Record<string, unknown>;
395
+ }): Record<string, unknown> {
396
+ if (opts.ids !== undefined) return { ids: opts.ids };
397
+ if (opts.filter !== undefined) return { filter: opts.filter };
398
+ throw new Error("Provide either ids or filter");
399
+ }
400
+
401
+ export class BulkSandboxesResource {
402
+ constructor(private readonly http: HttpClient) {}
403
+
404
+ async pause(opts: {
405
+ ids?: string[];
406
+ filter?: Record<string, unknown>;
407
+ }): Promise<BulkJobResponse> {
408
+ return this.http.post<BulkJobResponse>(
409
+ "/bulk/sandboxes/pause",
410
+ bulkBody(opts),
411
+ );
412
+ }
413
+
414
+ async resume(opts: {
415
+ ids?: string[];
416
+ filter?: Record<string, unknown>;
417
+ }): Promise<BulkJobResponse> {
418
+ return this.http.post<BulkJobResponse>(
419
+ "/bulk/sandboxes/resume",
420
+ bulkBody(opts),
421
+ );
422
+ }
423
+
424
+ async destroy(opts: {
425
+ ids?: string[];
426
+ filter?: Record<string, unknown>;
427
+ }): Promise<BulkJobResponse> {
428
+ return this.http.post<BulkJobResponse>(
429
+ "/bulk/sandboxes/destroy",
430
+ bulkBody(opts),
431
+ );
432
+ }
433
+ }
434
+
435
+ export class BulkPolicyResource {
436
+ constructor(private readonly http: HttpClient) {}
437
+
438
+ async apply(opts: {
439
+ tier: string;
440
+ idsOrFilter: string[] | Record<string, unknown>;
441
+ policy: PolicyDoc;
442
+ }): Promise<BulkJobResponse> {
443
+ const body: Record<string, unknown> = {
444
+ tier: opts.tier,
445
+ policy: opts.policy,
446
+ };
447
+ if (Array.isArray(opts.idsOrFilter)) {
448
+ body["ids"] = opts.idsOrFilter;
449
+ } else {
450
+ body["filter"] = opts.idsOrFilter;
451
+ }
452
+ return this.http.post<BulkJobResponse>("/bulk/policy/apply", body);
453
+ }
454
+ }
455
+
456
+ export class BulkJobsResource {
457
+ constructor(private readonly http: HttpClient) {}
458
+
459
+ async get(jobId: string): Promise<BulkJobStatus> {
460
+ const r = await this.http.get<{ data?: BulkJobStatus }>(
461
+ `/bulk/jobs/${jobId}`,
462
+ );
463
+ return (r as { data?: BulkJobStatus }).data ?? (r as BulkJobStatus);
464
+ }
465
+ }
466
+
467
+ export class BulkResource {
468
+ readonly sandboxes: BulkSandboxesResource;
469
+ readonly policy: BulkPolicyResource;
470
+ readonly jobs: BulkJobsResource;
471
+
472
+ constructor(http: HttpClient) {
473
+ this.sandboxes = new BulkSandboxesResource(http);
474
+ this.policy = new BulkPolicyResource(http);
475
+ this.jobs = new BulkJobsResource(http);
476
+ }
477
+ }
478
+
479
+ // ── Billing ───────────────────────────────────────────────────────────────────
480
+
481
+ export class BillingInvoicesResource {
482
+ constructor(private readonly http: HttpClient) {}
483
+
484
+ async list(opts?: { limit?: number; cursor?: string }): Promise<Invoice[]> {
485
+ const params = new URLSearchParams();
486
+ if (opts?.limit !== undefined) params.set("limit", String(opts.limit));
487
+ if (opts?.cursor) params.set("cursor", opts.cursor);
488
+ const qs = params.toString() ? `?${params.toString()}` : "";
489
+ const r = await this.http.get<{ data?: Invoice[] }>(
490
+ `/billing/invoices${qs}`,
491
+ );
492
+ return (r as { data?: Invoice[] }).data ?? (r as Invoice[]) ?? [];
493
+ }
494
+
495
+ async get(invoiceId: string): Promise<Invoice> {
496
+ const r = await this.http.get<{ data?: Invoice }>(
497
+ `/billing/invoices/${invoiceId}`,
498
+ );
499
+ return (r as { data?: Invoice }).data ?? (r as Invoice);
500
+ }
501
+ }
502
+
503
+ export class BillingResource {
504
+ readonly invoices: BillingInvoicesResource;
505
+
506
+ constructor(private readonly http: HttpClient) {
507
+ this.invoices = new BillingInvoicesResource(http);
508
+ }
509
+
510
+ async paymentMethods(): Promise<PaymentMethod[]> {
511
+ const r = await this.http.get<{ data?: PaymentMethod[] }>(
512
+ "/billing/payment-methods",
513
+ );
514
+ return (
515
+ (r as { data?: PaymentMethod[] }).data ?? (r as PaymentMethod[]) ?? []
516
+ );
517
+ }
518
+
519
+ async upcoming(): Promise<Record<string, unknown>> {
520
+ const r = await this.http.get<{ data?: Record<string, unknown> }>(
521
+ "/billing/upcoming",
522
+ );
523
+ return (
524
+ (r as { data?: Record<string, unknown> }).data ??
525
+ (r as Record<string, unknown>)
526
+ );
527
+ }
528
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Org Invites — email invite flow for org (tenant) membership.
3
+ *
4
+ * Public endpoints (no auth):
5
+ * GET /invites/:token
6
+ *
7
+ * Authenticated endpoints (admin/owner role required for create/list/revoke):
8
+ * POST /tenants/:id/invites
9
+ * GET /tenants/:id/invites
10
+ * DELETE /tenants/:id/invites/:invite_id
11
+ * POST /invites/:token/accept
12
+ */
13
+
14
+ import type { HttpClient } from "../http.js";
15
+
16
+ // ── Resource shapes ──────────────────────────────────────────────────────────
17
+
18
+ export type OrgRole = "owner" | "admin" | "member";
19
+
20
+ export interface OrgInvite {
21
+ id: string;
22
+ tenant_id: string;
23
+ email: string;
24
+ role: OrgRole;
25
+ invited_by: string | null;
26
+ expires_at: string;
27
+ accepted_at: string | null;
28
+ created_at: string;
29
+ }
30
+
31
+ export interface OrgInviteCreated {
32
+ invite_id: string;
33
+ email: string;
34
+ role: OrgRole;
35
+ expires_at: string;
36
+ /**
37
+ * Full URL for the invite landing page. On white-label tenants this uses
38
+ * the tenant's custom domain.
39
+ */
40
+ invite_url: string;
41
+ }
42
+
43
+ export interface OrgInvitePreview {
44
+ email: string;
45
+ tenant_name: string;
46
+ role: OrgRole;
47
+ expires_at: string;
48
+ expired: boolean;
49
+ accepted: boolean;
50
+ }
51
+
52
+ export interface TenantSummary {
53
+ id: string;
54
+ name: string;
55
+ slug: string;
56
+ owner_user_id: string | null;
57
+ plan_id: string | null;
58
+ plan_name: string | null;
59
+ settings: Record<string, unknown>;
60
+ inserted_at: string;
61
+ updated_at: string;
62
+ }
63
+
64
+ // ── Request payloads ─────────────────────────────────────────────────────────
65
+
66
+ export interface CreateOrgInviteParams {
67
+ email: string;
68
+ role?: OrgRole;
69
+ }
70
+
71
+ // ── Response shapes ───────────────────────────────────────────────────────────
72
+
73
+ export interface OrgInviteCreatedResponse {
74
+ data: OrgInviteCreated;
75
+ }
76
+
77
+ export interface OrgInviteListResponse {
78
+ data: OrgInvite[];
79
+ total: number;
80
+ }
81
+
82
+ export interface OrgInviteRevokeResponse {
83
+ invite_id: string;
84
+ revoked: boolean;
85
+ }
86
+
87
+ export interface OrgInvitePreviewResponse {
88
+ data: OrgInvitePreview;
89
+ }
90
+
91
+ export interface AcceptOrgInviteResponse {
92
+ accepted: boolean;
93
+ tenant_id: string;
94
+ tenant: TenantSummary | null;
95
+ }
96
+
97
+ // ── Main resource ─────────────────────────────────────────────────────────────
98
+
99
+ export class OrgInvites {
100
+ constructor(private readonly http: HttpClient) {}
101
+
102
+ /**
103
+ * Create an org invite and dispatch the invite email.
104
+ *
105
+ * The invite URL in the response is host-aware: on white-label tenants it
106
+ * uses the custom domain so the recipient lands on the branded experience.
107
+ * Requires `admin` or `owner` role in the tenant.
108
+ *
109
+ * `POST /tenants/:id/invites`
110
+ */
111
+ async create(
112
+ tenantId: string,
113
+ params: CreateOrgInviteParams,
114
+ ): Promise<OrgInviteCreated> {
115
+ const res = await this.http.post<OrgInviteCreatedResponse>(
116
+ `/tenants/${tenantId}/invites`,
117
+ params,
118
+ );
119
+ return res.data;
120
+ }
121
+
122
+ /**
123
+ * List all pending (non-expired, non-accepted, non-revoked) org invites.
124
+ *
125
+ * Requires `admin` or `owner` role.
126
+ *
127
+ * `GET /tenants/:id/invites`
128
+ */
129
+ async list(tenantId: string): Promise<OrgInvite[]> {
130
+ const res = await this.http.get<OrgInviteListResponse>(
131
+ `/tenants/${tenantId}/invites`,
132
+ );
133
+ return res.data ?? [];
134
+ }
135
+
136
+ /**
137
+ * Revoke a pending org invite.
138
+ *
139
+ * Returns `409` when the invite was already legitimately accepted.
140
+ * Requires `admin` or `owner` role.
141
+ *
142
+ * `DELETE /tenants/:id/invites/:invite_id`
143
+ */
144
+ async revoke(
145
+ tenantId: string,
146
+ inviteId: string,
147
+ ): Promise<OrgInviteRevokeResponse> {
148
+ return this.http.delete<OrgInviteRevokeResponse>(
149
+ `/tenants/${tenantId}/invites/${inviteId}`,
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Preview an org invite by token (no auth required).
155
+ *
156
+ * Returns `null` when the token is unknown or has been revoked.
157
+ *
158
+ * `GET /invites/:token`
159
+ */
160
+ async preview(token: string): Promise<OrgInvitePreview | null> {
161
+ try {
162
+ const res = await this.http.get<OrgInvitePreviewResponse>(
163
+ `/invites/${token}`,
164
+ );
165
+ return res.data ?? null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Accept an org invite on behalf of the authenticated user.
173
+ *
174
+ * The caller's JWT email must match the invite email (case-insensitive).
175
+ * On success inserts a `tenant_members` row.
176
+ *
177
+ * Error responses:
178
+ * - `400` — invalid or expired token.
179
+ * - `422 EMAIL_MISMATCH` — JWT email does not match the invite email.
180
+ *
181
+ * `POST /invites/:token/accept`
182
+ */
183
+ async accept(token: string): Promise<AcceptOrgInviteResponse> {
184
+ return this.http.post<AcceptOrgInviteResponse>(
185
+ `/invites/${token}/accept`,
186
+ {},
187
+ );
188
+ }
189
+ }