primitive-admin 1.1.0-alpha.3 → 1.1.0-alpha.31

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.
Files changed (99) hide show
  1. package/README.md +168 -21
  2. package/assets/skill/skills/primitive-platform/SKILL.md +226 -0
  3. package/dist/bin/primitive.js +198 -15
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +107 -0
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +464 -55
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +27 -13
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/auth.js +165 -5
  12. package/dist/src/commands/auth.js.map +1 -1
  13. package/dist/src/commands/blob-buckets.js +354 -0
  14. package/dist/src/commands/blob-buckets.js.map +1 -0
  15. package/dist/src/commands/catalog.js +17 -17
  16. package/dist/src/commands/catalog.js.map +1 -1
  17. package/dist/src/commands/collection-type-configs.js +178 -0
  18. package/dist/src/commands/collection-type-configs.js.map +1 -0
  19. package/dist/src/commands/collections.js +526 -0
  20. package/dist/src/commands/collections.js.map +1 -0
  21. package/dist/src/commands/comparisons.js +6 -6
  22. package/dist/src/commands/comparisons.js.map +1 -1
  23. package/dist/src/commands/cron-triggers.js +364 -0
  24. package/dist/src/commands/cron-triggers.js.map +1 -0
  25. package/dist/src/commands/database-types.js +462 -0
  26. package/dist/src/commands/database-types.js.map +1 -0
  27. package/dist/src/commands/databases.js +1067 -58
  28. package/dist/src/commands/databases.js.map +1 -1
  29. package/dist/src/commands/documents.js +510 -2
  30. package/dist/src/commands/documents.js.map +1 -1
  31. package/dist/src/commands/email-templates.js +281 -0
  32. package/dist/src/commands/email-templates.js.map +1 -0
  33. package/dist/src/commands/env.js +260 -0
  34. package/dist/src/commands/env.js.map +1 -0
  35. package/dist/src/commands/group-type-configs.js +189 -0
  36. package/dist/src/commands/group-type-configs.js.map +1 -0
  37. package/dist/src/commands/groups.js +28 -25
  38. package/dist/src/commands/groups.js.map +1 -1
  39. package/dist/src/commands/init.js +719 -188
  40. package/dist/src/commands/init.js.map +1 -1
  41. package/dist/src/commands/integrations.js +504 -33
  42. package/dist/src/commands/integrations.js.map +1 -1
  43. package/dist/src/commands/prompts.js +17 -16
  44. package/dist/src/commands/prompts.js.map +1 -1
  45. package/dist/src/commands/rule-sets.js +366 -0
  46. package/dist/src/commands/rule-sets.js.map +1 -0
  47. package/dist/src/commands/secrets.js +108 -0
  48. package/dist/src/commands/secrets.js.map +1 -0
  49. package/dist/src/commands/skill.js +29 -0
  50. package/dist/src/commands/skill.js.map +1 -0
  51. package/dist/src/commands/sync.js +2363 -182
  52. package/dist/src/commands/sync.js.map +1 -1
  53. package/dist/src/commands/tokens.js +9 -9
  54. package/dist/src/commands/tokens.js.map +1 -1
  55. package/dist/src/commands/users.js +426 -4
  56. package/dist/src/commands/users.js.map +1 -1
  57. package/dist/src/commands/webhooks.js +386 -0
  58. package/dist/src/commands/webhooks.js.map +1 -0
  59. package/dist/src/commands/workflows.js +614 -64
  60. package/dist/src/commands/workflows.js.map +1 -1
  61. package/dist/src/lib/api-client.js +942 -83
  62. package/dist/src/lib/api-client.js.map +1 -1
  63. package/dist/src/lib/config.js +51 -53
  64. package/dist/src/lib/config.js.map +1 -1
  65. package/dist/src/lib/constants.js +3 -0
  66. package/dist/src/lib/constants.js.map +1 -0
  67. package/dist/src/lib/credentials-store.js +307 -0
  68. package/dist/src/lib/credentials-store.js.map +1 -0
  69. package/dist/src/lib/env-resolver.js +121 -0
  70. package/dist/src/lib/env-resolver.js.map +1 -0
  71. package/dist/src/lib/init-config.js +87 -0
  72. package/dist/src/lib/init-config.js.map +1 -0
  73. package/dist/src/lib/migration-nag.js +163 -0
  74. package/dist/src/lib/migration-nag.js.map +1 -0
  75. package/dist/src/lib/output.js +58 -6
  76. package/dist/src/lib/output.js.map +1 -1
  77. package/dist/src/lib/paginate.js +42 -0
  78. package/dist/src/lib/paginate.js.map +1 -0
  79. package/dist/src/lib/project-config.js +209 -0
  80. package/dist/src/lib/project-config.js.map +1 -0
  81. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  82. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  83. package/dist/src/lib/skill-installer.js +135 -0
  84. package/dist/src/lib/skill-installer.js.map +1 -0
  85. package/dist/src/lib/sync-paths.js +102 -0
  86. package/dist/src/lib/sync-paths.js.map +1 -0
  87. package/dist/src/lib/template.js +279 -18
  88. package/dist/src/lib/template.js.map +1 -1
  89. package/dist/src/lib/toml-database-config.js +384 -0
  90. package/dist/src/lib/toml-database-config.js.map +1 -0
  91. package/dist/src/lib/toml-params-validator.js +183 -0
  92. package/dist/src/lib/toml-params-validator.js.map +1 -0
  93. package/dist/src/lib/version-check.js +172 -0
  94. package/dist/src/lib/version-check.js.map +1 -0
  95. package/dist/src/lib/workflow-fragments.js +121 -0
  96. package/dist/src/lib/workflow-fragments.js.map +1 -0
  97. package/dist/src/lib/workflow-toml-validator.js +328 -0
  98. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  99. package/package.json +7 -4
@@ -1,25 +1,167 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
2
  import { fetchWithTLS } from "./fetch.js";
