@miosa/sdk 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1209 -18
- package/dist/index.js +1698 -252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +44 -0
- package/src/index.ts +100 -0
- package/src/resources/api-keys.ts +5 -1
- package/src/resources/computer.ts +16 -0
- package/src/resources/cron-jobs.ts +7 -0
- package/src/resources/custom_domains.ts +7 -0
- package/src/resources/deployments.ts +13 -2
- package/src/resources/egress.test.ts +318 -0
- package/src/resources/egressAudit.ts +245 -0
- package/src/resources/egressNetwork.ts +450 -0
- package/src/resources/egressSecrets.ts +577 -0
- package/src/resources/flat-custom-domains.ts +7 -0
- package/src/resources/functions.ts +7 -0
- package/src/resources/org-invites.ts +189 -0
- package/src/resources/phase1.test.ts +187 -0
- package/src/resources/quotas.ts +77 -0
- package/src/resources/sandbox-processes.ts +112 -0
- package/src/resources/sandbox-shares.ts +83 -0
- package/src/resources/sandboxes.ts +239 -10
- package/src/resources/storage.ts +7 -0
- package/src/resources/tenant-events.ts +32 -0
- package/src/resources/tenant.ts +82 -3
- package/src/resources/volumes.ts +7 -0
- package/src/resources/webhooks.ts +55 -0
- package/src/resources/workspace-invites.ts +188 -0
- package/src/resources/workspace-members.test.ts +121 -0
- package/src/resources/workspace-members.ts +143 -0
- package/src/resources/workspaces.ts +285 -0
- package/src/types.ts +7 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { MiosaError } from "../errors.js";
|
|
2
2
|
import { HttpClient } from "../http.js";
|
|
3
|
+
import { SandboxAudit } from "./egressAudit.js";
|
|
4
|
+
import { SandboxNetwork } from "./egressNetwork.js";
|
|
5
|
+
import { SandboxSecrets } from "./egressSecrets.js";
|
|
6
|
+
|
|
7
|
+
function encodeContent(content: string | Uint8Array): string {
|
|
8
|
+
const bytes =
|
|
9
|
+
typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
10
|
+
const maybeBuffer = (
|
|
11
|
+
globalThis as {
|
|
12
|
+
Buffer?: { from(b: Uint8Array): { toString(e: string): string } };
|
|
13
|
+
}
|
|
14
|
+
).Buffer;
|
|
15
|
+
if (maybeBuffer) return maybeBuffer.from(bytes).toString("base64");
|
|
16
|
+
let bin = "";
|
|
17
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);
|
|
18
|
+
return btoa(bin);
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
export const SANDBOX_TEMPLATE = "miosa-sandbox";
|
|
5
22
|
|
|
@@ -47,6 +64,7 @@ export interface SandboxCreateParams {
|
|
|
47
64
|
tags?: string[];
|
|
48
65
|
idempotencyKey?: string;
|
|
49
66
|
idempotency_key?: string;
|
|
67
|
+
slug?: string;
|
|
50
68
|
// White-label attribution. See platform/attribution docs.
|
|
51
69
|
externalWorkspaceId?: string;
|
|
52
70
|
external_workspace_id?: string;
|
|
@@ -231,6 +249,37 @@ export interface SandboxFileEntry {
|
|
|
231
249
|
[key: string]: unknown;
|
|
232
250
|
}
|
|
233
251
|
|
|
252
|
+
export interface SandboxFileTreeNode {
|
|
253
|
+
path: string;
|
|
254
|
+
name: string;
|
|
255
|
+
type: "file" | "dir";
|
|
256
|
+
size?: number;
|
|
257
|
+
modified_at?: string;
|
|
258
|
+
children?: SandboxFileTreeNode[];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface SandboxWriteManyEntry {
|
|
262
|
+
path: string;
|
|
263
|
+
content: string | Uint8Array;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface SandboxWriteManyResult {
|
|
267
|
+
written: Array<{ path: string; size_bytes: number }>;
|
|
268
|
+
failed: Array<{ path: string; error: string }>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface SandboxFileChange {
|
|
272
|
+
type: "created" | "modified" | "deleted";
|
|
273
|
+
path: string;
|
|
274
|
+
size_bytes?: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface SandboxEnvVar {
|
|
278
|
+
key: string;
|
|
279
|
+
encrypted: boolean;
|
|
280
|
+
value?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
234
283
|
export interface SandboxFileList {
|
|
235
284
|
path?: string;
|
|
236
285
|
entries: SandboxFileEntry[];
|
|
@@ -328,6 +377,7 @@ function createBody(params: SandboxCreateParams = {}): Record<string, unknown> {
|
|
|
328
377
|
region: params.region,
|
|
329
378
|
entrypoint: params.entrypoint,
|
|
330
379
|
tags: params.tags,
|
|
380
|
+
slug: params.slug,
|
|
331
381
|
external_workspace_id:
|
|
332
382
|
params.externalWorkspaceId ?? params.external_workspace_id,
|
|
333
383
|
external_user_id: params.externalUserId ?? params.external_user_id,
|
|
@@ -403,6 +453,54 @@ export class SandboxFiles {
|
|
|
403
453
|
download(path: string): Promise<Uint8Array> {
|
|
404
454
|
return this.read(path);
|
|
405
455
|
}
|
|
456
|
+
|
|
457
|
+
/** GET /api/v1/sandboxes/{id}/files/tree — recursive directory tree. */
|
|
458
|
+
async tree(path = "/workspace", depth = 3): Promise<SandboxFileTreeNode> {
|
|
459
|
+
const http = (this.sandbox as unknown as { http: HttpClient }).http;
|
|
460
|
+
const response = await http.get<unknown>(
|
|
461
|
+
`/sandboxes/${this.sandbox.id}/files/tree`,
|
|
462
|
+
{ path, depth },
|
|
463
|
+
);
|
|
464
|
+
if (
|
|
465
|
+
response &&
|
|
466
|
+
typeof response === "object" &&
|
|
467
|
+
"data" in (response as object)
|
|
468
|
+
) {
|
|
469
|
+
return (response as { data: SandboxFileTreeNode }).data;
|
|
470
|
+
}
|
|
471
|
+
return response as SandboxFileTreeNode;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** POST /api/v1/sandboxes/{id}/files/write-many — write multiple files atomically. */
|
|
475
|
+
async writeMany(
|
|
476
|
+
files: SandboxWriteManyEntry[],
|
|
477
|
+
): Promise<SandboxWriteManyResult> {
|
|
478
|
+
const http = (this.sandbox as unknown as { http: HttpClient }).http;
|
|
479
|
+
const payload = files.map((f) => ({
|
|
480
|
+
path: f.path,
|
|
481
|
+
content_base64: encodeContent(f.content),
|
|
482
|
+
}));
|
|
483
|
+
const response = await http.post<unknown>(
|
|
484
|
+
`/sandboxes/${this.sandbox.id}/files/write-many`,
|
|
485
|
+
{ files: payload },
|
|
486
|
+
);
|
|
487
|
+
if (
|
|
488
|
+
response &&
|
|
489
|
+
typeof response === "object" &&
|
|
490
|
+
"data" in (response as object)
|
|
491
|
+
) {
|
|
492
|
+
return (response as { data: SandboxWriteManyResult }).data;
|
|
493
|
+
}
|
|
494
|
+
return response as SandboxWriteManyResult;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** GET /api/v1/sandboxes/{id}/files/watch (SSE) — live file change events. */
|
|
498
|
+
watch(): AsyncIterableIterator<SandboxFileChange> {
|
|
499
|
+
const http = (this.sandbox as unknown as { http: HttpClient }).http;
|
|
500
|
+
return http.stream<SandboxFileChange>(
|
|
501
|
+
`/sandboxes/${this.sandbox.id}/files/watch`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
406
504
|
}
|
|
407
505
|
|
|
408
506
|
export class SandboxPreview {
|
|
@@ -571,16 +669,53 @@ export class SandboxPreviews {
|
|
|
571
669
|
export class SandboxEnv {
|
|
572
670
|
constructor(private readonly sandbox: Sandbox) {}
|
|
573
671
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
) as
|
|
672
|
+
private get http(): HttpClient {
|
|
673
|
+
return (this.sandbox as unknown as { http: HttpClient }).http;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** GET /api/v1/sandboxes/{id}/env → list of env vars. */
|
|
677
|
+
async get(): Promise<SandboxEnvVar[]> {
|
|
678
|
+
const response = await this.http.get<unknown>(
|
|
679
|
+
`/sandboxes/${this.sandbox.id}/env`,
|
|
680
|
+
);
|
|
681
|
+
if (Array.isArray(response)) return response as SandboxEnvVar[];
|
|
682
|
+
if (response && typeof response === "object") {
|
|
683
|
+
const r = response as Record<string, unknown>;
|
|
684
|
+
for (const k of ["data", "vars", "env", "items"]) {
|
|
685
|
+
if (Array.isArray(r[k])) return r[k] as SandboxEnvVar[];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** @deprecated Use get() */
|
|
692
|
+
async list(): Promise<SandboxEnvVar[]> {
|
|
693
|
+
return this.get();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** PUT /api/v1/sandboxes/{id}/env — set (replace) env vars. */
|
|
697
|
+
async set(
|
|
698
|
+
vars: Array<{ key: string; value: string; encrypted?: boolean }>,
|
|
699
|
+
): Promise<SandboxEnvVar[]> {
|
|
700
|
+
const response = await this.http.put<unknown>(
|
|
701
|
+
`/sandboxes/${this.sandbox.id}/env`,
|
|
702
|
+
{ vars },
|
|
703
|
+
);
|
|
704
|
+
if (Array.isArray(response)) return response as SandboxEnvVar[];
|
|
705
|
+
if (response && typeof response === "object") {
|
|
706
|
+
const r = response as Record<string, unknown>;
|
|
707
|
+
for (const k of ["data", "vars", "env", "items"]) {
|
|
708
|
+
if (Array.isArray(r[k])) return r[k] as SandboxEnvVar[];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** DELETE /api/v1/sandboxes/{id}/env/{key} — remove a single env var. */
|
|
715
|
+
async delete(key: string): Promise<void> {
|
|
716
|
+
await this.http.delete<unknown>(
|
|
717
|
+
`/sandboxes/${this.sandbox.id}/env/${encodeURIComponent(key)}`,
|
|
718
|
+
);
|
|
584
719
|
}
|
|
585
720
|
}
|
|
586
721
|
|
|
@@ -616,6 +751,12 @@ export class Sandbox {
|
|
|
616
751
|
readonly env: SandboxEnv;
|
|
617
752
|
/** Tag replacement. */
|
|
618
753
|
readonly tags: SandboxTags;
|
|
754
|
+
/** Encrypted secrets + OAuth credentials scoped to this sandbox. */
|
|
755
|
+
readonly secrets: SandboxSecrets;
|
|
756
|
+
/** Egress allowlist + policies scoped to this sandbox. */
|
|
757
|
+
readonly network: SandboxNetwork;
|
|
758
|
+
/** Egress audit log + live tail scoped to this sandbox. */
|
|
759
|
+
readonly audit: SandboxAudit;
|
|
619
760
|
|
|
620
761
|
constructor(
|
|
621
762
|
private readonly http: HttpClient,
|
|
@@ -640,6 +781,11 @@ export class Sandbox {
|
|
|
640
781
|
this.previews = new SandboxPreviews(this);
|
|
641
782
|
this.env = new SandboxEnv(this);
|
|
642
783
|
this.tags = new SandboxTags(this);
|
|
784
|
+
// Egress (security) namespaces — pre-scoped to this sandbox id.
|
|
785
|
+
const sandboxId = data.id as string;
|
|
786
|
+
this.secrets = new SandboxSecrets(http, sandboxId);
|
|
787
|
+
this.network = new SandboxNetwork(http, sandboxId);
|
|
788
|
+
this.audit = new SandboxAudit(http, sandboxId);
|
|
643
789
|
}
|
|
644
790
|
|
|
645
791
|
get id(): SandboxId {
|
|
@@ -823,6 +969,89 @@ export class Sandbox {
|
|
|
823
969
|
await this.http.delete(`/sandboxes/${this.id}/snapshots/${snapshotId}`);
|
|
824
970
|
}
|
|
825
971
|
|
|
972
|
+
/**
|
|
973
|
+
* Fork (clone) this sandbox into a new sandbox via copy-on-write snapshot.
|
|
974
|
+
* The original sandbox continues running unchanged.
|
|
975
|
+
*/
|
|
976
|
+
async fork(
|
|
977
|
+
opts: { name?: string; metadata?: Record<string, unknown> } = {},
|
|
978
|
+
): Promise<Sandbox> {
|
|
979
|
+
this.assertRunning("fork");
|
|
980
|
+
const body: Record<string, unknown> = {};
|
|
981
|
+
if (opts.name !== undefined) body.name = opts.name;
|
|
982
|
+
if (opts.metadata !== undefined) body.metadata = opts.metadata;
|
|
983
|
+
const data = unwrap(
|
|
984
|
+
await this.http.post<WireEnvelope<SandboxData>>(
|
|
985
|
+
`/sandboxes/${this.id}/fork`,
|
|
986
|
+
body,
|
|
987
|
+
),
|
|
988
|
+
);
|
|
989
|
+
return new Sandbox(this.http, data);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* PATCH /api/v1/sandboxes/{id} — update mutable sandbox fields.
|
|
994
|
+
*/
|
|
995
|
+
async update(params: {
|
|
996
|
+
name?: string;
|
|
997
|
+
slug?: string;
|
|
998
|
+
tags?: string[];
|
|
999
|
+
metadata?: Record<string, unknown>;
|
|
1000
|
+
always_on?: boolean;
|
|
1001
|
+
timeout_sec?: number;
|
|
1002
|
+
idle_timeout_sec?: number;
|
|
1003
|
+
}): Promise<Sandbox> {
|
|
1004
|
+
const body: Record<string, unknown> = {};
|
|
1005
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1006
|
+
if (v !== undefined) body[k] = v;
|
|
1007
|
+
}
|
|
1008
|
+
const data = unwrap(
|
|
1009
|
+
await this.http.patch<WireEnvelope<SandboxData>>(
|
|
1010
|
+
`/sandboxes/${this.id}`,
|
|
1011
|
+
body,
|
|
1012
|
+
),
|
|
1013
|
+
);
|
|
1014
|
+
this.data = data;
|
|
1015
|
+
return this;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* POST /api/v1/sandboxes/{id}/preview-token → {token, url, expires_at, scope}
|
|
1020
|
+
*/
|
|
1021
|
+
async previewToken(
|
|
1022
|
+
expiresIn = 3600,
|
|
1023
|
+
scope = "read",
|
|
1024
|
+
): Promise<{
|
|
1025
|
+
token: string;
|
|
1026
|
+
url: string;
|
|
1027
|
+
expires_at: string;
|
|
1028
|
+
scope: string;
|
|
1029
|
+
[key: string]: unknown;
|
|
1030
|
+
}> {
|
|
1031
|
+
const raw = await this.http.post<unknown>(
|
|
1032
|
+
`/sandboxes/${this.id}/preview-token`,
|
|
1033
|
+
{ expires_in: expiresIn, scope },
|
|
1034
|
+
);
|
|
1035
|
+
if (raw && typeof raw === "object" && "data" in (raw as object)) {
|
|
1036
|
+
return (
|
|
1037
|
+
raw as {
|
|
1038
|
+
data: {
|
|
1039
|
+
token: string;
|
|
1040
|
+
url: string;
|
|
1041
|
+
expires_at: string;
|
|
1042
|
+
scope: string;
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
).data;
|
|
1046
|
+
}
|
|
1047
|
+
return raw as {
|
|
1048
|
+
token: string;
|
|
1049
|
+
url: string;
|
|
1050
|
+
expires_at: string;
|
|
1051
|
+
scope: string;
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
826
1055
|
async pause(): Promise<Sandbox> {
|
|
827
1056
|
const data = unwrap(
|
|
828
1057
|
await this.http.post<WireEnvelope<SandboxData>>(
|
package/src/resources/storage.ts
CHANGED
|
@@ -49,6 +49,13 @@ export interface BucketCreateParams {
|
|
|
49
49
|
visibility?: "private" | "public";
|
|
50
50
|
quota_bytes?: number;
|
|
51
51
|
public?: boolean;
|
|
52
|
+
// White-label attribution
|
|
53
|
+
externalWorkspaceId?: string;
|
|
54
|
+
external_workspace_id?: string;
|
|
55
|
+
externalUserId?: string;
|
|
56
|
+
external_user_id?: string;
|
|
57
|
+
externalProjectId?: string;
|
|
58
|
+
external_project_id?: string;
|
|
52
59
|
[key: string]: unknown;
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TenantEvents — tenant-scoped SSE event stream.
|
|
3
|
+
* Corresponds to: GET /api/v1/events/stream?types=sandbox.*,webhook.delivered
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HttpClient } from "../http.js";
|
|
7
|
+
|
|
8
|
+
// ── Resource shapes ──────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface TenantStreamEvent {
|
|
11
|
+
type: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Main resource ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export class TenantEvents {
|
|
18
|
+
constructor(private readonly http: HttpClient) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /api/v1/events/stream — tenant-scoped SSE event stream.
|
|
22
|
+
*
|
|
23
|
+
* @param types - Event type globs to filter. Accepts a comma-separated string
|
|
24
|
+
* or an array, e.g. `["sandbox.*", "webhook.delivered"]`.
|
|
25
|
+
* Omit to receive all event types.
|
|
26
|
+
*/
|
|
27
|
+
stream(types?: string | string[]): AsyncIterableIterator<TenantStreamEvent> {
|
|
28
|
+
const typesParam = Array.isArray(types) ? types.join(",") : types;
|
|
29
|
+
const query = typesParam ? `?types=${encodeURIComponent(typesParam)}` : "";
|
|
30
|
+
return this.http.stream<TenantStreamEvent>(`/events/stream${query}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/resources/tenant.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tenant — current tenant info and
|
|
2
|
+
* Tenant — current tenant info, preview domain, and branding.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { HttpClient } from "../http.js";
|
|
@@ -14,22 +14,101 @@ export interface TenantPlan {
|
|
|
14
14
|
[key: string]: unknown;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface PreviewDomainData {
|
|
18
|
+
domain: string;
|
|
19
|
+
verified_at?: string | null;
|
|
20
|
+
cname_target?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PreviewDomainVerifyResult {
|
|
25
|
+
verified: boolean;
|
|
26
|
+
target?: string;
|
|
27
|
+
records?: unknown[];
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BrandingData {
|
|
32
|
+
product_name?: string;
|
|
33
|
+
logo_url?: string;
|
|
34
|
+
support_url?: string;
|
|
35
|
+
support_email?: string;
|
|
36
|
+
primary_color?: string;
|
|
37
|
+
background_color?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
18
42
|
|
|
19
43
|
function unwrap<T>(payload: unknown): T {
|
|
20
44
|
if (payload && typeof payload === "object") {
|
|
21
45
|
const p = payload as Record<string, unknown>;
|
|
22
|
-
for (const k of ["data", "tenant", "items"]) {
|
|
46
|
+
for (const k of ["data", "tenant", "branding", "preview_domain", "items"]) {
|
|
23
47
|
if (k in p) return p[k] as T;
|
|
24
48
|
}
|
|
25
49
|
}
|
|
26
50
|
return payload as T;
|
|
27
51
|
}
|
|
28
52
|
|
|
53
|
+
// ── Sub-resources ─────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export class PreviewDomain {
|
|
56
|
+
constructor(private readonly http: HttpClient) {}
|
|
57
|
+
|
|
58
|
+
/** GET /api/v1/tenant/preview-domain → {domain, verified_at, cname_target} */
|
|
59
|
+
async get(): Promise<PreviewDomainData> {
|
|
60
|
+
return unwrap(await this.http.get<unknown>("/tenant/preview-domain"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** PUT /api/v1/tenant/preview-domain — set the preview domain. */
|
|
64
|
+
async set(domain: string): Promise<PreviewDomainData> {
|
|
65
|
+
return unwrap(
|
|
66
|
+
await this.http.put<unknown>("/tenant/preview-domain", { domain }),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** POST /api/v1/tenant/preview-domain/verify → {verified, target, records} */
|
|
71
|
+
async verify(): Promise<PreviewDomainVerifyResult> {
|
|
72
|
+
return unwrap(
|
|
73
|
+
await this.http.post<unknown>("/tenant/preview-domain/verify", {}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** DELETE /api/v1/tenant/preview-domain */
|
|
78
|
+
async delete(): Promise<void> {
|
|
79
|
+
await this.http.delete<unknown>("/tenant/preview-domain");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class Branding {
|
|
84
|
+
constructor(private readonly http: HttpClient) {}
|
|
85
|
+
|
|
86
|
+
/** GET /api/v1/tenant/branding */
|
|
87
|
+
async get(): Promise<BrandingData> {
|
|
88
|
+
return unwrap(await this.http.get<unknown>("/tenant/branding"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** PUT /api/v1/tenant/branding — keys: product_name, logo_url, support_url, support_email, primary_color, background_color */
|
|
92
|
+
async set(branding: BrandingData): Promise<BrandingData> {
|
|
93
|
+
return unwrap(await this.http.put<unknown>("/tenant/branding", branding));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** DELETE /api/v1/tenant/branding */
|
|
97
|
+
async delete(): Promise<void> {
|
|
98
|
+
await this.http.delete<unknown>("/tenant/branding");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
29
102
|
// ── Main resource ─────────────────────────────────────────────────────────────
|
|
30
103
|
|
|
31
104
|
export class Tenant {
|
|
32
|
-
|
|
105
|
+
readonly preview_domain: PreviewDomain;
|
|
106
|
+
readonly branding: Branding;
|
|
107
|
+
|
|
108
|
+
constructor(private readonly http: HttpClient) {
|
|
109
|
+
this.preview_domain = new PreviewDomain(http);
|
|
110
|
+
this.branding = new Branding(http);
|
|
111
|
+
}
|
|
33
112
|
|
|
34
113
|
/** Get the current tenant's plan, limits, and live usage counters. */
|
|
35
114
|
async current(): Promise<TenantPlan> {
|
package/src/resources/volumes.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface VolumeCreateParams {
|
|
|
40
40
|
sizeGb?: number;
|
|
41
41
|
region?: string;
|
|
42
42
|
idempotencyKey?: string;
|
|
43
|
+
// White-label attribution
|
|
44
|
+
externalWorkspaceId?: string;
|
|
45
|
+
external_workspace_id?: string;
|
|
46
|
+
externalUserId?: string;
|
|
47
|
+
external_user_id?: string;
|
|
48
|
+
externalProjectId?: string;
|
|
49
|
+
external_project_id?: string;
|
|
43
50
|
[key: string]: unknown;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -2,10 +2,48 @@
|
|
|
2
2
|
* Webhooks resource — tenant outgoing event delivery.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
|
|
7
8
|
import type { HttpClient } from "../http.js";
|
|
8
9
|
|
|
10
|
+
const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Verify an incoming ``X-Miosa-Signature`` header.
|
|
14
|
+
* Header format: ``t=<unix_ts>,v1=<hex_hmac>``
|
|
15
|
+
*/
|
|
16
|
+
export function verifySignature(
|
|
17
|
+
payload: string | Uint8Array,
|
|
18
|
+
signatureHeader: string,
|
|
19
|
+
secret: string,
|
|
20
|
+
): boolean {
|
|
21
|
+
const parts: Record<string, string> = {};
|
|
22
|
+
for (const part of signatureHeader.split(",")) {
|
|
23
|
+
const idx = part.indexOf("=");
|
|
24
|
+
if (idx > 0) parts[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
|
|
25
|
+
}
|
|
26
|
+
const tsStr = parts["t"];
|
|
27
|
+
const sig = parts["v1"];
|
|
28
|
+
if (!tsStr || !sig) return false;
|
|
29
|
+
|
|
30
|
+
const ts = parseInt(tsStr, 10);
|
|
31
|
+
if (isNaN(ts)) return false;
|
|
32
|
+
if (Date.now() - ts * 1000 > MAX_TIMESTAMP_AGE_MS) {
|
|
33
|
+
throw new Error(`Webhook timestamp is too old: ${ts}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const body =
|
|
37
|
+
payload instanceof Uint8Array ? payload : Buffer.from(payload, "utf-8");
|
|
38
|
+
const signed = Buffer.concat([Buffer.from(`${tsStr}.`, "utf-8"), body]);
|
|
39
|
+
const expected = createHmac("sha256", secret).update(signed).digest("hex");
|
|
40
|
+
|
|
41
|
+
return timingSafeEqual(
|
|
42
|
+
Buffer.from(expected, "utf-8"),
|
|
43
|
+
Buffer.from(sig, "utf-8"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
// ── Branded IDs ──────────────────────────────────────────────────────────────
|
|
10
48
|
|
|
11
49
|
export type WebhookId = string & { readonly __brand: "WebhookId" };
|
|
@@ -168,4 +206,21 @@ export class Webhooks {
|
|
|
168
206
|
"items",
|
|
169
207
|
]);
|
|
170
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Verify an incoming ``X-Miosa-Signature`` header.
|
|
212
|
+
*
|
|
213
|
+
* Header format: ``t=<unix_ts>,v1=<hex_hmac>``
|
|
214
|
+
* HMAC body: ``<t>.<raw_payload>``
|
|
215
|
+
*
|
|
216
|
+
* Throws if timestamp is older than 5 minutes.
|
|
217
|
+
* Returns true if signature matches.
|
|
218
|
+
*/
|
|
219
|
+
static verifySignature(
|
|
220
|
+
payload: string | Uint8Array,
|
|
221
|
+
signatureHeader: string,
|
|
222
|
+
secret: string,
|
|
223
|
+
): boolean {
|
|
224
|
+
return verifySignature(payload, signatureHeader, secret);
|
|
225
|
+
}
|
|
171
226
|
}
|