@bunbase-ae/js 1.0.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/src/admin.ts ADDED
@@ -0,0 +1,912 @@
1
+ // AdminClient — typed access to all /admin/* endpoints.
2
+ //
3
+ // Accessible via client.admin when the BunBaseClient is initialized with
4
+ // adminSecret, or when the authenticated user has the "admin" role.
5
+ //
6
+ // Sub-namespaces:
7
+ // client.admin.users — user management
8
+ // client.admin.sessions — session management
9
+ // client.admin.collections — collection + record + schema + index + FTS management
10
+ // client.admin.relations — collection relations
11
+ // client.admin.storage — files + buckets
12
+ // client.admin.settings — server settings, email templates, config, audit
13
+ // client.admin.backups — backup / restore
14
+ // client.admin.system — health + stats
15
+
16
+ import type { HttpClient } from "./http";
17
+
18
+ // ─── Shared admin types ───────────────────────────────────────────────────────
19
+
20
+ export interface AdminUser {
21
+ id: string;
22
+ email: string;
23
+ created_at: number;
24
+ updated_at: number;
25
+ is_verified: boolean;
26
+ session_count: number;
27
+ role: string | null;
28
+ metadata: Record<string, unknown>;
29
+ }
30
+
31
+ export interface AdminSession {
32
+ id: string;
33
+ user_id: string;
34
+ user_email: string | null;
35
+ expires_at: number;
36
+ created_at: number;
37
+ device_hint: string | null;
38
+ }
39
+
40
+ export interface AdminApiKey {
41
+ id: string;
42
+ name: string;
43
+ created_at: number;
44
+ last_used_at: number | null;
45
+ }
46
+
47
+ export interface ImpersonationResult {
48
+ access_token: string;
49
+ expires_in: number;
50
+ user_id: string;
51
+ }
52
+
53
+ export interface UpdateUserParams {
54
+ email?: string;
55
+ is_verified?: boolean;
56
+ role?: string | null;
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+
60
+ export type AccessRule = "public" | "authenticated" | "owner" | "disabled" | { role: string };
61
+
62
+ export interface CollectionRules {
63
+ read: AccessRule;
64
+ create: AccessRule;
65
+ update: AccessRule;
66
+ delete: AccessRule;
67
+ }
68
+
69
+ export interface AdminFieldRule {
70
+ field: string;
71
+ required?: boolean;
72
+ min?: number;
73
+ max?: number;
74
+ regex?: string;
75
+ enum?: unknown[];
76
+ }
77
+
78
+ export interface CollectionMeta {
79
+ name: string;
80
+ rules: CollectionRules;
81
+ field_rules: AdminFieldRule[];
82
+ created_at: number;
83
+ schema_version: number;
84
+ /** Webhook URL for async event delivery. Null means no webhook configured. */
85
+ webhook_url: string | null;
86
+ /** Whether an HMAC-SHA256 signing secret is configured. Secret value is write-only. */
87
+ has_webhook_secret: boolean;
88
+ /** Event filter. Null = all events. Array = only listed event types (create/update/delete). */
89
+ webhook_events: string[] | null;
90
+ }
91
+
92
+ export interface AdminRecord {
93
+ _id: string;
94
+ _created_at: number;
95
+ _updated_at: number;
96
+ _owner_id: string | null;
97
+ [key: string]: unknown;
98
+ }
99
+
100
+ export interface AdminListResult<T> {
101
+ items: T[];
102
+ total?: number;
103
+ /** @deprecated Present only when `page` was explicitly requested. */
104
+ page?: number;
105
+ limit: number;
106
+ /** Cursor for the next page. Null when no more pages or in non-keyset mode. */
107
+ next_cursor: string | null;
108
+ }
109
+
110
+ export interface ColumnInfo {
111
+ name: string;
112
+ type: string;
113
+ non_null_count: number | null;
114
+ is_system: boolean;
115
+ }
116
+
117
+ export interface CollectionStats {
118
+ row_count: number;
119
+ last_write: number | null;
120
+ }
121
+
122
+ export interface IndexInfo {
123
+ name: string;
124
+ column: string;
125
+ is_system: boolean;
126
+ }
127
+
128
+ export type RelationType = "one" | "many" | "many_via";
129
+ export type RelationOnDelete = "none" | "cascade" | "set_null";
130
+
131
+ export interface Relation {
132
+ id: string;
133
+ from_col: string;
134
+ from_field: string;
135
+ to_col: string;
136
+ type: RelationType;
137
+ via_col: string | null;
138
+ via_from: string | null;
139
+ via_to: string | null;
140
+ on_delete: RelationOnDelete;
141
+ created_at: number;
142
+ }
143
+
144
+ export interface CollectionIntrospection {
145
+ name: string;
146
+ columns: ColumnInfo[];
147
+ indexes: IndexInfo[];
148
+ relations: Relation[];
149
+ rules: CollectionRules;
150
+ field_rules: AdminFieldRule[];
151
+ fts_enabled: boolean;
152
+ webhook: {
153
+ url: string | null;
154
+ signing: boolean;
155
+ events: string[] | null;
156
+ };
157
+ stats: { row_count: number; last_write: number | null };
158
+ schema_version: number;
159
+ created_at: number;
160
+ }
161
+
162
+ export interface CreateRelationParams {
163
+ from_col: string;
164
+ from_field: string;
165
+ to_col: string;
166
+ type: RelationType;
167
+ via_col?: string;
168
+ via_from?: string;
169
+ via_to?: string;
170
+ on_delete?: RelationOnDelete;
171
+ }
172
+
173
+ export interface AdminStoredFile {
174
+ id: string;
175
+ key: string;
176
+ bucket: string;
177
+ filename: string | null;
178
+ collection: string | null;
179
+ record_id: string | null;
180
+ owner_id: string | null;
181
+ owner_email?: string | null;
182
+ size: number;
183
+ mime_type: string;
184
+ is_public: boolean;
185
+ created_at: number;
186
+ }
187
+
188
+ export type BucketAccessRule = "public" | "authenticated" | "owner" | "disabled";
189
+
190
+ export interface StorageBucket {
191
+ name: string;
192
+ access_read: BucketAccessRule;
193
+ access_write: BucketAccessRule;
194
+ allowed_mimes: string[] | null;
195
+ max_size_bytes: number | null;
196
+ cdn_url: string | null;
197
+ created_at: number;
198
+ }
199
+
200
+ export interface ServerSettings {
201
+ auto_create_buckets?: boolean;
202
+ auto_create_collections?: boolean;
203
+ storage_cdn_url?: string;
204
+ maintenance_mode?: boolean;
205
+ log_level?: "debug" | "info" | "warn" | "error" | "off";
206
+ registration_open?: boolean;
207
+ single_session_mode?: boolean;
208
+ require_email_verification?: boolean;
209
+ lockout_max_attempts?: number;
210
+ lockout_duration_ms?: number;
211
+ access_token_ttl_seconds?: number;
212
+ refresh_token_ttl_days?: number;
213
+ auth_password_enabled?: boolean;
214
+ auth_magic_link_enabled?: boolean;
215
+ auth_totp_enabled?: boolean;
216
+ auth_oauth_enabled?: boolean;
217
+ auth_api_keys_enabled?: boolean;
218
+ oauth_github_client_id?: string;
219
+ oauth_github_client_secret?: string;
220
+ oauth_google_client_id?: string;
221
+ oauth_google_client_secret?: string;
222
+ oauth_oidc_issuer?: string;
223
+ oauth_oidc_client_id?: string;
224
+ oauth_oidc_client_secret?: string;
225
+ oauth_oidc_scopes?: string;
226
+ app_name?: string;
227
+ app_url?: string;
228
+ logo_url?: string;
229
+ support_email?: string;
230
+ primary_color?: string;
231
+ rate_limit_public_reads?: number;
232
+ rate_limit_auth?: number;
233
+ rate_limit_authenticated?: number;
234
+ rate_limit_admin?: number;
235
+ rate_limit_file_upload?: number;
236
+ email_provider?: "console" | "resend" | "smtp";
237
+ email_from?: string;
238
+ resend_api_key?: string;
239
+ smtp_host?: string;
240
+ smtp_port?: number;
241
+ smtp_user?: string;
242
+ smtp_pass?: string;
243
+ smtp_secure?: boolean;
244
+ backup_auto_enabled?: boolean;
245
+ backup_interval_minutes?: number;
246
+ backup_keep_recent_hours?: number;
247
+ backup_keep_daily_days?: number;
248
+ backup_compression_format?: "gzip" | "zstd";
249
+ backup_compression_level?: number;
250
+ backup_remote_type?: "none" | "s3" | "rsync";
251
+ backup_remote_s3_endpoint?: string;
252
+ backup_remote_s3_bucket?: string;
253
+ backup_remote_s3_prefix?: string;
254
+ backup_remote_s3_region?: string;
255
+ backup_remote_s3_access_key_id?: string;
256
+ backup_remote_s3_secret_access_key?: string;
257
+ backup_remote_rsync_dest?: string;
258
+ server_timezone?: string;
259
+ server_locale?: string;
260
+ access_log_max_rows?: number;
261
+ app_log_max_rows?: number;
262
+ access_log_max_age_days?: number;
263
+ app_log_max_age_days?: number;
264
+ }
265
+
266
+ export type TemplateName = "password_reset" | "email_verification" | "magic_link";
267
+
268
+ export interface EmailTemplate {
269
+ subject: string;
270
+ html: string;
271
+ text: string;
272
+ is_default?: boolean;
273
+ }
274
+
275
+ export interface ConfigResponse {
276
+ effective: Record<string, string | null>;
277
+ overrides: Record<string, string | null>;
278
+ }
279
+
280
+ export interface SettingsAuditEntry {
281
+ id: string;
282
+ key: string;
283
+ old_value: unknown;
284
+ new_value: unknown;
285
+ changed_by: string;
286
+ changed_at: number;
287
+ }
288
+
289
+ export interface BackupFile {
290
+ filename: string;
291
+ size: number;
292
+ created_at: string;
293
+ }
294
+
295
+ export interface HealthResponse {
296
+ status: "ok" | "degraded";
297
+ version: string;
298
+ bun_version: string;
299
+ uptime_ms: number;
300
+ db: "ok" | "error";
301
+ db_provider: "sqlite" | "postgres";
302
+ redis: "ok" | "error" | "disabled";
303
+ }
304
+
305
+ export interface MinuteBucket {
306
+ minute: number;
307
+ requests: number;
308
+ errors: number;
309
+ }
310
+
311
+ export interface StatsResponse {
312
+ uptime_ms: number;
313
+ memory: { rss_mb: number; heap_used_mb: number };
314
+ pending_requests: number;
315
+ pending_websockets: number;
316
+ latency: { p50: number; p95: number; p99: number };
317
+ rpc: {
318
+ pending_high_water: number;
319
+ calls_by_method: Record<string, number>;
320
+ };
321
+ auth: {
322
+ outcomes: {
323
+ anonymous: number;
324
+ bearer_cache_hit: number;
325
+ bearer_cache_miss: number;
326
+ apikey_cache_hit: number;
327
+ apikey_cache_miss: number;
328
+ };
329
+ };
330
+ time_series: MinuteBucket[];
331
+ }
332
+
333
+ // ─── Sub-clients ──────────────────────────────────────────────────────────────
334
+
335
+ class AdminUsersClient {
336
+ constructor(private readonly http: HttpClient) {}
337
+
338
+ async list(): Promise<AdminUser[]> {
339
+ const res = await this.http.request<{ items: AdminUser[] }>("GET", "/api/v1/admin/users");
340
+ return res.items;
341
+ }
342
+
343
+ async get(id: string): Promise<AdminUser> {
344
+ return this.http.request<AdminUser>("GET", `/api/v1/admin/users/${id}`);
345
+ }
346
+
347
+ async update(id: string, params: UpdateUserParams): Promise<AdminUser> {
348
+ return this.http.request<AdminUser>("PATCH", `/api/v1/admin/users/${id}`, { body: params });
349
+ }
350
+
351
+ async delete(id: string): Promise<void> {
352
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/users/${id}`);
353
+ }
354
+
355
+ async setPassword(id: string, password: string): Promise<void> {
356
+ await this.http.request<{ ok: boolean }>("POST", `/api/v1/admin/users/${id}/set-password`, {
357
+ body: { password },
358
+ });
359
+ }
360
+
361
+ async listSessions(id: string): Promise<AdminSession[]> {
362
+ const res = await this.http.request<{ items: AdminSession[] }>(
363
+ "GET",
364
+ `/api/v1/admin/users/${id}/sessions`,
365
+ );
366
+ return res.items;
367
+ }
368
+
369
+ async listApiKeys(id: string): Promise<AdminApiKey[]> {
370
+ const res = await this.http.request<{ items: AdminApiKey[] }>(
371
+ "GET",
372
+ `/api/v1/admin/users/${id}/api-keys`,
373
+ );
374
+ return res.items;
375
+ }
376
+
377
+ async revokeApiKey(userId: string, keyId: string): Promise<void> {
378
+ await this.http.request<{ ok: boolean }>(
379
+ "DELETE",
380
+ `/api/v1/admin/users/${userId}/api-keys/${keyId}`,
381
+ );
382
+ }
383
+
384
+ async impersonate(id: string): Promise<ImpersonationResult> {
385
+ return this.http.request<ImpersonationResult>("POST", `/api/v1/admin/users/${id}/impersonate`, {
386
+ body: {},
387
+ });
388
+ }
389
+ }
390
+
391
+ class AdminSessionsClient {
392
+ constructor(private readonly http: HttpClient) {}
393
+
394
+ async list(): Promise<AdminSession[]> {
395
+ const res = await this.http.request<{ items: AdminSession[] }>("GET", "/api/v1/admin/sessions");
396
+ return res.items;
397
+ }
398
+
399
+ async revoke(id: string): Promise<void> {
400
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/sessions/${id}`);
401
+ }
402
+
403
+ async purge(): Promise<{ purged: number }> {
404
+ return this.http.request<{ purged: number }>("POST", "/api/v1/admin/sessions/purge", {
405
+ body: {},
406
+ });
407
+ }
408
+ }
409
+
410
+ class AdminCollectionsClient {
411
+ constructor(private readonly http: HttpClient) {}
412
+
413
+ async list(): Promise<CollectionMeta[]> {
414
+ const res = await this.http.request<{ items: CollectionMeta[] }>(
415
+ "GET",
416
+ "/api/v1/admin/collections",
417
+ );
418
+ return res.items;
419
+ }
420
+
421
+ async create(name: string, rules: CollectionRules): Promise<CollectionMeta> {
422
+ return this.http.request<CollectionMeta>("POST", "/api/v1/admin/collections", {
423
+ body: { name, rules },
424
+ });
425
+ }
426
+
427
+ async update(name: string, rules: CollectionRules): Promise<void> {
428
+ await this.http.request<{ ok: boolean }>("PATCH", `/api/v1/admin/collections/${name}`, {
429
+ body: { rules },
430
+ });
431
+ }
432
+
433
+ async setFieldRules(name: string, fieldRules: AdminFieldRule[]): Promise<void> {
434
+ await this.http.request<{ ok: boolean }>("PUT", `/api/v1/admin/collections/${name}/rules`, {
435
+ body: { field_rules: fieldRules },
436
+ });
437
+ }
438
+
439
+ /**
440
+ * Set or clear webhook URL, signing secret, and event filter.
441
+ * Each field follows omit/null/value semantics:
442
+ * - omitted: keep existing
443
+ * - null: clear (all events / no secret)
444
+ * - value: set new
445
+ */
446
+ async setWebhook(
447
+ name: string,
448
+ url: string | null,
449
+ opts?: { secret?: string | null; events?: string[] | null },
450
+ ): Promise<void> {
451
+ const body: Record<string, unknown> = { url };
452
+ if (opts?.secret !== undefined) body.secret = opts.secret;
453
+ if (opts?.events !== undefined) body.events = opts.events;
454
+ await this.http.request<{ ok: boolean; webhook_url: string | null; signing: boolean }>(
455
+ "PUT",
456
+ `/api/v1/admin/collections/${name}/webhook`,
457
+ { body },
458
+ );
459
+ }
460
+
461
+ async drop(name: string): Promise<void> {
462
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/collections/${name}`);
463
+ }
464
+
465
+ async schema(name: string): Promise<ColumnInfo[]> {
466
+ const res = await this.http.request<{ columns: ColumnInfo[] }>(
467
+ "GET",
468
+ `/api/v1/admin/collections/${name}/schema`,
469
+ );
470
+ return res.columns;
471
+ }
472
+
473
+ async dropColumn(name: string, column: string): Promise<void> {
474
+ await this.http.request<{ dropped: boolean }>(
475
+ "DELETE",
476
+ `/api/v1/admin/collections/${name}/schema/${column}`,
477
+ );
478
+ }
479
+
480
+ async renameColumn(name: string, column: string, newName: string): Promise<void> {
481
+ await this.http.request<{ renamed: boolean }>(
482
+ "PATCH",
483
+ `/api/v1/admin/collections/${name}/schema/${column}`,
484
+ { body: { newName } },
485
+ );
486
+ }
487
+
488
+ async stats(name: string): Promise<CollectionStats> {
489
+ return this.http.request<CollectionStats>("GET", `/api/v1/admin/collections/${name}/stats`);
490
+ }
491
+
492
+ /** Full schema introspection — columns, indexes, relations, rules, FTS, webhook, stats. */
493
+ async introspect(name: string): Promise<CollectionIntrospection> {
494
+ return this.http.request<CollectionIntrospection>(
495
+ "GET",
496
+ `/api/v1/admin/collections/${name}/introspect`,
497
+ );
498
+ }
499
+
500
+ // ── Records ─────────────────────────────────────────────────────────────────
501
+
502
+ async listRecords(
503
+ collection: string,
504
+ opts: {
505
+ /** @deprecated Use `after` for keyset pagination. */
506
+ page?: number;
507
+ limit?: number;
508
+ after?: string;
509
+ sort?: string;
510
+ filter?: Record<string, string>;
511
+ includeDeleted?: boolean;
512
+ search?: string;
513
+ } = {},
514
+ ): Promise<AdminListResult<AdminRecord>> {
515
+ const params: Record<string, string> = {
516
+ limit: String(opts.limit ?? 50),
517
+ };
518
+ if (opts.after) params.after = opts.after;
519
+ else if (opts.page !== undefined) params.page = String(opts.page);
520
+ if (opts.sort) params.sort = opts.sort;
521
+ if (opts.includeDeleted) params.include_deleted = "true";
522
+ if (opts.search) params.search = opts.search;
523
+ if (opts.filter) Object.assign(params, opts.filter);
524
+ return this.http.request<AdminListResult<AdminRecord>>(
525
+ "GET",
526
+ `/api/v1/admin/collections/${collection}/records`,
527
+ { query: params },
528
+ );
529
+ }
530
+
531
+ async getRecord(collection: string, id: string): Promise<AdminRecord | null> {
532
+ try {
533
+ return await this.http.request<AdminRecord>(
534
+ "GET",
535
+ `/api/v1/admin/collections/${collection}/records/${id}`,
536
+ );
537
+ } catch {
538
+ return null;
539
+ }
540
+ }
541
+
542
+ async createRecord(collection: string, data: Record<string, unknown>): Promise<AdminRecord> {
543
+ return this.http.request<AdminRecord>(
544
+ "POST",
545
+ `/api/v1/admin/collections/${collection}/records`,
546
+ { body: data },
547
+ );
548
+ }
549
+
550
+ async updateRecord(
551
+ collection: string,
552
+ id: string,
553
+ patch: Record<string, unknown>,
554
+ ): Promise<AdminRecord> {
555
+ return this.http.request<AdminRecord>(
556
+ "PATCH",
557
+ `/api/v1/admin/collections/${collection}/records/${id}`,
558
+ { body: patch },
559
+ );
560
+ }
561
+
562
+ async deleteRecord(collection: string, id: string): Promise<void> {
563
+ await this.http.request<{ ok: boolean }>(
564
+ "DELETE",
565
+ `/api/v1/admin/collections/${collection}/records/${id}`,
566
+ );
567
+ }
568
+
569
+ async restoreRecord(collection: string, id: string): Promise<AdminRecord> {
570
+ return this.http.request<AdminRecord>(
571
+ "POST",
572
+ `/api/v1/admin/collections/${collection}/records/${id}/restore`,
573
+ { body: {} },
574
+ );
575
+ }
576
+
577
+ // ── Indexes ──────────────────────────────────────────────────────────────────
578
+
579
+ async listIndexes(collection: string): Promise<IndexInfo[]> {
580
+ const res = await this.http.request<{ indexes: IndexInfo[] }>(
581
+ "GET",
582
+ `/api/v1/admin/collections/${collection}/indexes`,
583
+ );
584
+ return res.indexes;
585
+ }
586
+
587
+ async createIndex(collection: string, column: string): Promise<IndexInfo> {
588
+ return this.http.request<IndexInfo>("POST", `/api/v1/admin/collections/${collection}/indexes`, {
589
+ body: { column },
590
+ });
591
+ }
592
+
593
+ async dropIndex(collection: string, column: string): Promise<void> {
594
+ await this.http.request<{ dropped: boolean }>(
595
+ "DELETE",
596
+ `/api/v1/admin/collections/${collection}/indexes/${column}`,
597
+ );
598
+ }
599
+
600
+ // ── FTS ──────────────────────────────────────────────────────────────────────
601
+
602
+ async getFtsStatus(collection: string): Promise<{ exists: boolean }> {
603
+ return this.http.request<{ exists: boolean }>(
604
+ "GET",
605
+ `/api/v1/admin/collections/${collection}/fts`,
606
+ );
607
+ }
608
+
609
+ async createFts(collection: string): Promise<{ created: boolean; indexed?: number }> {
610
+ return this.http.request<{ created: boolean; indexed?: number }>(
611
+ "POST",
612
+ `/api/v1/admin/collections/${collection}/fts`,
613
+ { body: {} },
614
+ );
615
+ }
616
+
617
+ async dropFts(collection: string): Promise<void> {
618
+ await this.http.request("DELETE", `/api/v1/admin/collections/${collection}/fts`);
619
+ }
620
+ }
621
+
622
+ class AdminRelationsClient {
623
+ constructor(private readonly http: HttpClient) {}
624
+
625
+ async list(): Promise<Relation[]> {
626
+ const res = await this.http.request<{ items: Relation[] }>("GET", "/api/v1/admin/relations");
627
+ return res.items;
628
+ }
629
+
630
+ async create(params: CreateRelationParams): Promise<Relation> {
631
+ return this.http.request<Relation>("POST", "/api/v1/admin/relations", { body: params });
632
+ }
633
+
634
+ async delete(id: string): Promise<void> {
635
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/relations/${id}`);
636
+ }
637
+ }
638
+
639
+ class AdminStorageClient {
640
+ constructor(private readonly http: HttpClient) {}
641
+
642
+ async listFiles(): Promise<AdminStoredFile[]> {
643
+ const res = await this.http.request<{ items: AdminStoredFile[] }>(
644
+ "GET",
645
+ "/api/v1/admin/storage",
646
+ );
647
+ return res.items;
648
+ }
649
+
650
+ async uploadFile(params: {
651
+ file: File | Blob;
652
+ filename?: string;
653
+ isPublic: boolean;
654
+ collection?: string;
655
+ bucket?: string;
656
+ }): Promise<AdminStoredFile> {
657
+ const form = new FormData();
658
+ form.append("file", params.file, params.filename);
659
+ form.append("is_public", params.isPublic ? "true" : "false");
660
+ if (params.collection) form.append("collection", params.collection);
661
+ if (params.bucket) form.append("bucket", params.bucket);
662
+ return this.http.request<AdminStoredFile>("POST", "/api/v1/admin/storage/upload", {
663
+ formData: form,
664
+ });
665
+ }
666
+
667
+ async deleteFile(id: string): Promise<void> {
668
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/storage/${id}`);
669
+ }
670
+
671
+ async listBuckets(): Promise<StorageBucket[]> {
672
+ const res = await this.http.request<{ items: StorageBucket[] }>("GET", "/api/v1/admin/buckets");
673
+ return res.items;
674
+ }
675
+
676
+ async createBucket(params: {
677
+ name: string;
678
+ access_read: BucketAccessRule;
679
+ access_write: BucketAccessRule;
680
+ allowed_mimes?: string[] | null;
681
+ max_size_bytes?: number | null;
682
+ cdn_url?: string | null;
683
+ }): Promise<StorageBucket> {
684
+ return this.http.request<StorageBucket>("POST", "/api/v1/admin/buckets", { body: params });
685
+ }
686
+
687
+ async updateBucket(
688
+ name: string,
689
+ updates: Partial<Omit<StorageBucket, "name" | "created_at">>,
690
+ ): Promise<StorageBucket> {
691
+ return this.http.request<StorageBucket>("PATCH", `/api/v1/admin/buckets/${name}`, {
692
+ body: updates,
693
+ });
694
+ }
695
+
696
+ async deleteBucket(name: string): Promise<void> {
697
+ await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/admin/buckets/${name}`);
698
+ }
699
+ }
700
+
701
+ class AdminSettingsClient {
702
+ constructor(private readonly http: HttpClient) {}
703
+
704
+ async get(): Promise<ServerSettings> {
705
+ return this.http.request<ServerSettings>("GET", "/api/v1/admin/settings");
706
+ }
707
+
708
+ async update(updates: Partial<ServerSettings>): Promise<ServerSettings> {
709
+ return this.http.request<ServerSettings>("PATCH", "/api/v1/admin/settings", { body: updates });
710
+ }
711
+
712
+ async testEmail(to: string): Promise<{ ok: boolean; error?: string }> {
713
+ return this.http.request<{ ok: boolean; error?: string }>(
714
+ "POST",
715
+ "/api/v1/admin/settings/test-email",
716
+ { body: { to } },
717
+ );
718
+ }
719
+
720
+ async restart(): Promise<{ ok: boolean }> {
721
+ return this.http.request<{ ok: boolean }>("POST", "/api/v1/admin/restart", { body: {} });
722
+ }
723
+
724
+ async getEmailTemplates(): Promise<Record<TemplateName, EmailTemplate>> {
725
+ return this.http.request<Record<TemplateName, EmailTemplate>>(
726
+ "GET",
727
+ "/api/v1/admin/email-templates",
728
+ );
729
+ }
730
+
731
+ async updateEmailTemplate(
732
+ name: TemplateName,
733
+ tpl: Omit<EmailTemplate, "is_default">,
734
+ ): Promise<{ ok: boolean; template: EmailTemplate }> {
735
+ return this.http.request<{ ok: boolean; template: EmailTemplate }>(
736
+ "PUT",
737
+ `/api/v1/admin/email-templates/${name}`,
738
+ { body: tpl },
739
+ );
740
+ }
741
+
742
+ async resetEmailTemplate(name: TemplateName): Promise<{ ok: boolean; template: EmailTemplate }> {
743
+ return this.http.request<{ ok: boolean; template: EmailTemplate }>(
744
+ "DELETE",
745
+ `/api/v1/admin/email-templates/${name}`,
746
+ );
747
+ }
748
+
749
+ async getConfig(): Promise<ConfigResponse> {
750
+ return this.http.request<ConfigResponse>("GET", "/api/v1/admin/config");
751
+ }
752
+
753
+ async updateConfig(
754
+ updates: Record<string, string | null>,
755
+ ): Promise<{ ok: boolean; overrides: Record<string, string | null> }> {
756
+ return this.http.request<{ ok: boolean; overrides: Record<string, string | null> }>(
757
+ "PATCH",
758
+ "/api/v1/admin/config",
759
+ { body: updates },
760
+ );
761
+ }
762
+
763
+ async getAudit(limit = 50, offset = 0): Promise<{ items: SettingsAuditEntry[]; total: number }> {
764
+ return this.http.request<{ items: SettingsAuditEntry[]; total: number }>(
765
+ "GET",
766
+ `/api/v1/admin/settings/audit?limit=${limit}&offset=${offset}`,
767
+ );
768
+ }
769
+
770
+ // Returns raw Response so the caller can trigger a browser file download.
771
+ async exportSettings(): Promise<Response> {
772
+ return this.http.requestRaw("GET", "/api/v1/admin/settings/export");
773
+ }
774
+
775
+ async importSettings(
776
+ settings: Record<string, unknown>,
777
+ ): Promise<{ ok: boolean; imported: number; settings: ServerSettings }> {
778
+ return this.http.request<{ ok: boolean; imported: number; settings: ServerSettings }>(
779
+ "POST",
780
+ "/api/v1/admin/settings/import",
781
+ { body: settings },
782
+ );
783
+ }
784
+ }
785
+
786
+ class AdminBackupsClient {
787
+ constructor(private readonly http: HttpClient) {}
788
+
789
+ async list(): Promise<BackupFile[]> {
790
+ return this.http.request<BackupFile[]>("GET", "/api/v1/admin/backups");
791
+ }
792
+
793
+ async create(): Promise<BackupFile> {
794
+ return this.http.request<BackupFile>("POST", "/api/v1/admin/backups", { body: {} });
795
+ }
796
+
797
+ // Returns raw Response so the caller can trigger a browser file download.
798
+ async download(filename: string): Promise<Response> {
799
+ return this.http.requestRaw("GET", `/api/v1/admin/backups/${encodeURIComponent(filename)}`);
800
+ }
801
+
802
+ // Returns a full database snapshot as a raw Response (for the one-click download).
803
+ async snapshot(): Promise<Response> {
804
+ return this.http.requestRaw("POST", "/api/v1/admin/backup");
805
+ }
806
+
807
+ async delete(filename: string): Promise<void> {
808
+ await this.http.request("DELETE", `/api/v1/admin/backups/${encodeURIComponent(filename)}`);
809
+ }
810
+
811
+ async restore(filename: string): Promise<void> {
812
+ await this.http.request(
813
+ "POST",
814
+ `/api/v1/admin/backups/${encodeURIComponent(filename)}/restore`,
815
+ { body: {} },
816
+ );
817
+ }
818
+
819
+ async upload(file: File | Blob, filename?: string): Promise<BackupFile> {
820
+ const form = new FormData();
821
+ form.append("file", file, filename);
822
+ return this.http.request<BackupFile>("POST", "/api/v1/admin/backups/upload", {
823
+ formData: form,
824
+ });
825
+ }
826
+ }
827
+
828
+ class AdminSystemClient {
829
+ constructor(private readonly http: HttpClient) {}
830
+
831
+ async health(): Promise<HealthResponse> {
832
+ return this.http.request<HealthResponse>("GET", "/api/v1/admin/health");
833
+ }
834
+
835
+ async stats(): Promise<StatsResponse> {
836
+ return this.http.request<StatsResponse>("GET", "/api/v1/admin/stats");
837
+ }
838
+ }
839
+
840
+ export interface WebhookLogRow {
841
+ id: number;
842
+ ts: number;
843
+ collection: string;
844
+ event: string;
845
+ url: string;
846
+ attempt: number;
847
+ success: number;
848
+ response_status: number | null;
849
+ error: string | null;
850
+ duration_ms: number;
851
+ }
852
+
853
+ class AdminLogsClient {
854
+ constructor(private readonly http: HttpClient) {}
855
+
856
+ async webhook(opts?: { limit?: number }): Promise<{ rows: WebhookLogRow[]; total: number }> {
857
+ const params: Record<string, string> = {};
858
+ if (opts?.limit) params.limit = String(opts.limit);
859
+ return this.http.request<{ rows: WebhookLogRow[]; total: number }>(
860
+ "GET",
861
+ "/api/v1/admin/logs/webhook",
862
+ { query: params },
863
+ );
864
+ }
865
+
866
+ async clearWebhook(): Promise<void> {
867
+ await this.http.request("DELETE", "/api/v1/admin/logs/webhook");
868
+ }
869
+ }
870
+
871
+ export interface MigrationStatus {
872
+ applied: Array<{ name: string; applied_at: number }>;
873
+ pending: string[];
874
+ auto_migrate: boolean;
875
+ db_provider: "sqlite" | "postgres";
876
+ }
877
+
878
+ class AdminMigrationsClient {
879
+ constructor(private readonly http: HttpClient) {}
880
+
881
+ async status(): Promise<MigrationStatus> {
882
+ return this.http.request<MigrationStatus>("GET", "/api/v1/admin/migrations");
883
+ }
884
+ }
885
+
886
+ // ─── Main AdminClient ─────────────────────────────────────────────────────────
887
+
888
+ export class AdminClient {
889
+ readonly users: AdminUsersClient;
890
+ readonly sessions: AdminSessionsClient;
891
+ readonly collections: AdminCollectionsClient;
892
+ readonly relations: AdminRelationsClient;
893
+ readonly storage: AdminStorageClient;
894
+ readonly settings: AdminSettingsClient;
895
+ readonly backups: AdminBackupsClient;
896
+ readonly migrations: AdminMigrationsClient;
897
+ readonly logs: AdminLogsClient;
898
+ readonly system: AdminSystemClient;
899
+
900
+ constructor(http: HttpClient) {
901
+ this.users = new AdminUsersClient(http);
902
+ this.sessions = new AdminSessionsClient(http);
903
+ this.collections = new AdminCollectionsClient(http);
904
+ this.relations = new AdminRelationsClient(http);
905
+ this.storage = new AdminStorageClient(http);
906
+ this.settings = new AdminSettingsClient(http);
907
+ this.backups = new AdminBackupsClient(http);
908
+ this.migrations = new AdminMigrationsClient(http);
909
+ this.logs = new AdminLogsClient(http);
910
+ this.system = new AdminSystemClient(http);
911
+ }
912
+ }