3
+ import { paginateAll } from "./paginate.js";
4
+ import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
3
5
  export class ApiError extends Error {
4
6
  statusCode;
5
7
  code;
6
- constructor(message, statusCode, code) {
8
+ /**
9
+ * Structured `details` payload from `corsErrorResponse`.
10
+ *
11
+ * The server's error envelope can emit `details` as either:
12
+ * - an array of validation issues (e.g. `[{ path, message }, ...]`) —
13
+ * this is what most legacy endpoints produce, and what sync.ts walks
14
+ * via `Array.isArray(err.details)` / `for (const detail of ...)`.
15
+ * - a record of structured offender fields (e.g. `{ refs, operations,
16
+ * opCount, line, column, ... }`) — emitted by the issue #666 schema
17
+ * gate and consumed by the typed exception subclasses below.
18
+ *
19
+ * Callers must narrow before use: `Array.isArray(err.details)` for the
20
+ * legacy shape, otherwise treat as `Record<string, any>`.
21
+ */
22
+ details;
23
+ constructor(message, statusCode, code, details) {
7
24
  super(message);
8
25
  this.statusCode = statusCode;
9
26
  this.code = code;
10
27
  this.name = "ApiError";
28
+ if (details !== undefined) {
29
+ this.details = details;
30
+ }
11
31
  }
12
32
  }
13
33
  export class ConflictError extends ApiError {
14
34
  serverModifiedAt;
15
35
  expectedModifiedAt;
16
- constructor(message, serverModifiedAt, expectedModifiedAt) {
17
- super(message, 409, "CONFLICT");
36
+ constructor(message, serverModifiedAt, expectedModifiedAt, details) {
37
+ super(message, 409, "CONFLICT", details);
18
38
  this.serverModifiedAt = serverModifiedAt;
19
39
  this.expectedModifiedAt = expectedModifiedAt;
20
40
  this.name = "ConflictError";
21
41
  }
22
42
  }
43
+ /**
44
+ * Extract a human-readable error message + structured fields from a non-OK
45
+ * HTTP response body. Single source of truth used by every error-handler call
46
+ * site in this file (see issue #684).
47
+ */
48
+ export function parseErrorResponse(response, text, path) {
49
+ // Empty body → fall back to status code.
50
+ if (!text) {
51
+ return { message: `HTTP ${response.status}` };
52
+ }
53
+ let errorData;
54
+ try {
55
+ errorData = JSON.parse(text);
56
+ }
57
+ catch {
58
+ // Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
59
+ // 404 page from hitting the wrong path) so we don't regress the helpful
60
+ // "API endpoint not found" message at api-client.ts:343.
61
+ if (text.includes("<!DOCTYPE")) {
62
+ const where = path ? `: ${path}` : "";
63
+ return {
64
+ message: `API endpoint not found${where}. Make sure the server is running.`,
65
+ htmlNotFound: true,
66
+ };
67
+ }
68
+ // Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
69
+ // raw text so the operator at least sees what the server returned.
70
+ return { message: text };
71
+ }
72
+ // Server's standard envelope uses `error`; ConflictError + integrations
73
+ // proxy use `message`. Prefer `error` (more common), fall back to `message`.
74
+ const message = (typeof errorData?.error === "string" && errorData.error) ||
75
+ (typeof errorData?.message === "string" && errorData.message) ||
76
+ `HTTP ${response.status}`;
77
+ // Per issue #666 addendum A1, `code` may be at the top level or nested
78
+ // under `details.code` when the server's bespoke envelope didn't flatten.
79
+ const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
80
+ (typeof errorData?.details?.code === "string"
81
+ ? errorData.details.code
82
+ : undefined);
83
+ // Accept either an array (legacy) or a plain object (#666 schema gate).
84
+ const details = Array.isArray(errorData?.details)
85
+ ? errorData.details
86
+ : errorData?.details && typeof errorData.details === "object"
87
+ ? errorData.details
88
+ : undefined;
89
+ return { message, code, details, raw: errorData };
90
+ }
91
+ /**
92
+ * Typed exception classes for the database-schema feature (issue #666).
93
+ * Each maps 1:1 to a server `code` value emitted from the op-edit or
94
+ * schema-edit gate. They all extend ApiError so existing catch-all paths
95
+ * continue to work; specialized catch blocks can branch on `instanceof`.
96
+ *
97
+ * Per round-2 addendum A1, `details` is always preserved so callers can
98
+ * extract structured offender lists (refs[], operations[], etc.).
99
+ */
100
+ export class SchemaRequiredError extends ApiError {
101
+ constructor(message, details) {
102
+ super(message, 422, "SCHEMA_REQUIRED", details);
103
+ this.name = "SchemaRequiredError";
104
+ }
105
+ }
106
+ function detailsRecord(details) {
107
+ return details && !Array.isArray(details) && typeof details === "object"
108
+ ? details
109
+ : undefined;
110
+ }
111
+ export class OperationRefError extends ApiError {
112
+ constructor(message, details) {
113
+ super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
114
+ this.name = "OperationRefError";
115
+ }
116
+ get refs() {
117
+ const d = detailsRecord(this.details);
118
+ return Array.isArray(d?.refs) ? d.refs : [];
119
+ }
120
+ }
121
+ export class SchemaBreaksOpsError extends ApiError {
122
+ constructor(message, details) {
123
+ super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
124
+ this.name = "SchemaBreaksOpsError";
125
+ }
126
+ get operations() {
127
+ const d = detailsRecord(this.details);
128
+ return Array.isArray(d?.operations) ? d.operations : [];
129
+ }
130
+ }
131
+ export class SchemaHasUncheckableOpsError extends ApiError {
132
+ constructor(message, details) {
133
+ super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
134
+ this.name = "SchemaHasUncheckableOpsError";
135
+ }
136
+ get operations() {
137
+ const d = detailsRecord(this.details);
138
+ return Array.isArray(d?.operations) ? d.operations : [];
139
+ }
140
+ }
141
+ export class TomlParseError extends ApiError {
142
+ constructor(message, details) {
143
+ super(message, 400, "TOML_PARSE_ERROR", details);
144
+ this.name = "TomlParseError";
145
+ }
146
+ get line() {
147
+ const d = detailsRecord(this.details);
148
+ return typeof d?.line === "number" ? d.line : undefined;
149
+ }
150
+ get column() {
151
+ const d = detailsRecord(this.details);
152
+ return typeof d?.column === "number" ? d.column : undefined;
153
+ }
154
+ }
155
+ export class OpsExistError extends ApiError {
156
+ constructor(message, details) {
157
+ super(message, 409, "OPS_EXIST", details);
158
+ this.name = "OpsExistError";
159
+ }
160
+ get opCount() {
161
+ const d = detailsRecord(this.details);
162
+ return typeof d?.opCount === "number" ? d.opCount : 0;
163
+ }
164
+ }
23
165
  export class ApiClient {
24
166
  credentials = null;
25
167
  constructor() {
@@ -39,39 +181,24 @@ export class ApiClient {
39
181
  return this.credentials;
40
182
  }
41
183
  async refreshToken() {
42
- if (!this.credentials?.refreshToken) {
43
- throw new ApiError("No refresh token available. Please login again.", 401);
184
+ if (!this.credentials) {
185
+ throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
44
186
  }
45
- const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
46
187
  try {
47
- const headers = {
48
- "Content-Type": "application/json",
49
- };
50
- if (this.credentials.globalAdminAppId) {
51
- headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
52
- }
53
- const response = await fetchWithTLS(url, {
54
- method: "POST",
55
- headers,
56
- body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
57
- });
58
- if (!response.ok) {
59
- throw new ApiError("Token refresh failed. Please login again.", 401);
60
- }
61
- const data = await response.json();
62
- // Update credentials with new tokens
63
- this.credentials = {
64
- ...this.credentials,
65
- accessToken: data.accessToken || data.token,
66
- refreshToken: data.refreshToken || this.credentials.refreshToken,
67
- expiresAt: data.expiresAt,
68
- };
188
+ const updated = await refreshAdminCredentials(this.credentials);
189
+ this.credentials = updated;
69
190
  saveCredentials(this.credentials);
70
191
  }
71
- catch (error) {
72
- if (error instanceof ApiError)
73
- throw error;
74
- throw new ApiError("Token refresh failed. Please login again.", 401);
192
+ catch (err) {
193
+ if (err instanceof RefreshError) {
194
+ // Preserve historical behavior: ApiClient surfaces refresh failures
195
+ // as 401s regardless of whether the underlying cause was a network
196
+ // error or a server-side rejection. Callers that need a finer
197
+ // distinction (e.g. `primitive token`) consume RefreshError directly
198
+ // from refresh-admin-credentials.ts instead of going through here.
199
+ throw new ApiError("Token refresh failed. Please login again.", 401);
200
+ }
201
+ throw err;
75
202
  }
76
203
  }
77
204
  async request(path, options = {}) {
@@ -92,20 +219,42 @@ export class ApiClient {
92
219
  });
93
220
  const text = await response.text();
94
221
  if (!response.ok) {
95
- let errorData;
96
- try {
97
- errorData = JSON.parse(text);
222
+ const parsed = parseErrorResponse(response, text, path);
223
+ // Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
224
+ if (parsed.htmlNotFound) {
225
+ throw new ApiError(parsed.message, 404);
98
226
  }
99
- catch {
100
- if (text.includes("<!DOCTYPE")) {
101
- throw new ApiError(`API endpoint not found: ${path}. Make sure the server is running.`, 404);
102
- }
103
- errorData = { message: text || `HTTP ${response.status}` };
227
+ // Narrow details to the record shape for the issue #666 typed-exception
228
+ // dispatch below. Conflict metadata may live under `details.*` (canonical
229
+ // location per A1) or on the top-level envelope (legacy).
230
+ const detailsRecord = parsed.details && !Array.isArray(parsed.details)
231
+ ? parsed.details
232
+ : undefined;
233
+ const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
234
+ const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
235
+ // Typed exceptions for the schema-feature (issue #666).
236
+ if (response.status === 409 && parsed.code === "CONFLICT") {
237
+ throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
238
+ }
239
+ if (response.status === 409 && parsed.code === "OPS_EXIST") {
240
+ throw new OpsExistError(parsed.message, detailsRecord);
241
+ }
242
+ if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
243
+ throw new TomlParseError(parsed.message, detailsRecord);
104
244
  }
105
- if (response.status === 409 && errorData?.code === "CONFLICT") {
106
- throw new ConflictError(errorData.message || "Resource conflict", errorData.serverModifiedAt, errorData.expectedModifiedAt);
245
+ if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
246
+ throw new SchemaRequiredError(parsed.message, detailsRecord);
107
247
  }
108
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
248
+ if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
249
+ throw new OperationRefError(parsed.message, detailsRecord);
250
+ }
251
+ if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
252
+ throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
253
+ }
254
+ if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
255
+ throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
256
+ }
257
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
109
258
  }
110
259
  return text ? JSON.parse(text) : null;
111
260
  }
@@ -155,7 +304,18 @@ export class ApiClient {
155
304
  // APPS
156
305
  // ============================================
157
306
  async listApps() {
158
- return this.get("/admin/api/admins/me/apps");
307
+ // The server paginates cursor-style (default pageSize=25, cap=100). Historically
308
+ // this method issued a single un-paginated GET and silently truncated to the
309
+ // first page, which broke `primitive use <app-id>` and `primitive apps list`
310
+ // for admins with more than 25 apps (see #436). Iterate every page.
311
+ const apps = await paginateAll(async (cursor) => {
312
+ const qs = new URLSearchParams({ limit: "100" });
313
+ if (cursor)
314
+ qs.set("cursor", cursor);
315
+ const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
316
+ return { items: Array.isArray(resp.apps) ? resp.apps : [], nextCursor: resp.nextCursor };
317
+ });
318
+ return { apps };
159
319
  }
160
320
  async createApp(data) {
161
321
  return this.post("/admin/api/apps", data);
@@ -181,8 +341,32 @@ export class ApiClient {
181
341
  async addUserByEmail(appId, data) {
182
342
  return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
183
343
  }
184
- async listUsers(appId) {
185
- return this.get(`/app/${appId}/api/users`);
344
+ async mintTestJwt(appId, userId, role) {
345
+ return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
346
+ }
347
+ async rebuildUserSearchText(appId) {
348
+ return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
349
+ }
350
+ async listUsers(appId, params) {
351
+ const query = new URLSearchParams();
352
+ if (params?.name)
353
+ query.set("name", params.name);
354
+ if (params?.email)
355
+ query.set("email", params.email);
356
+ if (params?.userId)
357
+ query.set("userId", params.userId);
358
+ if (params?.limit)
359
+ query.set("limit", String(params.limit));
360
+ if (params?.cursor)
361
+ query.set("cursor", params.cursor);
362
+ const path = query.toString()
363
+ ? `/app/${appId}/api/users?${query.toString()}`
364
+ : `/app/${appId}/api/users`;
365
+ const result = await this.get(path);
366
+ return {
367
+ items: result?.items ?? (Array.isArray(result) ? result : []),
368
+ nextCursor: result?.nextCursor ?? null,
369
+ };
186
370
  }
187
371
  async removeUser(appId, userId) {
188
372
  return this.delete(`/app/${appId}/api/users/${userId}`);
@@ -196,14 +380,30 @@ export class ApiClient {
196
380
  async transferAdminOwnership(appId, adminId) {
197
381
  return this.post(`/admin/api/apps/${appId}/admins/transfer-ownership`, { adminId });
198
382
  }
383
+ // App-level console admin management
384
+ async listAppAdmins(appId) {
385
+ return this.get(`/admin/api/apps/${appId}/admins`);
386
+ }
387
+ async addAppAdmin(appId, data) {
388
+ return this.post(`/admin/api/apps/${appId}/admins`, data);
389
+ }
390
+ async removeAppAdmin(appId, adminId) {
391
+ return this.delete(`/admin/api/apps/${appId}/admins/${adminId}`);
392
+ }
393
+ async listAppAdminInvitations(appId) {
394
+ return this.get(`/admin/api/apps/${appId}/admin-invitations`);
395
+ }
396
+ async deleteAppAdminInvitation(appId, invitationId) {
397
+ return this.delete(`/admin/api/apps/${appId}/admin-invitations/${invitationId}`);
398
+ }
199
399
  async transferDocumentOwnership(appId, documentId, newOwnerId) {
200
400
  return this.post(`/app/${appId}/api/documents/${documentId}/permissions/transfer`, { newOwnerId });
201
401
  }
202
402
  // ============================================
203
403
  // INVITATIONS
204
404
  // ============================================
205
- async listInvitations(appId) {
206
- return this.get(`/app/${appId}/api/invitations`);
405
+ async listInvitations(appId, params) {
406
+ return this.get(`/app/${appId}/api/invitations`, params);
207
407
  }
208
408
  async createInvitation(appId, data) {
209
409
  return this.post(`/app/${appId}/api/invitations`, data);
@@ -228,6 +428,19 @@ export class ApiClient {
228
428
  return this.delete(`/app/${appId}/api/waitlist/${waitlistId}`);
229
429
  }
230
430
  // ============================================
431
+ // DEFERRED GRANTS
432
+ // ============================================
433
+ async listDeferredGrants(appId, params) {
434
+ const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
435
+ return {
436
+ grants: result?.grants ?? [],
437
+ nextCursor: result?.nextCursor ?? null,
438
+ };
439
+ }
440
+ async revokeDeferredGrant(appId, deferredId, type) {
441
+ return this.delete(`/app/${appId}/api/deferred-grants/${deferredId}?type=${type}`);
442
+ }
443
+ // ============================================
231
444
  // INTEGRATIONS
232
445
  // ============================================
233
446
  async listIntegrations(appId, params) {
@@ -260,6 +473,10 @@ export class ApiClient {
260
473
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
261
474
  return result?.items ?? [];
262
475
  }
476
+ async listWorkflowRunIntegrationLogs(appId, runId, params) {
477
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
478
+ return result?.items ?? [];
479
+ }
263
480
  async listIntegrationSecrets(appId, integrationId, params) {
264
481
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
265
482
  return result?.items ?? [];
@@ -274,6 +491,86 @@ export class ApiClient {
274
491
  });
275
492
  }
276
493
  // ============================================
494
+ // APP SECRETS
495
+ // ============================================
496
+ async listAppSecrets(appId) {
497
+ const result = await this.get(`/admin/api/apps/${appId}/secrets`);
498
+ return result?.items ?? [];
499
+ }
500
+ async createAppSecret(appId, payload) {
501
+ return this.post(`/admin/api/apps/${appId}/secrets`, payload);
502
+ }
503
+ async updateAppSecret(appId, secretId, payload) {
504
+ return this.put(`/admin/api/apps/${appId}/secrets/${secretId}`, payload);
505
+ }
506
+ async upsertAppSecret(appId, key, payload) {
507
+ return this.put(`/admin/api/apps/${appId}/secrets/by-key/${key}`, payload);
508
+ }
509
+ async deleteAppSecret(appId, secretId) {
510
+ return this.delete(`/admin/api/apps/${appId}/secrets/${secretId}`);
511
+ }
512
+ // ============================================
513
+ // WEBHOOKS
514
+ // ============================================
515
+ async listWebhooks(appId, params) {
516
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
517
+ return {
518
+ items: result?.items ?? [],
519
+ nextCursor: result?.nextCursor ?? null,
520
+ };
521
+ }
522
+ async getWebhook(appId, webhookId) {
523
+ return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
524
+ }
525
+ async createWebhook(appId, payload) {
526
+ return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
527
+ }
528
+ async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
529
+ const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
530
+ return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
531
+ }
532
+ async deleteWebhook(appId, webhookId) {
533
+ return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
534
+ }
535
+ async rotateWebhookSecret(appId, webhookId, payload) {
536
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
537
+ }
538
+ async listWebhookEvents(appId, webhookId, params) {
539
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
540
+ return result;
541
+ }
542
+ async testWebhook(appId, webhookId, payload) {
543
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
544
+ }
545
+ // ============================================
546
+ // CRON TRIGGERS
547
+ // ============================================
548
+ async listCronTriggers(appId) {
549
+ const result = await this.get(`/app/${appId}/api/cron-triggers`);
550
+ return { items: result?.items ?? [] };
551
+ }
552
+ async getCronTrigger(appId, triggerId) {
553
+ return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
554
+ }
555
+ async createCronTrigger(appId, payload) {
556
+ return this.post(`/app/${appId}/api/cron-triggers`, payload);
557
+ }
558
+ async updateCronTrigger(appId, triggerId, payload) {
559
+ return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
560
+ }
561
+ async deleteCronTrigger(appId, triggerId) {
562
+ return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}`);
563
+ }
564
+ async pauseCronTrigger(appId, triggerId) {
565
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/pause`, {});
566
+ }
567
+ async resumeCronTrigger(appId, triggerId) {
568
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/resume`, {});
569
+ }
570
+ async testCronTrigger(appId, triggerId) {
571
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
572
+ }
573
+ // ============================================
277
574
  // PROMPTS
278
575
  // ============================================
279
576
  async listPrompts(appId, params) {
@@ -415,14 +712,8 @@ export class ApiClient {
415
712
  });
416
713
  const text = await response.text();
417
714
  if (!response.ok) {
418
- let errorData;
419
- try {
420
- errorData = JSON.parse(text);
421
- }
422
- catch {
423
- errorData = { message: text || `HTTP ${response.status}` };
424
- }
425
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
715
+ const parsed = parseErrorResponse(response, text, url);
716
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
426
717
  }
427
718
  return text ? JSON.parse(text) : null;
428
719
  }
@@ -442,14 +733,8 @@ export class ApiClient {
442
733
  });
443
734
  if (!response.ok) {
444
735
  const text = await response.text();
445
- let errorData;
446
- try {
447
- errorData = JSON.parse(text);
448
- }
449
- catch {
450
- errorData = { message: text || `HTTP ${response.status}` };
451
- }
452
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
736
+ const parsed = parseErrorResponse(response, text, url);
737
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
453
738
  }
454
739
  const arrayBuffer = await response.arrayBuffer();
455
740
  return {
@@ -519,6 +804,16 @@ export class ApiClient {
519
804
  async getWorkflowRunStatus(appId, workflowId, runId) {
520
805
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
521
806
  }
807
+ async getWorkflowStepRuns(appId, workflowId, runId) {
808
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
809
+ return { items: result?.items ?? [] };
810
+ }
811
+ async getWorkflowAnalytics(appId, params) {
812
+ return this.get(`/app/${appId}/api/analytics/workflows/overview`, params);
813
+ }
814
+ async getTopWorkflows(appId, params) {
815
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
816
+ }
522
817
  // ============================================
523
818
  // WORKFLOW CONFIGURATIONS
524
819
  // ============================================
@@ -547,21 +842,54 @@ export class ApiClient {
547
842
  // ============================================
548
843
  // ANALYTICS
549
844
  // ============================================
550
- async getAnalyticsOverview(appId, params) {
551
- return this.get(`/app/${appId}/api/analytics/overview`, params);
552
- }
553
845
  async getAnalyticsTopUsers(appId, params) {
554
846
  return this.get(`/app/${appId}/api/analytics/users/top`, params);
555
847
  }
556
- async getAnalyticsUserTimeline(appId, userUlid, params) {
557
- return this.get(`/app/${appId}/api/analytics/users/${userUlid}/timeline`, params);
558
- }
559
- async getAnalyticsUserEvents(appId, userUlid, params) {
560
- return this.get(`/app/${appId}/api/analytics/users/${userUlid}/events`, params);
561
- }
562
848
  async getAnalyticsIntegrationMetrics(appId, params) {
563
849
  return this.get(`/app/${appId}/api/analytics/integrations`, params);
564
850
  }
851
+ async getAnalyticsOverviewDau(appId) {
852
+ return this.get(`/app/${appId}/api/analytics/overview/dau`);
853
+ }
854
+ async getAnalyticsOverviewWau(appId) {
855
+ return this.get(`/app/${appId}/api/analytics/overview/wau`);
856
+ }
857
+ async getAnalyticsOverviewMau(appId) {
858
+ return this.get(`/app/${appId}/api/analytics/overview/mau`);
859
+ }
860
+ async getAnalyticsOverviewGrowth(appId, params) {
861
+ return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
862
+ }
863
+ async getAnalyticsDailyActive(appId, params) {
864
+ return this.get(`/app/${appId}/api/analytics/daily-active`, params);
865
+ }
866
+ async getAnalyticsRollingActive(appId, params) {
867
+ return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
868
+ }
869
+ async getAnalyticsCohortRetention(appId) {
870
+ return this.get(`/app/${appId}/api/analytics/cohort-retention`);
871
+ }
872
+ async getAnalyticsUserSearch(appId, params) {
873
+ return this.get(`/app/${appId}/api/analytics/users/search`, params);
874
+ }
875
+ async getAnalyticsUserDetail(appId, userUlid) {
876
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
877
+ }
878
+ async getAnalyticsUserSnapshot(appId, userUlid) {
879
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
880
+ }
881
+ async getAnalyticsEvents(appId, params) {
882
+ return this.get(`/app/${appId}/api/analytics/events`, params);
883
+ }
884
+ async getAnalyticsEventsGrouped(appId, params) {
885
+ return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
886
+ }
887
+ async getAnalyticsTopWorkflows(appId, params) {
888
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
889
+ }
890
+ async getAnalyticsTopPrompts(appId, params) {
891
+ return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
892
+ }
565
893
  // ============================================
566
894
  // SUPER ADMIN: ADMINS
567
895
  // ============================================
@@ -670,6 +998,24 @@ export class ApiClient {
670
998
  return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
671
999
  }
672
1000
  // ============================================
1001
+ // EMAIL TEMPLATES
1002
+ // ============================================
1003
+ async listEmailTemplates(appId) {
1004
+ return this.get(`/admin/api/apps/${appId}/email-templates`);
1005
+ }
1006
+ async getEmailTemplate(appId, emailType) {
1007
+ return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1008
+ }
1009
+ async upsertEmailTemplate(appId, emailType, payload) {
1010
+ return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, payload);
1011
+ }
1012
+ async deleteEmailTemplate(appId, emailType) {
1013
+ return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1014
+ }
1015
+ async testEmailTemplate(appId, emailType, payload) {
1016
+ return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
1017
+ }
1018
+ // ============================================
673
1019
  // ACCESS TOKENS
674
1020
  // ============================================
675
1021
  async createToken(appId, data) {
@@ -697,32 +1043,186 @@ export class ApiClient {
697
1043
  async getDatabase(appId, databaseId) {
698
1044
  return this.get(`/app/${appId}/api/databases/${databaseId}`);
699
1045
  }
1046
+ async updateDatabase(appId, databaseId, data) {
1047
+ return this.patch(`/app/${appId}/api/databases/${databaseId}`, data);
1048
+ }
700
1049
  async deleteDatabase(appId, databaseId) {
701
1050
  return this.delete(`/app/${appId}/api/databases/${databaseId}`);
702
1051
  }
1052
+ /**
1053
+ * Read a database's CEL context dict.
1054
+ *
1055
+ * The HTTP path stays `/metadata` because the wire field name is still
1056
+ * `metadata`; only the client/CLI-facing helper names were reframed.
1057
+ */
1058
+ async getDatabaseCelContext(appId, databaseId) {
1059
+ return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1060
+ }
1061
+ /** Update a database's CEL context dict (merge with existing). */
1062
+ async updateDatabaseCelContext(appId, databaseId, celContext) {
1063
+ return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1064
+ }
1065
+ /** @deprecated Use {@link getDatabaseCelContext} instead. */
1066
+ async getDatabaseMetadata(appId, databaseId) {
1067
+ return this.getDatabaseCelContext(appId, databaseId);
1068
+ }
1069
+ /** @deprecated Use {@link updateDatabaseCelContext} instead. */
1070
+ async updateDatabaseMetadata(appId, databaseId, metadata) {
1071
+ return this.updateDatabaseCelContext(appId, databaseId, metadata);
1072
+ }
703
1073
  // ============================================
704
1074
  // DATABASE PERMISSIONS
705
1075
  // ============================================
706
1076
  async listDatabasePermissions(appId, databaseId) {
707
1077
  return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
708
1078
  }
1079
+ async addDatabaseManager(appId, databaseId, userId) {
1080
+ return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
1081
+ userId,
1082
+ permission: "manager",
1083
+ });
1084
+ }
1085
+ async removeDatabaseManager(appId, databaseId, userId) {
1086
+ return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
1087
+ }
1088
+ /** @deprecated Use {@link addDatabaseManager} instead. */
709
1089
  async grantDatabasePermission(appId, databaseId, data) {
710
- return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, data);
1090
+ return this.addDatabaseManager(appId, databaseId, data.userId);
711
1091
  }
1092
+ /** @deprecated Use {@link removeDatabaseManager} instead. */
712
1093
  async revokeDatabasePermission(appId, databaseId, userId) {
713
1094
  return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
714
1095
  }
715
1096
  // ============================================
716
- // DATABASE GROUP PERMISSIONS
1097
+ // DATABASE RECORDS & SCHEMA
1098
+ // ============================================
1099
+ async listDatabaseModels(appId, databaseId) {
1100
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/models`);
1101
+ }
1102
+ async describeDatabaseModel(appId, databaseId, modelName) {
1103
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
1104
+ }
1105
+ async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1106
+ const body = { modelName };
1107
+ if (queryOptions?.filter)
1108
+ body.filter = queryOptions.filter;
1109
+ const options = {};
1110
+ if (queryOptions?.limit)
1111
+ options.limit = queryOptions.limit;
1112
+ if (queryOptions?.cursor)
1113
+ options.uniqueStartKey = queryOptions.cursor;
1114
+ if (Object.keys(options).length > 0)
1115
+ body.options = options;
1116
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/query`, body);
1117
+ }
1118
+ // ============================================
1119
+ // DATABASE OPERATIONS (registered operations)
1120
+ // ============================================
1121
+ async listDatabaseOperations(appId, databaseId) {
1122
+ return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
1123
+ }
1124
+ async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
1125
+ const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
1126
+ const extraHeaders = {};
1127
+ if (options?.timing) {
1128
+ extraHeaders["X-Timing"] = "true";
1129
+ }
1130
+ if (token) {
1131
+ return this.requestWithToken(path, token, {
1132
+ method: "POST",
1133
+ body: JSON.stringify(data || {}),
1134
+ headers: extraHeaders,
1135
+ });
1136
+ }
1137
+ return this.request(path, {
1138
+ method: "POST",
1139
+ body: JSON.stringify(data || {}),
1140
+ headers: extraHeaders,
1141
+ });
1142
+ }
1143
+ /**
1144
+ * Make a request using a specific JWT token instead of the CLI's credentials.
1145
+ * Used for executing operations as a different user.
1146
+ */
1147
+ async requestWithToken(path, token, options = {}) {
1148
+ const credentials = await this.ensureAuthenticated();
1149
+ const url = `${credentials.serverUrl}${path}`;
1150
+ const headers = {
1151
+ Authorization: `Bearer ${token}`,
1152
+ "Content-Type": "application/json",
1153
+ ...(options.headers || {}),
1154
+ };
1155
+ const response = await fetchWithTLS(url, { ...options, headers });
1156
+ const text = await response.text();
1157
+ if (!response.ok) {
1158
+ const parsed = parseErrorResponse(response, text, path);
1159
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
1160
+ }
1161
+ return text ? JSON.parse(text) : null;
1162
+ }
1163
+ // ============================================
1164
+ // DATABASE INDEXES
1165
+ // ============================================
1166
+ async listDatabaseIndexes(appId, databaseId, modelName) {
1167
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/indexes`, modelName ? { modelName } : undefined);
1168
+ }
1169
+ async registerDatabaseIndex(appId, databaseId, data) {
1170
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/register`, data);
1171
+ }
1172
+ async dropDatabaseIndex(appId, databaseId, data) {
1173
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
1174
+ }
1175
+ // ============================================
1176
+ // DATABASE TYPE CONFIGS
717
1177
  // ============================================
718
- async listDatabaseGroupPermissions(appId, databaseId) {
719
- return this.get(`/app/${appId}/api/databases/${databaseId}/group-permissions`);
1178
+ async listDatabaseTypeConfigs(appId) {
1179
+ return this.get(`/app/${appId}/api/databases/types`);
1180
+ }
1181
+ async getDatabaseTypeConfig(appId, databaseType) {
1182
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1183
+ }
1184
+ async createDatabaseTypeConfig(appId, data) {
1185
+ return this.post(`/app/${appId}/api/databases/types`, data);
1186
+ }
1187
+ async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1188
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1189
+ const qs = new URLSearchParams();
1190
+ if (options?.dryRun)
1191
+ qs.set("dryRun", "true");
1192
+ if (options?.acceptWarnings)
1193
+ qs.set("acceptWarnings", "true");
1194
+ const query = qs.toString();
1195
+ const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
1196
+ return this.patch(path, body);
1197
+ }
1198
+ async deleteDatabaseTypeConfig(appId, databaseType) {
1199
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1200
+ }
1201
+ /**
1202
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
1203
+ * existing ops + DO field introspection. Read-only — does NOT persist.
1204
+ */
1205
+ async scaffoldDatabaseTypeSchema(appId, databaseType) {
1206
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
720
1207
  }
721
- async grantDatabaseGroupPermission(appId, databaseId, data) {
722
- return this.post(`/app/${appId}/api/databases/${databaseId}/group-permissions`, data);
1208
+ // ============================================
1209
+ // DATABASE TYPE OPERATIONS
1210
+ // ============================================
1211
+ async listDatabaseTypeOperations(appId, databaseType) {
1212
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
1213
+ }
1214
+ async getDatabaseTypeOperation(appId, databaseType, name) {
1215
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
723
1216
  }
724
- async revokeDatabaseGroupPermission(appId, databaseId, groupType, groupId) {
725
- return this.delete(`/app/${appId}/api/databases/${databaseId}/group-permissions/${groupType}/${groupId}`);
1217
+ async createDatabaseTypeOperation(appId, databaseType, data) {
1218
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`, data);
1219
+ }
1220
+ async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt) {
1221
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1222
+ return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`, body);
1223
+ }
1224
+ async deleteDatabaseTypeOperation(appId, databaseType, name) {
1225
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
726
1226
  }
727
1227
  // ============================================
728
1228
  // GROUPS
@@ -754,8 +1254,11 @@ export class ApiClient {
754
1254
  async removeGroupMember(appId, groupType, groupId, userId) {
755
1255
  return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
756
1256
  }
757
- async updateGroupMemberRole(appId, groupType, groupId, userId, data) {
758
- return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`, data);
1257
+ // ============================================
1258
+ // GROUP RESOURCE LISTINGS
1259
+ // ============================================
1260
+ async listGroupDocuments(appId, groupType, groupId) {
1261
+ return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
759
1262
  }
760
1263
  // ============================================
761
1264
  // USER MEMBERSHIPS
@@ -763,6 +1266,362 @@ export class ApiClient {
763
1266
  async listUserMemberships(appId, userId) {
764
1267
  return this.get(`/app/${appId}/api/users/${userId}/memberships`);
765
1268
  }
1269
+ // ============================================
1270
+ // DOCUMENT GROUP PERMISSIONS
1271
+ // ============================================
1272
+ async listDocumentGroupPermissions(appId, documentId) {
1273
+ return this.get(`/app/${appId}/api/documents/${documentId}/group-permissions`);
1274
+ }
1275
+ async grantDocumentGroupPermission(appId, documentId, data) {
1276
+ return this.post(`/app/${appId}/api/documents/${documentId}/group-permissions`, data);
1277
+ }
1278
+ async revokeDocumentGroupPermission(appId, documentId, groupType, groupId) {
1279
+ return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
1280
+ }
1281
+ // ============================================
1282
+ // COLLECTIONS
1283
+ // ============================================
1284
+ async listCollections(appId, params) {
1285
+ const qs = new URLSearchParams();
1286
+ if (params?.limit)
1287
+ qs.set("limit", String(params.limit));
1288
+ if (params?.cursor)
1289
+ qs.set("cursor", params.cursor);
1290
+ const q = qs.toString();
1291
+ const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1292
+ return {
1293
+ items: result?.items ?? [],
1294
+ nextCursor: result?.cursor ?? null,
1295
+ };
1296
+ }
1297
+ async listAllCollections(appId, params) {
1298
+ const qs = new URLSearchParams();
1299
+ if (params?.limit)
1300
+ qs.set("limit", String(params.limit));
1301
+ if (params?.cursor)
1302
+ qs.set("cursor", params.cursor);
1303
+ const q = qs.toString();
1304
+ const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1305
+ return {
1306
+ items: result?.items ?? [],
1307
+ nextCursor: result?.cursor ?? null,
1308
+ };
1309
+ }
1310
+ async createCollection(appId, data) {
1311
+ return this.post(`/app/${appId}/api/collections`, data);
1312
+ }
1313
+ async getCollection(appId, collectionId) {
1314
+ return this.get(`/app/${appId}/api/collections/${collectionId}`);
1315
+ }
1316
+ async updateCollection(appId, collectionId, data) {
1317
+ return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
1318
+ }
1319
+ async deleteCollection(appId, collectionId) {
1320
+ return this.delete(`/app/${appId}/api/collections/${collectionId}`);
1321
+ }
1322
+ // ============================================
1323
+ // COLLECTION DOCUMENTS
1324
+ // ============================================
1325
+ async listCollectionDocuments(appId, collectionId, params) {
1326
+ const qs = new URLSearchParams();
1327
+ if (params?.limit)
1328
+ qs.set("limit", String(params.limit));
1329
+ if (params?.cursor)
1330
+ qs.set("cursor", params.cursor);
1331
+ const q = qs.toString();
1332
+ const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1333
+ return {
1334
+ items: result?.items ?? [],
1335
+ nextCursor: result?.cursor ?? null,
1336
+ };
1337
+ }
1338
+ async addCollectionDocument(appId, collectionId, data) {
1339
+ return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
1340
+ }
1341
+ async removeCollectionDocument(appId, collectionId, documentId) {
1342
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
1343
+ }
1344
+ async listCollectionsForDocument(appId, documentId, params) {
1345
+ const qs = new URLSearchParams();
1346
+ if (params?.limit)
1347
+ qs.set("limit", String(params.limit));
1348
+ if (params?.cursor)
1349
+ qs.set("cursor", params.cursor);
1350
+ const q = qs.toString();
1351
+ const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
1352
+ return {
1353
+ items: result?.items ?? [],
1354
+ nextCursor: result?.cursor ?? null,
1355
+ };
1356
+ }
1357
+ // ============================================
1358
+ // COLLECTION ACCESS (GROUPS + MEMBERS)
1359
+ // ============================================
1360
+ async getCollectionAccess(appId, collectionId) {
1361
+ return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
1362
+ }
1363
+ async grantCollectionGroupPermission(appId, collectionId, data) {
1364
+ return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
1365
+ }
1366
+ async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
1367
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
1368
+ }
1369
+ async addCollectionMember(appId, collectionId, data) {
1370
+ return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
1371
+ }
1372
+ async removeCollectionMember(appId, collectionId, userId) {
1373
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
1374
+ }
1375
+ // ============================================
1376
+ // COLLECTION TYPE CONFIGS
1377
+ // ============================================
1378
+ async listCollectionTypeConfigs(appId) {
1379
+ return this.get(`/app/${appId}/api/collection-type-configs`);
1380
+ }
1381
+ async getCollectionTypeConfig(appId, collectionType) {
1382
+ return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
1383
+ }
1384
+ async createCollectionTypeConfig(appId, data) {
1385
+ return this.post(`/app/${appId}/api/collection-type-configs`, data);
1386
+ }
1387
+ async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
1388
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1389
+ return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
1390
+ }
1391
+ async deleteCollectionTypeConfig(appId, collectionType) {
1392
+ return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
1393
+ }
1394
+ // ============================================
1395
+ // ACCESS RULE SETS
1396
+ // ============================================
1397
+ async listRuleSets(appId, params) {
1398
+ return this.get(`/app/${appId}/api/rule-sets`, params);
1399
+ }
1400
+ async createRuleSet(appId, data) {
1401
+ return this.post(`/app/${appId}/api/rule-sets`, data);
1402
+ }
1403
+ async getRuleSet(appId, ruleSetId) {
1404
+ return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
1405
+ }
1406
+ async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
1407
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1408
+ return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
1409
+ }
1410
+ async deleteRuleSet(appId, ruleSetId) {
1411
+ return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
1412
+ }
1413
+ async getRuleSetSchema(appId) {
1414
+ return this.get(`/app/${appId}/api/rule-sets/schema`);
1415
+ }
1416
+ async testRuleSet(appId, ruleSetId, data) {
1417
+ return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
1418
+ }
1419
+ async debugRuleSet(appId, data) {
1420
+ return this.post(`/app/${appId}/api/rule-sets/debug`, data);
1421
+ }
1422
+ // ============================================
1423
+ // GROUP TYPE CONFIGS
1424
+ // ============================================
1425
+ async listGroupTypeConfigs(appId) {
1426
+ return this.get(`/app/${appId}/api/group-type-configs`);
1427
+ }
1428
+ async getGroupTypeConfig(appId, groupType) {
1429
+ return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
1430
+ }
1431
+ async createGroupTypeConfig(appId, data) {
1432
+ return this.post(`/app/${appId}/api/group-type-configs`, data);
1433
+ }
1434
+ async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
1435
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1436
+ return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
1437
+ }
1438
+ async deleteGroupTypeConfig(appId, groupType) {
1439
+ return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
1440
+ }
1441
+ // ============================================
1442
+ // DOCUMENT EXPORT / IMPORT
1443
+ // ============================================
1444
+ async exportDocumentState(appId, documentId) {
1445
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
1446
+ }
1447
+ async importDocumentState(appId, documentId, stateBase64) {
1448
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
1449
+ }
1450
+ async getDocument(appId, documentId) {
1451
+ return this.get(`/app/${appId}/api/documents/${documentId}`);
1452
+ }
1453
+ async createDocument(appId, data) {
1454
+ return this.post(`/app/${appId}/api/documents`, data);
1455
+ }
1456
+ async listDocumentPermissions(appId, documentId) {
1457
+ return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
1458
+ }
1459
+ async grantDocumentPermission(appId, documentId, permissions) {
1460
+ return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
1461
+ }
1462
+ async listDocumentInvitations(appId, documentId) {
1463
+ return this.get(`/app/${appId}/api/documents/${documentId}/invitations`);
1464
+ }
1465
+ async listDocumentBlobs(appId, documentId) {
1466
+ return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
1467
+ }
1468
+ async downloadBlob(appId, documentId, blobId) {
1469
+ const credentials = await this.ensureAuthenticated();
1470
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
1471
+ const headers = {
1472
+ Authorization: `Bearer ${credentials.accessToken}`,
1473
+ };
1474
+ if (credentials.globalAdminAppId) {
1475
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1476
+ }
1477
+ const response = await fetchWithTLS(url, { headers });
1478
+ if (!response.ok) {
1479
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
1480
+ }
1481
+ const arrayBuffer = await response.arrayBuffer();
1482
+ return Buffer.from(arrayBuffer);
1483
+ }
1484
+ async uploadBlob(appId, documentId, blobId, data, meta) {
1485
+ const credentials = await this.ensureAuthenticated();
1486
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
1487
+ const headers = {
1488
+ Authorization: `Bearer ${credentials.accessToken}`,
1489
+ "Content-Type": meta.contentType,
1490
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
1491
+ "X-Blob-Size": String(data.length),
1492
+ "X-Blob-Sha256": meta.sha256,
1493
+ };
1494
+ if (credentials.globalAdminAppId) {
1495
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1496
+ }
1497
+ const response = await fetchWithTLS(url, {
1498
+ method: "PUT",
1499
+ headers,
1500
+ body: data,
1501
+ });
1502
+ if (!response.ok) {
1503
+ const text = await response.text();
1504
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
1505
+ }
1506
+ return response.json();
1507
+ }
1508
+ async listDocumentAliases(appId, documentId) {
1509
+ return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
1510
+ }
1511
+ async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
1512
+ return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
1513
+ documentId,
1514
+ userId: ownerUserId,
1515
+ mustNotExist,
1516
+ });
1517
+ }
1518
+ async findUserByEmail(appId, email) {
1519
+ try {
1520
+ const result = await this.get(`/app/${appId}/api/users`, { email });
1521
+ const items = result?.items || (Array.isArray(result) ? result : []);
1522
+ return items[0] || null;
1523
+ }
1524
+ catch {
1525
+ return null;
1526
+ }
1527
+ }
1528
+ async listAdminDocuments(appId, userId) {
1529
+ const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
1530
+ return result?.documents || result || [];
1531
+ }
1532
+ // ============================================
1533
+ // DATABASE EXPORT / IMPORT
1534
+ // ============================================
1535
+ async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
1536
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/save`, {
1537
+ modelName,
1538
+ id,
1539
+ data,
1540
+ });
1541
+ }
1542
+ async batchDatabaseRecords(appId, databaseId, operations) {
1543
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
1544
+ }
1545
+ async deleteDatabaseRecord(appId, databaseId, modelName, id) {
1546
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/delete`, { modelName, id });
1547
+ }
1548
+ async batchDeleteDatabaseRecords(appId, databaseId, operations) {
1549
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
1550
+ }
1551
+ async listDatabaseUniqueConstraints(appId, databaseId) {
1552
+ const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
1553
+ return result?.constraints || result || [];
1554
+ }
1555
+ async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
1556
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
1557
+ }
1558
+ // ============================================
1559
+ // BLOB BUCKETS
1560
+ // ============================================
1561
+ async listBlobBuckets(appId) {
1562
+ return this.get(`/app/${appId}/api/blob-buckets`);
1563
+ }
1564
+ async createBlobBucket(appId, payload) {
1565
+ return this.post(`/app/${appId}/api/blob-buckets`, payload);
1566
+ }
1567
+ async getBlobBucket(appId, bucketIdOrKey) {
1568
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1569
+ }
1570
+ async deleteBlobBucket(appId, bucketIdOrKey) {
1571
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1572
+ }
1573
+ async listBucketBlobs(appId, bucketIdOrKey, params) {
1574
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
1575
+ }
1576
+ async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
1577
+ const credentials = await this.ensureAuthenticated();
1578
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
1579
+ const headers = {
1580
+ Authorization: `Bearer ${credentials.accessToken}`,
1581
+ "Content-Type": meta.contentType,
1582
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
1583
+ };
1584
+ if (meta.tags && meta.tags.length > 0) {
1585
+ headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
1586
+ }
1587
+ if (credentials.globalAdminAppId) {
1588
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1589
+ }
1590
+ const response = await fetchWithTLS(url, {
1591
+ method: "POST",
1592
+ headers,
1593
+ body: data,
1594
+ });
1595
+ if (!response.ok) {
1596
+ const text = await response.text();
1597
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
1598
+ }
1599
+ return response.json();
1600
+ }
1601
+ async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
1602
+ const credentials = await this.ensureAuthenticated();
1603
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
1604
+ const headers = {
1605
+ Authorization: `Bearer ${credentials.accessToken}`,
1606
+ };
1607
+ if (credentials.globalAdminAppId) {
1608
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1609
+ }
1610
+ const response = await fetchWithTLS(url, { headers });
1611
+ if (!response.ok) {
1612
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
1613
+ }
1614
+ const arrayBuffer = await response.arrayBuffer();
1615
+ return Buffer.from(arrayBuffer);
1616
+ }
1617
+ async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
1618
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
1619
+ }
1620
+ async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
1621
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
1622
+ expiresInSeconds: expiresInSeconds || 300,
1623
+ });
1624
+ }
766
1625
  }
767
1626
  // Export a singleton instance
768
1627
  export const apiClient = new ApiClient();