@miosa/sdk 1.2.4 → 1.2.6
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 +75 -3
- package/dist/index.d.ts +323 -175
- package/dist/index.js +441 -386
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/src/client.ts +0 -293
- package/src/errors.ts +0 -136
- package/src/http.test.ts +0 -374
- package/src/http.ts +0 -390
- package/src/index.ts +0 -564
- package/src/resources/admin.ts +0 -359
- package/src/resources/analytics.ts +0 -60
- package/src/resources/api-keys.ts +0 -135
- package/src/resources/audit-log.ts +0 -64
- package/src/resources/benchmarks.ts +0 -104
- package/src/resources/builder-sessions.ts +0 -75
- package/src/resources/channels.ts +0 -143
- package/src/resources/checkpoints.ts +0 -225
- package/src/resources/command-center.ts +0 -73
- package/src/resources/community.ts +0 -103
- package/src/resources/completions.ts +0 -104
- package/src/resources/computer-auto-stop.ts +0 -43
- package/src/resources/computer-env.ts +0 -76
- package/src/resources/computer-logs.ts +0 -50
- package/src/resources/computer-osa.ts +0 -76
- package/src/resources/computer-ports.ts +0 -91
- package/src/resources/computer-terminal.ts +0 -61
- package/src/resources/computer-volumes.ts +0 -64
- package/src/resources/computer.ts +0 -551
- package/src/resources/computers.ts +0 -75
- package/src/resources/credits.ts +0 -43
- package/src/resources/cron-jobs.ts +0 -191
- package/src/resources/custom_domains.ts +0 -123
- package/src/resources/dashboard.ts +0 -49
- package/src/resources/databases.ts +0 -218
- package/src/resources/deployments.test.ts +0 -194
- package/src/resources/deployments.ts +0 -1060
- package/src/resources/desktop.ts +0 -134
- package/src/resources/docker-deploy.test.ts +0 -102
- package/src/resources/docker-deploy.ts +0 -183
- package/src/resources/egress.test.ts +0 -318
- package/src/resources/egressAudit.ts +0 -245
- package/src/resources/egressNetwork.ts +0 -450
- package/src/resources/egressSecrets.ts +0 -577
- package/src/resources/email.ts +0 -212
- package/src/resources/embeddings.ts +0 -36
- package/src/resources/events.ts +0 -296
- package/src/resources/exec.ts +0 -319
- package/src/resources/external-keys.ts +0 -79
- package/src/resources/files.test.ts +0 -339
- package/src/resources/files.ts +0 -220
- package/src/resources/flat-custom-domains.ts +0 -127
- package/src/resources/functions.ts +0 -178
- package/src/resources/governance.test.ts +0 -355
- package/src/resources/governance.ts +0 -528
- package/src/resources/health-checks.ts +0 -165
- package/src/resources/integrations.ts +0 -183
- package/src/resources/mcp.ts +0 -70
- package/src/resources/models.ts +0 -45
- package/src/resources/network_policy.ts +0 -73
- package/src/resources/open-computers/agents.ts +0 -91
- package/src/resources/open-computers/apps.ts +0 -102
- package/src/resources/open-computers/clusters.ts +0 -88
- package/src/resources/open-computers/desktop.ts +0 -34
- package/src/resources/open-computers/files.ts +0 -97
- package/src/resources/open-computers/hosts.ts +0 -85
- package/src/resources/open-computers/index.ts +0 -98
- package/src/resources/open-computers/jobs.ts +0 -75
- package/src/resources/open-computers/open_computers.test.ts +0 -288
- package/src/resources/open-computers/secrets.ts +0 -115
- package/src/resources/open-computers/terminal.ts +0 -33
- package/src/resources/open-computers/tunnels.ts +0 -87
- package/src/resources/open-computers/types.ts +0 -343
- package/src/resources/open-computers/workspaces.ts +0 -135
- package/src/resources/org-invites.ts +0 -189
- package/src/resources/phase1.test.ts +0 -187
- package/src/resources/project-auth.ts +0 -142
- package/src/resources/project-integrations.ts +0 -133
- package/src/resources/provider-defaults.ts +0 -89
- package/src/resources/quotas.ts +0 -77
- package/src/resources/regions.ts +0 -94
- package/src/resources/sandbox-processes.ts +0 -112
- package/src/resources/sandbox-shares.ts +0 -83
- package/src/resources/sandbox-templates.ts +0 -195
- package/src/resources/sandboxes.live.test.ts +0 -92
- package/src/resources/sandboxes.test.ts +0 -653
- package/src/resources/sandboxes.ts +0 -1433
- package/src/resources/settings.ts +0 -143
- package/src/resources/snapshots-standalone.ts +0 -51
- package/src/resources/storage.ts +0 -221
- package/src/resources/tenant-events.ts +0 -32
- package/src/resources/tenant.ts +0 -148
- package/src/resources/usage.ts +0 -85
- package/src/resources/volumes.ts +0 -117
- package/src/resources/webhooks.ts +0 -224
- package/src/resources/workspace-invites.ts +0 -188
- package/src/resources/workspace-members.test.ts +0 -121
- package/src/resources/workspace-members.ts +0 -143
- package/src/resources/workspaces.ts +0 -285
- package/src/types.ts +0 -463
package/src/resources/volumes.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Volumes resource — persistent block storage.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
|
|
7
|
-
import type { HttpClient } from "../http.js";
|
|
8
|
-
|
|
9
|
-
// ── Branded IDs ──────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export type VolumeId = string & { readonly __brand: "VolumeId" };
|
|
12
|
-
|
|
13
|
-
// ── Resource shapes ──────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export interface VolumeData {
|
|
16
|
-
id: VolumeId;
|
|
17
|
-
tenant_id: string;
|
|
18
|
-
name: string;
|
|
19
|
-
size_gb: number;
|
|
20
|
-
state?: string;
|
|
21
|
-
region?: string | null;
|
|
22
|
-
attached_to?: string | null;
|
|
23
|
-
created_at?: string;
|
|
24
|
-
updated_at?: string;
|
|
25
|
-
[key: string]: unknown;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Request payloads ─────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
export interface VolumeListParams {
|
|
31
|
-
limit?: number;
|
|
32
|
-
cursor?: string;
|
|
33
|
-
state?: string;
|
|
34
|
-
[key: string]: string | number | boolean | undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface VolumeCreateParams {
|
|
38
|
-
name: string;
|
|
39
|
-
size_gb: number;
|
|
40
|
-
sizeGb?: number;
|
|
41
|
-
region?: string;
|
|
42
|
-
idempotencyKey?: string;
|
|
43
|
-
[key: string]: unknown;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
function unwrap<T>(payload: unknown): T {
|
|
49
|
-
if (payload && typeof payload === "object" && "data" in (payload as object)) {
|
|
50
|
-
return (payload as { data: T }).data;
|
|
51
|
-
}
|
|
52
|
-
return payload as T;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function listItems<T>(
|
|
56
|
-
payload: unknown,
|
|
57
|
-
candidateKeys: string[] = ["data", "volumes", "items"],
|
|
58
|
-
): T[] {
|
|
59
|
-
if (Array.isArray(payload)) return payload;
|
|
60
|
-
if (!payload || typeof payload !== "object") return [];
|
|
61
|
-
const p = payload as Record<string, unknown>;
|
|
62
|
-
if (Array.isArray(p.data)) return p.data as T[];
|
|
63
|
-
for (const key of candidateKeys) {
|
|
64
|
-
if (Array.isArray(p[key])) return p[key] as T[];
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function stripUndefined(
|
|
70
|
-
input: Record<string, unknown>,
|
|
71
|
-
): Record<string, unknown> {
|
|
72
|
-
return Object.fromEntries(
|
|
73
|
-
Object.entries(input).filter(([, v]) => v !== undefined),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function idempotencyKey(key?: string): string {
|
|
78
|
-
return key ?? randomUUID();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Main resource ─────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
export class Volumes {
|
|
84
|
-
constructor(private readonly http: HttpClient) {}
|
|
85
|
-
|
|
86
|
-
async list(params: VolumeListParams = {}): Promise<VolumeData[]> {
|
|
87
|
-
const query = stripUndefined({ ...params }) as Record<
|
|
88
|
-
string,
|
|
89
|
-
string | number | boolean | undefined
|
|
90
|
-
>;
|
|
91
|
-
const data = await this.http.get<unknown>("/volumes", query);
|
|
92
|
-
return listItems<VolumeData>(data);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async get(volumeId: string): Promise<VolumeData> {
|
|
96
|
-
const data = await this.http.get<unknown>(`/volumes/${volumeId}`);
|
|
97
|
-
return unwrap<VolumeData>(data);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async create(params: VolumeCreateParams): Promise<VolumeData> {
|
|
101
|
-
const { idempotencyKey: ikey, sizeGb, ...rest } = params;
|
|
102
|
-
const body = stripUndefined({
|
|
103
|
-
...rest,
|
|
104
|
-
size_gb: sizeGb ?? rest.size_gb,
|
|
105
|
-
});
|
|
106
|
-
const data = await this.http.request<unknown>("/volumes", {
|
|
107
|
-
method: "POST",
|
|
108
|
-
body,
|
|
109
|
-
headers: { "Idempotency-Key": idempotencyKey(ikey) },
|
|
110
|
-
});
|
|
111
|
-
return unwrap<VolumeData>(data);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async delete(volumeId: string): Promise<void> {
|
|
115
|
-
await this.http.delete<unknown>(`/volumes/${volumeId}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Webhooks resource — tenant outgoing event delivery.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
|
-
|
|
7
|
-
import type { HttpClient } from "../http.js";
|
|
8
|
-
|
|
9
|
-
// ── Branded IDs ──────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export type WebhookId = string & { readonly __brand: "WebhookId" };
|
|
12
|
-
export type WebhookDeliveryId = string & {
|
|
13
|
-
readonly __brand: "WebhookDeliveryId";
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// ── Resource shapes ──────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
export interface WebhookData {
|
|
19
|
-
id: WebhookId;
|
|
20
|
-
tenant_id: string;
|
|
21
|
-
url: string;
|
|
22
|
-
events: string[];
|
|
23
|
-
state?: string;
|
|
24
|
-
secret?: string | null;
|
|
25
|
-
enabled?: boolean;
|
|
26
|
-
created_at?: string;
|
|
27
|
-
updated_at?: string;
|
|
28
|
-
[key: string]: unknown;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface WebhookDeliveryData {
|
|
32
|
-
id: WebhookDeliveryId;
|
|
33
|
-
webhook_id: WebhookId;
|
|
34
|
-
event?: string;
|
|
35
|
-
state?: string;
|
|
36
|
-
response_status?: number | null;
|
|
37
|
-
attempt_count?: number;
|
|
38
|
-
next_retry_at?: string | null;
|
|
39
|
-
delivered_at?: string | null;
|
|
40
|
-
created_at?: string;
|
|
41
|
-
[key: string]: unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Request payloads ─────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
export interface WebhookListParams {
|
|
47
|
-
limit?: number;
|
|
48
|
-
cursor?: string;
|
|
49
|
-
[key: string]: string | number | boolean | undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface WebhookCreateParams {
|
|
53
|
-
url: string;
|
|
54
|
-
events: string[];
|
|
55
|
-
secret?: string;
|
|
56
|
-
enabled?: boolean;
|
|
57
|
-
idempotencyKey?: string;
|
|
58
|
-
[key: string]: unknown;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface WebhookUpdateParams {
|
|
62
|
-
url?: string;
|
|
63
|
-
events?: string[];
|
|
64
|
-
secret?: string;
|
|
65
|
-
enabled?: boolean;
|
|
66
|
-
[key: string]: unknown;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
function unwrap<T>(payload: unknown): T {
|
|
72
|
-
if (payload && typeof payload === "object" && "data" in (payload as object)) {
|
|
73
|
-
return (payload as { data: T }).data;
|
|
74
|
-
}
|
|
75
|
-
return payload as T;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function listItems<T>(
|
|
79
|
-
payload: unknown,
|
|
80
|
-
candidateKeys: string[] = ["data", "webhooks", "deliveries", "items"],
|
|
81
|
-
): T[] {
|
|
82
|
-
if (Array.isArray(payload)) return payload;
|
|
83
|
-
if (!payload || typeof payload !== "object") return [];
|
|
84
|
-
const p = payload as Record<string, unknown>;
|
|
85
|
-
if (Array.isArray(p.data)) return p.data as T[];
|
|
86
|
-
for (const key of candidateKeys) {
|
|
87
|
-
if (Array.isArray(p[key])) return p[key] as T[];
|
|
88
|
-
}
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function stripUndefined(
|
|
93
|
-
input: Record<string, unknown>,
|
|
94
|
-
): Record<string, unknown> {
|
|
95
|
-
return Object.fromEntries(
|
|
96
|
-
Object.entries(input).filter(([, v]) => v !== undefined),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function idempotencyKey(key?: string): string {
|
|
101
|
-
return key ?? randomUUID();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function bodyBuffer(body: Buffer | Uint8Array | string): Buffer {
|
|
105
|
-
if (Buffer.isBuffer(body)) return body;
|
|
106
|
-
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
107
|
-
return Buffer.from(body);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Verify the `Miosa-Signature` webhook header.
|
|
112
|
-
*
|
|
113
|
-
* Header format: `t=<unix_seconds>,v1=<hex_hmac>`.
|
|
114
|
-
* Signed payload: `<timestamp>.<raw_body>`.
|
|
115
|
-
*/
|
|
116
|
-
export function verifySignature(
|
|
117
|
-
body: Buffer | Uint8Array | string,
|
|
118
|
-
header: string,
|
|
119
|
-
secret: string,
|
|
120
|
-
toleranceSec = 300,
|
|
121
|
-
): boolean {
|
|
122
|
-
const parts = Object.fromEntries(
|
|
123
|
-
header
|
|
124
|
-
.split(",")
|
|
125
|
-
.map((chunk) => chunk.split("=", 2))
|
|
126
|
-
.filter(([key, value]) => key && value),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const timestamp = parts.t;
|
|
130
|
-
const received = parts.v1;
|
|
131
|
-
if (!timestamp || !received || !secret) return false;
|
|
132
|
-
|
|
133
|
-
const unixSeconds = Number(timestamp);
|
|
134
|
-
if (!Number.isFinite(unixSeconds)) return false;
|
|
135
|
-
|
|
136
|
-
const ageSec = Math.abs(Date.now() / 1000 - unixSeconds);
|
|
137
|
-
if (ageSec > toleranceSec) {
|
|
138
|
-
throw new Error("webhook timestamp too old");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const rawBody = bodyBuffer(body);
|
|
142
|
-
const signed = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
|
|
143
|
-
const expected = createHmac("sha256", secret).update(signed).digest("hex");
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
return timingSafeEqual(
|
|
147
|
-
Buffer.from(expected, "utf8"),
|
|
148
|
-
Buffer.from(received, "utf8"),
|
|
149
|
-
);
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Main resource ─────────────────────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
export class Webhooks {
|
|
158
|
-
constructor(private readonly http: HttpClient) {}
|
|
159
|
-
|
|
160
|
-
static verifySignature = verifySignature;
|
|
161
|
-
|
|
162
|
-
async list(params: WebhookListParams = {}): Promise<WebhookData[]> {
|
|
163
|
-
const query = stripUndefined({ ...params }) as Record<
|
|
164
|
-
string,
|
|
165
|
-
string | number | boolean | undefined
|
|
166
|
-
>;
|
|
167
|
-
const data = await this.http.get<unknown>("/webhooks", query);
|
|
168
|
-
return listItems<WebhookData>(data);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async get(webhookId: string): Promise<WebhookData> {
|
|
172
|
-
const data = await this.http.get<unknown>(`/webhooks/${webhookId}`);
|
|
173
|
-
return unwrap<WebhookData>(data);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async create(params: WebhookCreateParams): Promise<WebhookData> {
|
|
177
|
-
const { idempotencyKey: ikey, ...rest } = params;
|
|
178
|
-
const body = stripUndefined(rest as Record<string, unknown>);
|
|
179
|
-
const data = await this.http.request<unknown>("/webhooks", {
|
|
180
|
-
method: "POST",
|
|
181
|
-
body,
|
|
182
|
-
headers: { "Idempotency-Key": idempotencyKey(ikey) },
|
|
183
|
-
});
|
|
184
|
-
return unwrap<WebhookData>(data);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async update(
|
|
188
|
-
webhookId: string,
|
|
189
|
-
params: WebhookUpdateParams,
|
|
190
|
-
): Promise<WebhookData> {
|
|
191
|
-
const body = stripUndefined(params as Record<string, unknown>);
|
|
192
|
-
const data = await this.http.patch<unknown>(`/webhooks/${webhookId}`, body);
|
|
193
|
-
return unwrap<WebhookData>(data);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async delete(webhookId: string): Promise<void> {
|
|
197
|
-
await this.http.delete<unknown>(`/webhooks/${webhookId}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async test(
|
|
201
|
-
webhookId: string,
|
|
202
|
-
opts: { idempotencyKey?: string } = {},
|
|
203
|
-
): Promise<Record<string, unknown>> {
|
|
204
|
-
const data = await this.http.request<unknown>(
|
|
205
|
-
`/webhooks/${webhookId}/test`,
|
|
206
|
-
{
|
|
207
|
-
method: "POST",
|
|
208
|
-
headers: { "Idempotency-Key": idempotencyKey(opts.idempotencyKey) },
|
|
209
|
-
},
|
|
210
|
-
);
|
|
211
|
-
return unwrap<Record<string, unknown>>(data);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async deliveries(webhookId: string): Promise<WebhookDeliveryData[]> {
|
|
215
|
-
const data = await this.http.get<unknown>(
|
|
216
|
-
`/webhooks/${webhookId}/deliveries`,
|
|
217
|
-
);
|
|
218
|
-
return listItems<WebhookDeliveryData>(data, [
|
|
219
|
-
"data",
|
|
220
|
-
"deliveries",
|
|
221
|
-
"items",
|
|
222
|
-
]);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace Invites — email invite flow for workspace access.
|
|
3
|
-
*
|
|
4
|
-
* Sending an invite to an email that already belongs to a tenant member
|
|
5
|
-
* short-circuits to directly adding that user (returns `type: "added"`).
|
|
6
|
-
* Accepting a workspace invite for an unknown email auto-creates both a
|
|
7
|
-
* `tenant_members` and a `workspace_members` row atomically.
|
|
8
|
-
*
|
|
9
|
-
* Public endpoints (no auth):
|
|
10
|
-
* GET /workspace-invites/:token
|
|
11
|
-
*
|
|
12
|
-
* Authenticated endpoints:
|
|
13
|
-
* POST /workspaces/:id/invites
|
|
14
|
-
* GET /workspaces/:id/invites
|
|
15
|
-
* DELETE /workspaces/:id/invites/:invite_id
|
|
16
|
-
* POST /workspace-invites/:token/accept
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { HttpClient } from "../http.js";
|
|
20
|
-
import type {
|
|
21
|
-
WorkspaceMemberRecord,
|
|
22
|
-
WorkspaceRole,
|
|
23
|
-
} from "./workspace-members.js";
|
|
24
|
-
|
|
25
|
-
// ── Resource shapes ──────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface WorkspaceInvite {
|
|
28
|
-
id: string;
|
|
29
|
-
workspace_id: string;
|
|
30
|
-
tenant_id: string;
|
|
31
|
-
email: string;
|
|
32
|
-
role: WorkspaceRole;
|
|
33
|
-
invited_by: string | null;
|
|
34
|
-
expires_at: string;
|
|
35
|
-
accepted_at: string | null;
|
|
36
|
-
inserted_at: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface WorkspaceInvitePreview {
|
|
40
|
-
workspace_name: string;
|
|
41
|
-
tenant_name: string;
|
|
42
|
-
role: WorkspaceRole;
|
|
43
|
-
email: string;
|
|
44
|
-
expires_at: string;
|
|
45
|
-
expired: boolean;
|
|
46
|
-
revoked: boolean;
|
|
47
|
-
accepted: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── Request payloads ─────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
export interface CreateWorkspaceInviteParams {
|
|
53
|
-
email: string;
|
|
54
|
-
role?: WorkspaceRole;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Response shapes ───────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/** Returned when the email was unknown — an invite was created. */
|
|
60
|
-
export interface WorkspaceInviteCreatedResponse {
|
|
61
|
-
data: WorkspaceInvite;
|
|
62
|
-
type: "invited";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Returned when the email already had a tenant_members row — added directly. */
|
|
66
|
-
export interface WorkspaceMemberAddedResponse {
|
|
67
|
-
data: WorkspaceMemberRecord;
|
|
68
|
-
type: "added";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export type CreateWorkspaceInviteResponse =
|
|
72
|
-
| WorkspaceInviteCreatedResponse
|
|
73
|
-
| WorkspaceMemberAddedResponse;
|
|
74
|
-
|
|
75
|
-
export interface WorkspaceInviteListResponse {
|
|
76
|
-
data: WorkspaceInvite[];
|
|
77
|
-
total: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface WorkspaceInviteRevokeResponse {
|
|
81
|
-
invite_id: string;
|
|
82
|
-
revoked: boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface WorkspaceInvitePreviewResponse {
|
|
86
|
-
data: WorkspaceInvitePreview;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface AcceptWorkspaceInviteResponse {
|
|
90
|
-
accepted: boolean;
|
|
91
|
-
workspace_id: string;
|
|
92
|
-
tenant_id: string;
|
|
93
|
-
role: WorkspaceRole;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Main resource ─────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
export class WorkspaceInvites {
|
|
99
|
-
constructor(private readonly http: HttpClient) {}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Create a workspace invite or add a member directly.
|
|
103
|
-
*
|
|
104
|
-
* If `email` already maps to a tenant member the user is added directly and
|
|
105
|
-
* `type === "added"` is returned with a `WorkspaceMemberRecord`. Otherwise
|
|
106
|
-
* an invite row is created and `type === "invited"` is returned.
|
|
107
|
-
*
|
|
108
|
-
* `POST /workspaces/:id/invites`
|
|
109
|
-
*/
|
|
110
|
-
async create(
|
|
111
|
-
workspaceId: string,
|
|
112
|
-
params: CreateWorkspaceInviteParams,
|
|
113
|
-
): Promise<CreateWorkspaceInviteResponse> {
|
|
114
|
-
return this.http.post<CreateWorkspaceInviteResponse>(
|
|
115
|
-
`/workspaces/${workspaceId}/invites`,
|
|
116
|
-
params,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* List all pending (non-expired, non-accepted, non-revoked) workspace invites.
|
|
122
|
-
*
|
|
123
|
-
* `GET /workspaces/:id/invites`
|
|
124
|
-
*/
|
|
125
|
-
async list(workspaceId: string): Promise<WorkspaceInvite[]> {
|
|
126
|
-
const res = await this.http.get<WorkspaceInviteListResponse>(
|
|
127
|
-
`/workspaces/${workspaceId}/invites`,
|
|
128
|
-
);
|
|
129
|
-
return res.data ?? [];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Revoke a pending workspace invite.
|
|
134
|
-
*
|
|
135
|
-
* Already-revoked invites are idempotent (returns `revoked: true`). An invite
|
|
136
|
-
* that was legitimately accepted throws `409 ALREADY_ACCEPTED`.
|
|
137
|
-
*
|
|
138
|
-
* `DELETE /workspaces/:id/invites/:invite_id`
|
|
139
|
-
*/
|
|
140
|
-
async revoke(
|
|
141
|
-
workspaceId: string,
|
|
142
|
-
inviteId: string,
|
|
143
|
-
): Promise<WorkspaceInviteRevokeResponse> {
|
|
144
|
-
return this.http.delete<WorkspaceInviteRevokeResponse>(
|
|
145
|
-
`/workspaces/${workspaceId}/invites/${inviteId}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Preview a workspace invite by token (no auth required).
|
|
151
|
-
*
|
|
152
|
-
* Use this to render the invite landing page before prompting the user to
|
|
153
|
-
* log in or sign up. Returns `null` when the token is unknown or revoked.
|
|
154
|
-
*
|
|
155
|
-
* `GET /workspace-invites/:token`
|
|
156
|
-
*/
|
|
157
|
-
async preview(token: string): Promise<WorkspaceInvitePreview | null> {
|
|
158
|
-
try {
|
|
159
|
-
const res = await this.http.get<WorkspaceInvitePreviewResponse>(
|
|
160
|
-
`/workspace-invites/${token}`,
|
|
161
|
-
);
|
|
162
|
-
return res.data ?? null;
|
|
163
|
-
} catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Accept a workspace invite on behalf of the authenticated user.
|
|
170
|
-
*
|
|
171
|
-
* The caller's JWT email must match the invite email (case-insensitive).
|
|
172
|
-
*
|
|
173
|
-
* Error codes:
|
|
174
|
-
* - `INVALID_TOKEN` (404) — token not found.
|
|
175
|
-
* - `EXPIRED` (410) — invite TTL elapsed.
|
|
176
|
-
* - `REVOKED` (409) — invite was revoked.
|
|
177
|
-
* - `ALREADY_ACCEPTED` (409) — already used.
|
|
178
|
-
* - `EMAIL_MISMATCH` (422) — JWT email differs from invite email.
|
|
179
|
-
*
|
|
180
|
-
* `POST /workspace-invites/:token/accept`
|
|
181
|
-
*/
|
|
182
|
-
async accept(token: string): Promise<AcceptWorkspaceInviteResponse> {
|
|
183
|
-
return this.http.post<AcceptWorkspaceInviteResponse>(
|
|
184
|
-
`/workspace-invites/${token}/accept`,
|
|
185
|
-
{},
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { HttpClient } from "../http.js";
|
|
3
|
-
import { WorkspaceMembers } from "./workspace-members.js";
|
|
4
|
-
|
|
5
|
-
const mockGet = vi.fn();
|
|
6
|
-
const mockPost = vi.fn();
|
|
7
|
-
const mockPatch = vi.fn();
|
|
8
|
-
const mockDelete = vi.fn();
|
|
9
|
-
const mockRequest = vi.fn();
|
|
10
|
-
|
|
11
|
-
function makeHttp(): HttpClient {
|
|
12
|
-
const http = {} as HttpClient;
|
|
13
|
-
http.get = mockGet;
|
|
14
|
-
http.post = mockPost;
|
|
15
|
-
http.patch = mockPatch;
|
|
16
|
-
http.delete = mockDelete;
|
|
17
|
-
http.request = mockRequest;
|
|
18
|
-
return http;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const MEMBER = {
|
|
22
|
-
user_id: "usr_abc",
|
|
23
|
-
email: "alice@example.com",
|
|
24
|
-
name: "Alice",
|
|
25
|
-
avatar_url: null,
|
|
26
|
-
role: "member" as const,
|
|
27
|
-
joined_at: "2026-05-01T10:00:00Z",
|
|
28
|
-
added_by: null,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
vi.clearAllMocks();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("WorkspaceMembers", () => {
|
|
36
|
-
describe("list()", () => {
|
|
37
|
-
it("calls GET /workspaces/:id/members and returns array", async () => {
|
|
38
|
-
mockGet.mockResolvedValue({ data: [MEMBER] });
|
|
39
|
-
const resource = new WorkspaceMembers(makeHttp());
|
|
40
|
-
|
|
41
|
-
const result = await resource.list("ws-uuid");
|
|
42
|
-
|
|
43
|
-
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-uuid/members");
|
|
44
|
-
expect(result).toHaveLength(1);
|
|
45
|
-
expect(result[0].user_id).toBe("usr_abc");
|
|
46
|
-
expect(result[0].email).toBe("alice@example.com");
|
|
47
|
-
expect(result[0].role).toBe("member");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns empty array when data is empty", async () => {
|
|
51
|
-
mockGet.mockResolvedValue({ data: [] });
|
|
52
|
-
const resource = new WorkspaceMembers(makeHttp());
|
|
53
|
-
|
|
54
|
-
const result = await resource.list("ws-uuid");
|
|
55
|
-
expect(result).toEqual([]);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("add()", () => {
|
|
60
|
-
it("calls POST /workspaces/:id/members with userId and role", async () => {
|
|
61
|
-
const record = {
|
|
62
|
-
user_id: "usr_def",
|
|
63
|
-
workspace_id: "ws-uuid",
|
|
64
|
-
role: "member",
|
|
65
|
-
joined_at: "2026-05-22T09:00:00Z",
|
|
66
|
-
added_by: "usr_abc",
|
|
67
|
-
};
|
|
68
|
-
mockPost.mockResolvedValue({ data: record });
|
|
69
|
-
const resource = new WorkspaceMembers(makeHttp());
|
|
70
|
-
|
|
71
|
-
const result = await resource.add("ws-uuid", {
|
|
72
|
-
user_id: "usr_def",
|
|
73
|
-
role: "member",
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(mockPost).toHaveBeenCalledWith(
|
|
77
|
-
"/workspaces/ws-uuid/members",
|
|
78
|
-
expect.objectContaining({ user_id: "usr_def", role: "member" }),
|
|
79
|
-
);
|
|
80
|
-
expect(result.user_id).toBe("usr_def");
|
|
81
|
-
expect(result.role).toBe("member");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("updateRole()", () => {
|
|
86
|
-
it("calls PATCH /workspaces/:id/members/:userId with new role", async () => {
|
|
87
|
-
const updated = {
|
|
88
|
-
user_id: "usr_def",
|
|
89
|
-
workspace_id: "ws-uuid",
|
|
90
|
-
role: "admin",
|
|
91
|
-
joined_at: "2026-05-22T09:00:00Z",
|
|
92
|
-
added_by: "usr_abc",
|
|
93
|
-
};
|
|
94
|
-
mockPatch.mockResolvedValue({ data: updated });
|
|
95
|
-
const resource = new WorkspaceMembers(makeHttp());
|
|
96
|
-
|
|
97
|
-
const result = await resource.updateRole("ws-uuid", "usr_def", {
|
|
98
|
-
role: "admin",
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
expect(mockPatch).toHaveBeenCalledWith(
|
|
102
|
-
"/workspaces/ws-uuid/members/usr_def",
|
|
103
|
-
{ role: "admin" },
|
|
104
|
-
);
|
|
105
|
-
expect(result.role).toBe("admin");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("remove()", () => {
|
|
110
|
-
it("calls DELETE /workspaces/:id/members/:userId", async () => {
|
|
111
|
-
mockDelete.mockResolvedValue({ deleted: true });
|
|
112
|
-
const resource = new WorkspaceMembers(makeHttp());
|
|
113
|
-
|
|
114
|
-
await resource.remove("ws-uuid", "usr_def");
|
|
115
|
-
|
|
116
|
-
expect(mockDelete).toHaveBeenCalledWith(
|
|
117
|
-
"/workspaces/ws-uuid/members/usr_def",
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
});
|