primitive-admin 1.1.0-alpha.4 → 1.1.0-alpha.40

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 (216) hide show
  1. package/README.md +325 -31
  2. package/assets/skill/skills/primitive-platform/SKILL.md +281 -0
  3. package/dist/bin/primitive.d.ts +2 -0
  4. package/dist/bin/primitive.js +266 -16
  5. package/dist/bin/primitive.js.map +1 -1
  6. package/dist/src/commands/admins.d.ts +2 -0
  7. package/dist/src/commands/admins.js +123 -18
  8. package/dist/src/commands/admins.js.map +1 -1
  9. package/dist/src/commands/analytics.d.ts +2 -0
  10. package/dist/src/commands/analytics.js +464 -55
  11. package/dist/src/commands/analytics.js.map +1 -1
  12. package/dist/src/commands/apps.d.ts +2 -0
  13. package/dist/src/commands/apps.js +55 -13
  14. package/dist/src/commands/apps.js.map +1 -1
  15. package/dist/src/commands/auth.d.ts +2 -0
  16. package/dist/src/commands/auth.js +177 -7
  17. package/dist/src/commands/auth.js.map +1 -1
  18. package/dist/src/commands/blob-buckets.d.ts +2 -0
  19. package/dist/src/commands/blob-buckets.js +422 -0
  20. package/dist/src/commands/blob-buckets.js.map +1 -0
  21. package/dist/src/commands/catalog.d.ts +2 -0
  22. package/dist/src/commands/catalog.js +34 -35
  23. package/dist/src/commands/catalog.js.map +1 -1
  24. package/dist/src/commands/collection-type-configs.d.ts +2 -0
  25. package/dist/src/commands/collection-type-configs.js +178 -0
  26. package/dist/src/commands/collection-type-configs.js.map +1 -0
  27. package/dist/src/commands/collections.d.ts +2 -0
  28. package/dist/src/commands/collections.js +515 -0
  29. package/dist/src/commands/collections.js.map +1 -0
  30. package/dist/src/commands/comparisons.d.ts +2 -0
  31. package/dist/src/commands/comparisons.js +6 -6
  32. package/dist/src/commands/comparisons.js.map +1 -1
  33. package/dist/src/commands/cron-triggers.d.ts +2 -0
  34. package/dist/src/commands/cron-triggers.js +357 -0
  35. package/dist/src/commands/cron-triggers.js.map +1 -0
  36. package/dist/src/commands/database-types.d.ts +2 -0
  37. package/dist/src/commands/database-types.js +471 -0
  38. package/dist/src/commands/database-types.js.map +1 -0
  39. package/dist/src/commands/databases.d.ts +2 -0
  40. package/dist/src/commands/databases.js +1462 -83
  41. package/dist/src/commands/databases.js.map +1 -1
  42. package/dist/src/commands/documents.d.ts +2 -0
  43. package/dist/src/commands/documents.js +518 -11
  44. package/dist/src/commands/documents.js.map +1 -1
  45. package/dist/src/commands/email-templates.d.ts +2 -0
  46. package/dist/src/commands/email-templates.js +281 -0
  47. package/dist/src/commands/email-templates.js.map +1 -0
  48. package/dist/src/commands/env.d.ts +12 -0
  49. package/dist/src/commands/env.js +260 -0
  50. package/dist/src/commands/env.js.map +1 -0
  51. package/dist/src/commands/group-type-configs.d.ts +2 -0
  52. package/dist/src/commands/group-type-configs.js +181 -0
  53. package/dist/src/commands/group-type-configs.js.map +1 -0
  54. package/dist/src/commands/groups.d.ts +2 -0
  55. package/dist/src/commands/groups.js +45 -43
  56. package/dist/src/commands/groups.js.map +1 -1
  57. package/dist/src/commands/guides.d.ts +159 -0
  58. package/dist/src/commands/guides.js +449 -35
  59. package/dist/src/commands/guides.js.map +1 -1
  60. package/dist/src/commands/init.d.ts +17 -0
  61. package/dist/src/commands/init.js +811 -201
  62. package/dist/src/commands/init.js.map +1 -1
  63. package/dist/src/commands/integrations.d.ts +2 -0
  64. package/dist/src/commands/integrations.js +533 -46
  65. package/dist/src/commands/integrations.js.map +1 -1
  66. package/dist/src/commands/llm.d.ts +2 -0
  67. package/dist/src/commands/llm.js +4 -2
  68. package/dist/src/commands/llm.js.map +1 -1
  69. package/dist/src/commands/prompts.d.ts +2 -0
  70. package/dist/src/commands/prompts.js +50 -52
  71. package/dist/src/commands/prompts.js.map +1 -1
  72. package/dist/src/commands/rule-sets.d.ts +3 -0
  73. package/dist/src/commands/rule-sets.js +388 -0
  74. package/dist/src/commands/rule-sets.js.map +1 -0
  75. package/dist/src/commands/scripts.d.ts +2 -0
  76. package/dist/src/commands/scripts.js +667 -0
  77. package/dist/src/commands/scripts.js.map +1 -0
  78. package/dist/src/commands/secrets.d.ts +2 -0
  79. package/dist/src/commands/secrets.js +108 -0
  80. package/dist/src/commands/secrets.js.map +1 -0
  81. package/dist/src/commands/skill.d.ts +2 -0
  82. package/dist/src/commands/skill.js +29 -0
  83. package/dist/src/commands/skill.js.map +1 -0
  84. package/dist/src/commands/sync.d.ts +174 -0
  85. package/dist/src/commands/sync.js +4392 -235
  86. package/dist/src/commands/sync.js.map +1 -1
  87. package/dist/src/commands/tokens.d.ts +2 -0
  88. package/dist/src/commands/tokens.js +122 -19
  89. package/dist/src/commands/tokens.js.map +1 -1
  90. package/dist/src/commands/users.d.ts +2 -0
  91. package/dist/src/commands/users.js +440 -22
  92. package/dist/src/commands/users.js.map +1 -1
  93. package/dist/src/commands/waitlist.d.ts +2 -0
  94. package/dist/src/commands/waitlist.js +10 -10
  95. package/dist/src/commands/waitlist.js.map +1 -1
  96. package/dist/src/commands/webhooks.d.ts +2 -0
  97. package/dist/src/commands/webhooks.js +391 -0
  98. package/dist/src/commands/webhooks.js.map +1 -0
  99. package/dist/src/commands/workflows.d.ts +49 -0
  100. package/dist/src/commands/workflows.js +1009 -123
  101. package/dist/src/commands/workflows.js.map +1 -1
  102. package/dist/src/lib/api-client.d.ts +1272 -0
  103. package/dist/src/lib/api-client.js +1115 -83
  104. package/dist/src/lib/api-client.js.map +1 -1
  105. package/dist/src/lib/auth-flow.d.ts +8 -0
  106. package/dist/src/lib/block-layout.d.ts +160 -0
  107. package/dist/src/lib/block-layout.js +451 -0
  108. package/dist/src/lib/block-layout.js.map +1 -0
  109. package/dist/src/lib/cli-manifest.d.ts +60 -0
  110. package/dist/src/lib/cli-manifest.js +70 -0
  111. package/dist/src/lib/cli-manifest.js.map +1 -0
  112. package/dist/src/lib/config.d.ts +53 -0
  113. package/dist/src/lib/config.js +92 -53
  114. package/dist/src/lib/config.js.map +1 -1
  115. package/dist/src/lib/confirm-prompt.d.ts +83 -0
  116. package/dist/src/lib/confirm-prompt.js +110 -0
  117. package/dist/src/lib/confirm-prompt.js.map +1 -0
  118. package/dist/src/lib/constants.d.ts +2 -0
  119. package/dist/src/lib/constants.js +3 -0
  120. package/dist/src/lib/constants.js.map +1 -0
  121. package/dist/src/lib/crash-handlers.d.ts +20 -0
  122. package/dist/src/lib/crash-handlers.js +49 -0
  123. package/dist/src/lib/crash-handlers.js.map +1 -0
  124. package/dist/src/lib/credentials-store.d.ts +79 -0
  125. package/dist/src/lib/credentials-store.js +307 -0
  126. package/dist/src/lib/credentials-store.js.map +1 -0
  127. package/dist/src/lib/csv.d.ts +48 -0
  128. package/dist/src/lib/csv.js +177 -0
  129. package/dist/src/lib/csv.js.map +1 -0
  130. package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
  131. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  132. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  133. package/dist/src/lib/db-codegen/dbGenerator.d.ts +111 -0
  134. package/dist/src/lib/db-codegen/dbGenerator.js +371 -0
  135. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  136. package/dist/src/lib/db-codegen/dbNaming.d.ts +45 -0
  137. package/dist/src/lib/db-codegen/dbNaming.js +104 -0
  138. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  139. package/dist/src/lib/db-codegen/dbTemplates.d.ts +97 -0
  140. package/dist/src/lib/db-codegen/dbTemplates.js +159 -0
  141. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  142. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +78 -0
  143. package/dist/src/lib/db-codegen/dbTsTypes.js +112 -0
  144. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  145. package/dist/src/lib/env-resolver.d.ts +62 -0
  146. package/dist/src/lib/env-resolver.js +121 -0
  147. package/dist/src/lib/env-resolver.js.map +1 -0
  148. package/dist/src/lib/fetch.d.ts +5 -0
  149. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  150. package/dist/src/lib/generated-allowlist.js +213 -0
  151. package/dist/src/lib/generated-allowlist.js.map +1 -0
  152. package/dist/src/lib/init-config.d.ts +59 -0
  153. package/dist/src/lib/init-config.js +113 -0
  154. package/dist/src/lib/init-config.js.map +1 -0
  155. package/dist/src/lib/migration-nag.d.ts +49 -0
  156. package/dist/src/lib/migration-nag.js +163 -0
  157. package/dist/src/lib/migration-nag.js.map +1 -0
  158. package/dist/src/lib/output.d.ts +86 -0
  159. package/dist/src/lib/output.js +150 -8
  160. package/dist/src/lib/output.js.map +1 -1
  161. package/dist/src/lib/paginate.d.ts +33 -0
  162. package/dist/src/lib/paginate.js +42 -0
  163. package/dist/src/lib/paginate.js.map +1 -0
  164. package/dist/src/lib/project-config.d.ts +97 -0
  165. package/dist/src/lib/project-config.js +209 -0
  166. package/dist/src/lib/project-config.js.map +1 -0
  167. package/dist/src/lib/query-operators.d.ts +43 -0
  168. package/dist/src/lib/query-operators.js +80 -0
  169. package/dist/src/lib/query-operators.js.map +1 -0
  170. package/dist/src/lib/refresh-admin-credentials.d.ts +65 -0
  171. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  172. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  173. package/dist/src/lib/resolve-platform.d.ts +45 -0
  174. package/dist/src/lib/resolve-platform.js +43 -0
  175. package/dist/src/lib/resolve-platform.js.map +1 -0
  176. package/dist/src/lib/skill-installer.d.ts +23 -0
  177. package/dist/src/lib/skill-installer.js +135 -0
  178. package/dist/src/lib/skill-installer.js.map +1 -0
  179. package/dist/src/lib/snapshots.d.ts +99 -0
  180. package/dist/src/lib/snapshots.js +357 -0
  181. package/dist/src/lib/snapshots.js.map +1 -0
  182. package/dist/src/lib/sync-paths.d.ts +72 -0
  183. package/dist/src/lib/sync-paths.js +130 -0
  184. package/dist/src/lib/sync-paths.js.map +1 -0
  185. package/dist/src/lib/template.d.ts +97 -0
  186. package/dist/src/lib/template.js +336 -62
  187. package/dist/src/lib/template.js.map +1 -1
  188. package/dist/src/lib/token-inject.d.ts +56 -0
  189. package/dist/src/lib/token-inject.js +204 -0
  190. package/dist/src/lib/token-inject.js.map +1 -0
  191. package/dist/src/lib/toml-database-config.d.ts +152 -0
  192. package/dist/src/lib/toml-database-config.js +600 -0
  193. package/dist/src/lib/toml-database-config.js.map +1 -0
  194. package/dist/src/lib/toml-metadata-config.d.ts +108 -0
  195. package/dist/src/lib/toml-metadata-config.js +371 -0
  196. package/dist/src/lib/toml-metadata-config.js.map +1 -0
  197. package/dist/src/lib/toml-params-validator.d.ts +129 -0
  198. package/dist/src/lib/toml-params-validator.js +298 -0
  199. package/dist/src/lib/toml-params-validator.js.map +1 -0
  200. package/dist/src/lib/version-check.d.ts +10 -0
  201. package/dist/src/lib/version-check.js +172 -0
  202. package/dist/src/lib/version-check.js.map +1 -0
  203. package/dist/src/lib/workflow-apply.d.ts +66 -0
  204. package/dist/src/lib/workflow-apply.js +117 -0
  205. package/dist/src/lib/workflow-apply.js.map +1 -0
  206. package/dist/src/lib/workflow-fragments.d.ts +41 -0
  207. package/dist/src/lib/workflow-fragments.js +121 -0
  208. package/dist/src/lib/workflow-fragments.js.map +1 -0
  209. package/dist/src/lib/workflow-toml-validator.d.ts +95 -0
  210. package/dist/src/lib/workflow-toml-validator.js +323 -0
  211. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  212. package/dist/src/types/index.d.ts +520 -0
  213. package/dist/src/validators.d.ts +64 -0
  214. package/dist/src/validators.js +63 -0
  215. package/dist/src/validators.js.map +1 -0
  216. package/package.json +18 -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);
244
+ }
245
+ if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
246
+ throw new SchemaRequiredError(parsed.message, detailsRecord);
247
+ }
248
+ if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
249
+ throw new OperationRefError(parsed.message, detailsRecord);
104
250
  }
105
- if (response.status === 409 && errorData?.code === "CONFLICT") {
106
- throw new ConflictError(errorData.message || "Resource conflict", errorData.serverModifiedAt, errorData.expectedModifiedAt);
251
+ if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
252
+ throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
107
253
  }
108
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
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,99 @@ 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
+ // ============================================
574
+ // ITERATIONS (iterate-users introspection / reset — #1209)
575
+ // ============================================
576
+ async listIterations(appId) {
577
+ const result = await this.get(`/app/${appId}/api/iterations`);
578
+ return { items: result?.items ?? [] };
579
+ }
580
+ async getIteration(appId, iterationName) {
581
+ return this.get(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}`);
582
+ }
583
+ async resetIteration(appId, iterationName) {
584
+ return this.post(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}/reset`, {});
585
+ }
586
+ // ============================================
277
587
  // PROMPTS
278
588
  // ============================================
279
589
  async listPrompts(appId, params) {
@@ -325,6 +635,55 @@ export class ApiClient {
325
635
  async duplicatePromptConfig(appId, promptId, configId, payload) {
326
636
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/configs/${configId}/duplicate`, payload || {});
327
637
  }
638
+ // ============================================
639
+ // SCRIPTS (Rhai transforms) — #1000 (prompt-model convergence)
640
+ // ============================================
641
+ //
642
+ // The `Script` model is the authoring surface for `script` workflow
643
+ // steps. A `Script` is a HEADER (name/description/activeConfigId); the
644
+ // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
645
+ // time. CLI `sync push` reads `transforms/*.rhai` files and reconciles
646
+ // them: a new file → create script (mints a default config); a changed
647
+ // file → create a new config + activate it (zero fan-out — referencing
648
+ // workflows pick up the new body on their next run).
649
+ async listScripts(appId) {
650
+ const result = await this.get(`/admin/api/apps/${appId}/scripts`);
651
+ return { items: result?.items ?? [] };
652
+ }
653
+ async getScript(appId, scriptId) {
654
+ return this.get(`/admin/api/apps/${appId}/scripts/${scriptId}`);
655
+ }
656
+ /**
657
+ * List a script's versioned configs (the `ScriptConfig` rows). Each config
658
+ * is a distinct block version: `configId` is the stable version selector,
659
+ * `contentHash` is its content identity, and `status` reports whether it is
660
+ * `active`/`draft`/`archived`. The script header's `activeConfigId` names
661
+ * the live version. Used by `scripts configs list` and by the script test
662
+ * commands to run against a specific pinned version.
663
+ */
664
+ async listScriptConfigs(appId, scriptId) {
665
+ const result = await this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`);
666
+ return { items: result?.items ?? [] };
667
+ }
668
+ async createScript(appId, payload) {
669
+ // Mints the script header + a default `active` config carrying the body.
670
+ return this.post(`/admin/api/apps/${appId}/scripts`, payload);
671
+ }
672
+ /**
673
+ * Push a new body for an existing script by creating a new `ScriptConfig`
674
+ * and activating it (the prompt-model "edit → activate" flow). Referencing
675
+ * workflows pick up the new body on their next run with no fan-out. The
676
+ * config name is unique per script, so we mint a timestamped name.
677
+ */
678
+ async pushScriptBody(appId, scriptId, body) {
679
+ const configName = `sync-${Date.now()}`;
680
+ const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
681
+ await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, {});
682
+ return config;
683
+ }
684
+ async deleteScript(appId, scriptId) {
685
+ return this.delete(`/admin/api/apps/${appId}/scripts/${scriptId}`);
686
+ }
328
687
  async getPromptSchema(appId, promptId) {
329
688
  return this.get(`/admin/api/apps/${appId}/prompts/${promptId}/schema`);
330
689
  }
@@ -415,14 +774,8 @@ export class ApiClient {
415
774
  });
416
775
  const text = await response.text();
417
776
  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);
777
+ const parsed = parseErrorResponse(response, text, url);
778
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
426
779
  }
427
780
  return text ? JSON.parse(text) : null;
428
781
  }
@@ -442,14 +795,8 @@ export class ApiClient {
442
795
  });
443
796
  if (!response.ok) {
444
797
  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);
798
+ const parsed = parseErrorResponse(response, text, url);
799
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
453
800
  }
454
801
  const arrayBuffer = await response.arrayBuffer();
455
802
  return {
@@ -519,6 +866,16 @@ export class ApiClient {
519
866
  async getWorkflowRunStatus(appId, workflowId, runId) {
520
867
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
521
868
  }
869
+ async getWorkflowStepRuns(appId, workflowId, runId) {
870
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
871
+ return { items: result?.items ?? [] };
872
+ }
873
+ async getWorkflowAnalytics(appId, params) {
874
+ return this.get(`/app/${appId}/api/analytics/workflows/overview`, params);
875
+ }
876
+ async getTopWorkflows(appId, params) {
877
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
878
+ }
522
879
  // ============================================
523
880
  // WORKFLOW CONFIGURATIONS
524
881
  // ============================================
@@ -547,21 +904,54 @@ export class ApiClient {
547
904
  // ============================================
548
905
  // ANALYTICS
549
906
  // ============================================
550
- async getAnalyticsOverview(appId, params) {
551
- return this.get(`/app/${appId}/api/analytics/overview`, params);
552
- }
553
907
  async getAnalyticsTopUsers(appId, params) {
554
908
  return this.get(`/app/${appId}/api/analytics/users/top`, params);
555
909
  }
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
910
  async getAnalyticsIntegrationMetrics(appId, params) {
563
911
  return this.get(`/app/${appId}/api/analytics/integrations`, params);
564
912
  }
913
+ async getAnalyticsOverviewDau(appId) {
914
+ return this.get(`/app/${appId}/api/analytics/overview/dau`);
915
+ }
916
+ async getAnalyticsOverviewWau(appId) {
917
+ return this.get(`/app/${appId}/api/analytics/overview/wau`);
918
+ }
919
+ async getAnalyticsOverviewMau(appId) {
920
+ return this.get(`/app/${appId}/api/analytics/overview/mau`);
921
+ }
922
+ async getAnalyticsOverviewGrowth(appId, params) {
923
+ return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
924
+ }
925
+ async getAnalyticsDailyActive(appId, params) {
926
+ return this.get(`/app/${appId}/api/analytics/daily-active`, params);
927
+ }
928
+ async getAnalyticsRollingActive(appId, params) {
929
+ return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
930
+ }
931
+ async getAnalyticsCohortRetention(appId) {
932
+ return this.get(`/app/${appId}/api/analytics/cohort-retention`);
933
+ }
934
+ async getAnalyticsUserSearch(appId, params) {
935
+ return this.get(`/app/${appId}/api/analytics/users/search`, params);
936
+ }
937
+ async getAnalyticsUserDetail(appId, userUlid) {
938
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
939
+ }
940
+ async getAnalyticsUserSnapshot(appId, userUlid) {
941
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
942
+ }
943
+ async getAnalyticsEvents(appId, params) {
944
+ return this.get(`/app/${appId}/api/analytics/events`, params);
945
+ }
946
+ async getAnalyticsEventsGrouped(appId, params) {
947
+ return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
948
+ }
949
+ async getAnalyticsTopWorkflows(appId, params) {
950
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
951
+ }
952
+ async getAnalyticsTopPrompts(appId, params) {
953
+ return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
954
+ }
565
955
  // ============================================
566
956
  // SUPER ADMIN: ADMINS
567
957
  // ============================================
@@ -670,6 +1060,24 @@ export class ApiClient {
670
1060
  return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
671
1061
  }
672
1062
  // ============================================
1063
+ // EMAIL TEMPLATES
1064
+ // ============================================
1065
+ async listEmailTemplates(appId) {
1066
+ return this.get(`/admin/api/apps/${appId}/email-templates`);
1067
+ }
1068
+ async getEmailTemplate(appId, emailType) {
1069
+ return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1070
+ }
1071
+ async upsertEmailTemplate(appId, emailType, payload) {
1072
+ return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, payload);
1073
+ }
1074
+ async deleteEmailTemplate(appId, emailType) {
1075
+ return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1076
+ }
1077
+ async testEmailTemplate(appId, emailType, payload) {
1078
+ return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
1079
+ }
1080
+ // ============================================
673
1081
  // ACCESS TOKENS
674
1082
  // ============================================
675
1083
  async createToken(appId, data) {
@@ -697,32 +1105,268 @@ export class ApiClient {
697
1105
  async getDatabase(appId, databaseId) {
698
1106
  return this.get(`/app/${appId}/api/databases/${databaseId}`);
699
1107
  }
1108
+ async updateDatabase(appId, databaseId, data) {
1109
+ return this.patch(`/app/${appId}/api/databases/${databaseId}`, data);
1110
+ }
700
1111
  async deleteDatabase(appId, databaseId) {
701
1112
  return this.delete(`/app/${appId}/api/databases/${databaseId}`);
702
1113
  }
1114
+ /**
1115
+ * Read a database's CEL context dict.
1116
+ *
1117
+ * The HTTP path stays `/metadata` because the wire field name is still
1118
+ * `metadata`; only the client/CLI-facing helper names were reframed.
1119
+ */
1120
+ async getDatabaseCelContext(appId, databaseId) {
1121
+ return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1122
+ }
1123
+ /** Update a database's CEL context dict (merge with existing). */
1124
+ async updateDatabaseCelContext(appId, databaseId, celContext) {
1125
+ return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1126
+ }
1127
+ /** @deprecated Use {@link getDatabaseCelContext} instead. */
1128
+ async getDatabaseMetadata(appId, databaseId) {
1129
+ return this.getDatabaseCelContext(appId, databaseId);
1130
+ }
1131
+ /** @deprecated Use {@link updateDatabaseCelContext} instead. */
1132
+ async updateDatabaseMetadata(appId, databaseId, metadata) {
1133
+ return this.updateDatabaseCelContext(appId, databaseId, metadata);
1134
+ }
703
1135
  // ============================================
704
1136
  // DATABASE PERMISSIONS
705
1137
  // ============================================
706
1138
  async listDatabasePermissions(appId, databaseId) {
707
1139
  return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
708
1140
  }
1141
+ async addDatabaseManager(appId, databaseId, userId) {
1142
+ return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
1143
+ userId,
1144
+ permission: "manager",
1145
+ });
1146
+ }
1147
+ async removeDatabaseManager(appId, databaseId, userId) {
1148
+ return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
1149
+ }
1150
+ /** @deprecated Use {@link addDatabaseManager} instead. */
709
1151
  async grantDatabasePermission(appId, databaseId, data) {
710
- return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, data);
1152
+ return this.addDatabaseManager(appId, databaseId, data.userId);
711
1153
  }
1154
+ /** @deprecated Use {@link removeDatabaseManager} instead. */
712
1155
  async revokeDatabasePermission(appId, databaseId, userId) {
713
1156
  return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
714
1157
  }
715
1158
  // ============================================
716
- // DATABASE GROUP PERMISSIONS
1159
+ // DATABASE RECORDS & SCHEMA
1160
+ // ============================================
1161
+ async listDatabaseModels(appId, databaseId) {
1162
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/models`);
1163
+ }
1164
+ async describeDatabaseModel(appId, databaseId, modelName) {
1165
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
1166
+ }
1167
+ async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1168
+ const body = { modelName };
1169
+ if (queryOptions?.filter)
1170
+ body.filter = queryOptions.filter;
1171
+ const options = {};
1172
+ if (queryOptions?.limit)
1173
+ options.limit = queryOptions.limit;
1174
+ if (queryOptions?.cursor)
1175
+ options.uniqueStartKey = queryOptions.cursor;
1176
+ if (Object.keys(options).length > 0)
1177
+ body.options = options;
1178
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/query`, body);
1179
+ }
1180
+ // ============================================
1181
+ // DATABASE OPERATIONS (registered operations)
1182
+ // ============================================
1183
+ async listDatabaseOperations(appId, databaseId) {
1184
+ return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
1185
+ }
1186
+ async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
1187
+ const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
1188
+ const extraHeaders = {};
1189
+ if (options?.timing) {
1190
+ extraHeaders["X-Timing"] = "true";
1191
+ }
1192
+ if (token) {
1193
+ return this.requestWithToken(path, token, {
1194
+ method: "POST",
1195
+ body: JSON.stringify(data || {}),
1196
+ headers: extraHeaders,
1197
+ });
1198
+ }
1199
+ return this.request(path, {
1200
+ method: "POST",
1201
+ body: JSON.stringify(data || {}),
1202
+ headers: extraHeaders,
1203
+ });
1204
+ }
1205
+ /**
1206
+ * Execute a registered batch (bulk) database operation. Posts a chunk of
1207
+ * items to the canonical `operations/:name/batch` endpoint (the same one the
1208
+ * client library's `executeBatch` uses — NOT the deprecated `import-bulk`
1209
+ * alias). Used by `databases import-csv`.
1210
+ *
1211
+ * @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
1212
+ * validation/access failures abort the whole chunk with a 4xx (thrown).
1213
+ */
1214
+ async executeBatch(appId, databaseId, operationName, batch) {
1215
+ return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
1216
+ }
1217
+ /**
1218
+ * Make a request using a specific JWT token instead of the CLI's credentials.
1219
+ * Used for executing operations as a different user.
1220
+ */
1221
+ async requestWithToken(path, token, options = {}) {
1222
+ const credentials = await this.ensureAuthenticated();
1223
+ const url = `${credentials.serverUrl}${path}`;
1224
+ const headers = {
1225
+ Authorization: `Bearer ${token}`,
1226
+ "Content-Type": "application/json",
1227
+ ...(options.headers || {}),
1228
+ };
1229
+ const response = await fetchWithTLS(url, { ...options, headers });
1230
+ const text = await response.text();
1231
+ if (!response.ok) {
1232
+ const parsed = parseErrorResponse(response, text, path);
1233
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
1234
+ }
1235
+ return text ? JSON.parse(text) : null;
1236
+ }
1237
+ // ============================================
1238
+ // DATABASE INDEXES
1239
+ // ============================================
1240
+ async listDatabaseIndexes(appId, databaseId, modelName) {
1241
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/indexes`, modelName ? { modelName } : undefined);
1242
+ }
1243
+ async registerDatabaseIndex(appId, databaseId, data) {
1244
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/register`, data);
1245
+ }
1246
+ async dropDatabaseIndex(appId, databaseId, data) {
1247
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
1248
+ }
1249
+ /**
1250
+ * Issue #974: back-provision an existing database instance's schema-declared
1251
+ * unique indexes by reading its type schema and registering any declared-but-
1252
+ * missing unique index. Idempotent.
1253
+ */
1254
+ async reindexDatabaseFromSchema(appId, databaseId) {
1255
+ return this.post(`/app/${appId}/api/databases/${databaseId}/reindex`, {});
1256
+ }
1257
+ // ============================================
1258
+ // DATABASE TYPE CONFIGS
717
1259
  // ============================================
718
- async listDatabaseGroupPermissions(appId, databaseId) {
719
- return this.get(`/app/${appId}/api/databases/${databaseId}/group-permissions`);
1260
+ async listDatabaseTypeConfigs(appId) {
1261
+ return this.get(`/app/${appId}/api/databases/types`);
1262
+ }
1263
+ async getDatabaseTypeConfig(appId, databaseType) {
1264
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1265
+ }
1266
+ async createDatabaseTypeConfig(appId, data) {
1267
+ return this.post(`/app/${appId}/api/databases/types`, data);
1268
+ }
1269
+ async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1270
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1271
+ const qs = new URLSearchParams();
1272
+ if (options?.dryRun)
1273
+ qs.set("dryRun", "true");
1274
+ if (options?.acceptWarnings)
1275
+ qs.set("acceptWarnings", "true");
1276
+ const query = qs.toString();
1277
+ const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
1278
+ return this.patch(path, body);
1279
+ }
1280
+ async deleteDatabaseTypeConfig(appId, databaseType, options) {
1281
+ const query = options?.force ? "?force=true" : "";
1282
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
1283
+ }
1284
+ /**
1285
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
1286
+ * existing ops + DO field introspection. Read-only — does NOT persist.
1287
+ */
1288
+ async scaffoldDatabaseTypeSchema(appId, databaseType) {
1289
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
720
1290
  }
721
- async grantDatabaseGroupPermission(appId, databaseId, data) {
722
- return this.post(`/app/${appId}/api/databases/${databaseId}/group-permissions`, data);
1291
+ // ============================================
1292
+ // DATABASE TYPE OPERATIONS
1293
+ // ============================================
1294
+ async listDatabaseTypeOperations(appId, databaseType) {
1295
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
1296
+ }
1297
+ async getDatabaseTypeOperation(appId, databaseType, name) {
1298
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
1299
+ }
1300
+ async createDatabaseTypeOperation(appId, databaseType, data,
1301
+ // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
1302
+ // without persisting — used by `sync push --dry-run` to surface gate
1303
+ // failures the real push would hit. `schemaOverride` lets the gate run
1304
+ // against the schema the SAME push is about to land (so an op that depends
1305
+ // on a new field isn't falsely rejected against the stale stored schema).
1306
+ //
1307
+ // Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
1308
+ // does not exist on the server yet, so the server's "access required"
1309
+ // check has no `defaultAccess` to fall back on. Thread the TOML-declared
1310
+ // type-level `defaultAccess` so an op that omits `access` (intending to
1311
+ // inherit it after the type is created) isn't falsely rejected during the
1312
+ // dry-run. Server only trusts this on the fresh-type dry-run path.
1313
+ options) {
1314
+ const query = options?.dryRun ? "?dryRun=true" : "";
1315
+ let body = data;
1316
+ if (options?.dryRun && options.schemaOverride !== undefined) {
1317
+ body = { ...body, schemaOverride: options.schemaOverride };
1318
+ }
1319
+ if (options?.dryRun && options.defaultAccess !== undefined) {
1320
+ body = { ...body, defaultAccess: options.defaultAccess };
1321
+ }
1322
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
1323
+ }
1324
+ async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
1325
+ // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
1326
+ // `schemaOverride` gates against the schema the same push is landing.
1327
+ options) {
1328
+ let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
1329
+ if (options?.dryRun && options.schemaOverride !== undefined) {
1330
+ body = { ...body, schemaOverride: options.schemaOverride };
1331
+ }
1332
+ const query = options?.dryRun ? "?dryRun=true" : "";
1333
+ return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
1334
+ }
1335
+ async deleteDatabaseTypeOperation(appId, databaseType, name) {
1336
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
723
1337
  }
724
- async revokeDatabaseGroupPermission(appId, databaseId, groupType, groupId) {
725
- return this.delete(`/app/${appId}/api/databases/${databaseId}/group-permissions/${groupType}/${groupId}`);
1338
+ // ============================================
1339
+ // DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
1340
+ // ============================================
1341
+ //
1342
+ // Transparent passthroughs against the server's type-scoped subscription
1343
+ // routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
1344
+ // The wire format is authoritative in the controller
1345
+ // (`database-type-subscriptions-controller.ts`): create requires
1346
+ // `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
1347
+ // (CEL); optional `description`, `select` (string[]), `emit`
1348
+ // (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
1349
+ //
1350
+ // The list endpoint wraps results in `{ items: [...] }` (unlike operations,
1351
+ // which returns a bare array). We unwrap here so callers get an array,
1352
+ // matching `listDatabaseTypeOperations`.
1353
+ async listDatabaseTypeSubscriptions(appId, databaseType) {
1354
+ const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
1355
+ if (Array.isArray(result))
1356
+ return result;
1357
+ return Array.isArray(result?.items) ? result.items : [];
1358
+ }
1359
+ async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1360
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
1361
+ }
1362
+ async createDatabaseTypeSubscription(appId, databaseType, data) {
1363
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
1364
+ }
1365
+ async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data) {
1366
+ return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, data);
1367
+ }
1368
+ async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1369
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
726
1370
  }
727
1371
  // ============================================
728
1372
  // GROUPS
@@ -754,8 +1398,11 @@ export class ApiClient {
754
1398
  async removeGroupMember(appId, groupType, groupId, userId) {
755
1399
  return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
756
1400
  }
757
- async updateGroupMemberRole(appId, groupType, groupId, userId, data) {
758
- return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`, data);
1401
+ // ============================================
1402
+ // GROUP RESOURCE LISTINGS
1403
+ // ============================================
1404
+ async listGroupDocuments(appId, groupType, groupId) {
1405
+ return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
759
1406
  }
760
1407
  // ============================================
761
1408
  // USER MEMBERSHIPS
@@ -763,6 +1410,391 @@ export class ApiClient {
763
1410
  async listUserMemberships(appId, userId) {
764
1411
  return this.get(`/app/${appId}/api/users/${userId}/memberships`);
765
1412
  }
1413
+ // ============================================
1414
+ // DOCUMENT GROUP PERMISSIONS
1415
+ // ============================================
1416
+ async listDocumentGroupPermissions(appId, documentId) {
1417
+ return this.get(`/app/${appId}/api/documents/${documentId}/group-permissions`);
1418
+ }
1419
+ async grantDocumentGroupPermission(appId, documentId, data) {
1420
+ return this.post(`/app/${appId}/api/documents/${documentId}/group-permissions`, data);
1421
+ }
1422
+ async revokeDocumentGroupPermission(appId, documentId, groupType, groupId) {
1423
+ return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
1424
+ }
1425
+ // ============================================
1426
+ // COLLECTIONS
1427
+ // ============================================
1428
+ async listCollections(appId, params) {
1429
+ const qs = new URLSearchParams();
1430
+ if (params?.limit)
1431
+ qs.set("limit", String(params.limit));
1432
+ if (params?.cursor)
1433
+ qs.set("cursor", params.cursor);
1434
+ const q = qs.toString();
1435
+ const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1436
+ return {
1437
+ items: result?.items ?? [],
1438
+ nextCursor: result?.cursor ?? null,
1439
+ };
1440
+ }
1441
+ async listAllCollections(appId, params) {
1442
+ const qs = new URLSearchParams();
1443
+ if (params?.limit)
1444
+ qs.set("limit", String(params.limit));
1445
+ if (params?.cursor)
1446
+ qs.set("cursor", params.cursor);
1447
+ const q = qs.toString();
1448
+ const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1449
+ return {
1450
+ items: result?.items ?? [],
1451
+ nextCursor: result?.cursor ?? null,
1452
+ };
1453
+ }
1454
+ async createCollection(appId, data) {
1455
+ return this.post(`/app/${appId}/api/collections`, data);
1456
+ }
1457
+ async getCollection(appId, collectionId) {
1458
+ return this.get(`/app/${appId}/api/collections/${collectionId}`);
1459
+ }
1460
+ async updateCollection(appId, collectionId, data) {
1461
+ return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
1462
+ }
1463
+ async deleteCollection(appId, collectionId) {
1464
+ return this.delete(`/app/${appId}/api/collections/${collectionId}`);
1465
+ }
1466
+ // ============================================
1467
+ // COLLECTION DOCUMENTS
1468
+ // ============================================
1469
+ async listCollectionDocuments(appId, collectionId, params) {
1470
+ const qs = new URLSearchParams();
1471
+ if (params?.limit)
1472
+ qs.set("limit", String(params.limit));
1473
+ if (params?.cursor)
1474
+ qs.set("cursor", params.cursor);
1475
+ const q = qs.toString();
1476
+ const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1477
+ return {
1478
+ items: result?.items ?? [],
1479
+ nextCursor: result?.cursor ?? null,
1480
+ };
1481
+ }
1482
+ async addCollectionDocument(appId, collectionId, data) {
1483
+ return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
1484
+ }
1485
+ async removeCollectionDocument(appId, collectionId, documentId) {
1486
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
1487
+ }
1488
+ async listCollectionsForDocument(appId, documentId, params) {
1489
+ const qs = new URLSearchParams();
1490
+ if (params?.limit)
1491
+ qs.set("limit", String(params.limit));
1492
+ if (params?.cursor)
1493
+ qs.set("cursor", params.cursor);
1494
+ const q = qs.toString();
1495
+ const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
1496
+ return {
1497
+ items: result?.items ?? [],
1498
+ nextCursor: result?.cursor ?? null,
1499
+ };
1500
+ }
1501
+ // ============================================
1502
+ // COLLECTION ACCESS (GROUPS + MEMBERS)
1503
+ // ============================================
1504
+ async getCollectionAccess(appId, collectionId) {
1505
+ return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
1506
+ }
1507
+ async grantCollectionGroupPermission(appId, collectionId, data) {
1508
+ return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
1509
+ }
1510
+ async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
1511
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
1512
+ }
1513
+ async addCollectionMember(appId, collectionId, data) {
1514
+ return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
1515
+ }
1516
+ async removeCollectionMember(appId, collectionId, userId) {
1517
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
1518
+ }
1519
+ // ============================================
1520
+ // COLLECTION TYPE CONFIGS
1521
+ // ============================================
1522
+ async listCollectionTypeConfigs(appId) {
1523
+ return this.get(`/app/${appId}/api/collection-type-configs`);
1524
+ }
1525
+ async getCollectionTypeConfig(appId, collectionType) {
1526
+ return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
1527
+ }
1528
+ async createCollectionTypeConfig(appId, data) {
1529
+ return this.post(`/app/${appId}/api/collection-type-configs`, data);
1530
+ }
1531
+ async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
1532
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1533
+ return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
1534
+ }
1535
+ async deleteCollectionTypeConfig(appId, collectionType) {
1536
+ return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
1537
+ }
1538
+ // ============================================
1539
+ // METADATA CATEGORY CONFIGS (issue #1304, P-B)
1540
+ // ============================================
1541
+ /**
1542
+ * List all metadata category configs for an app. The route returns
1543
+ * `{ configs: [...] }`; unwrap to a bare array to match the other
1544
+ * `list*Configs` helpers.
1545
+ */
1546
+ async listMetadataCategoryConfigs(appId) {
1547
+ const res = await this.get(`/app/${appId}/api/metadata-categories`);
1548
+ return Array.isArray(res?.configs) ? res.configs : [];
1549
+ }
1550
+ async getMetadataCategoryConfig(appId, resourceType, category) {
1551
+ return this.get(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
1552
+ }
1553
+ /**
1554
+ * Create or replace a metadata category config (idempotent upsert via the
1555
+ * path-addressed PUT route). `resourceType` / `category` identify the config;
1556
+ * the body carries `schema` / `readRule` / `writeRule` / `description`.
1557
+ */
1558
+ async upsertMetadataCategoryConfig(appId, resourceType, category, data) {
1559
+ return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, data);
1560
+ }
1561
+ // ============================================
1562
+ // ACCESS RULE SETS
1563
+ // ============================================
1564
+ async listRuleSets(appId, params) {
1565
+ return this.get(`/app/${appId}/api/rule-sets`, params);
1566
+ }
1567
+ async createRuleSet(appId, data) {
1568
+ return this.post(`/app/${appId}/api/rule-sets`, data);
1569
+ }
1570
+ async getRuleSet(appId, ruleSetId) {
1571
+ return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
1572
+ }
1573
+ async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
1574
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1575
+ return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
1576
+ }
1577
+ async deleteRuleSet(appId, ruleSetId) {
1578
+ return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
1579
+ }
1580
+ async getRuleSetSchema(appId) {
1581
+ return this.get(`/app/${appId}/api/rule-sets/schema`);
1582
+ }
1583
+ async getRuleSetResourceTypes(appId) {
1584
+ return this.get(`/app/${appId}/api/rule-sets/resource-types`);
1585
+ }
1586
+ async testRuleSet(appId, ruleSetId, data) {
1587
+ return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
1588
+ }
1589
+ async debugRuleSet(appId, data) {
1590
+ return this.post(`/app/${appId}/api/rule-sets/debug`, data);
1591
+ }
1592
+ // ============================================
1593
+ // GROUP TYPE CONFIGS
1594
+ // ============================================
1595
+ async listGroupTypeConfigs(appId) {
1596
+ return this.get(`/app/${appId}/api/group-type-configs`);
1597
+ }
1598
+ async getGroupTypeConfig(appId, groupType) {
1599
+ return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
1600
+ }
1601
+ async createGroupTypeConfig(appId, data) {
1602
+ return this.post(`/app/${appId}/api/group-type-configs`, data);
1603
+ }
1604
+ async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
1605
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1606
+ return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
1607
+ }
1608
+ async deleteGroupTypeConfig(appId, groupType) {
1609
+ return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
1610
+ }
1611
+ // ============================================
1612
+ // DOCUMENT EXPORT / IMPORT
1613
+ // ============================================
1614
+ async exportDocumentState(appId, documentId) {
1615
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
1616
+ }
1617
+ async importDocumentState(appId, documentId, stateBase64) {
1618
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
1619
+ }
1620
+ async getDocument(appId, documentId) {
1621
+ return this.get(`/app/${appId}/api/documents/${documentId}`);
1622
+ }
1623
+ async createDocument(appId, data) {
1624
+ return this.post(`/app/${appId}/api/documents`, data);
1625
+ }
1626
+ async listDocumentPermissions(appId, documentId) {
1627
+ return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
1628
+ }
1629
+ async grantDocumentPermission(appId, documentId, permissions) {
1630
+ return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
1631
+ }
1632
+ async listDocumentInvitations(appId, documentId) {
1633
+ return this.get(`/app/${appId}/api/documents/${documentId}/invitations`);
1634
+ }
1635
+ async listDocumentBlobs(appId, documentId) {
1636
+ return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
1637
+ }
1638
+ async downloadBlob(appId, documentId, blobId) {
1639
+ const credentials = await this.ensureAuthenticated();
1640
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
1641
+ const headers = {
1642
+ Authorization: `Bearer ${credentials.accessToken}`,
1643
+ };
1644
+ if (credentials.globalAdminAppId) {
1645
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1646
+ }
1647
+ const response = await fetchWithTLS(url, { headers });
1648
+ if (!response.ok) {
1649
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
1650
+ }
1651
+ const arrayBuffer = await response.arrayBuffer();
1652
+ return Buffer.from(arrayBuffer);
1653
+ }
1654
+ async uploadBlob(appId, documentId, blobId, data, meta) {
1655
+ const credentials = await this.ensureAuthenticated();
1656
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
1657
+ const headers = {
1658
+ Authorization: `Bearer ${credentials.accessToken}`,
1659
+ "Content-Type": meta.contentType,
1660
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
1661
+ "X-Blob-Size": String(data.length),
1662
+ "X-Blob-Sha256": meta.sha256,
1663
+ };
1664
+ if (credentials.globalAdminAppId) {
1665
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1666
+ }
1667
+ const response = await fetchWithTLS(url, {
1668
+ method: "PUT",
1669
+ headers,
1670
+ body: data,
1671
+ });
1672
+ if (!response.ok) {
1673
+ const text = await response.text();
1674
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
1675
+ }
1676
+ return response.json();
1677
+ }
1678
+ async listDocumentAliases(appId, documentId) {
1679
+ return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
1680
+ }
1681
+ async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
1682
+ return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
1683
+ documentId,
1684
+ userId: ownerUserId,
1685
+ mustNotExist,
1686
+ });
1687
+ }
1688
+ async findUserByEmail(appId, email) {
1689
+ try {
1690
+ const result = await this.get(`/app/${appId}/api/users`, { email });
1691
+ const items = result?.items || (Array.isArray(result) ? result : []);
1692
+ return items[0] || null;
1693
+ }
1694
+ catch {
1695
+ return null;
1696
+ }
1697
+ }
1698
+ async listAdminDocuments(appId, userId) {
1699
+ const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
1700
+ return result?.documents || result || [];
1701
+ }
1702
+ // ============================================
1703
+ // DATABASE EXPORT / IMPORT
1704
+ // ============================================
1705
+ async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
1706
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/save`, {
1707
+ modelName,
1708
+ id,
1709
+ data,
1710
+ });
1711
+ }
1712
+ async batchDatabaseRecords(appId, databaseId, operations) {
1713
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
1714
+ }
1715
+ async deleteDatabaseRecord(appId, databaseId, modelName, id) {
1716
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/delete`, { modelName, id });
1717
+ }
1718
+ async batchDeleteDatabaseRecords(appId, databaseId, operations) {
1719
+ return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
1720
+ }
1721
+ async listDatabaseUniqueConstraints(appId, databaseId) {
1722
+ const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
1723
+ return result?.constraints || result || [];
1724
+ }
1725
+ async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
1726
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
1727
+ }
1728
+ // ============================================
1729
+ // BLOB BUCKETS
1730
+ // ============================================
1731
+ async listBlobBuckets(appId) {
1732
+ return this.get(`/app/${appId}/api/blob-buckets`);
1733
+ }
1734
+ async createBlobBucket(appId, payload) {
1735
+ return this.post(`/app/${appId}/api/blob-buckets`, payload);
1736
+ }
1737
+ async getBlobBucket(appId, bucketIdOrKey) {
1738
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1739
+ }
1740
+ async updateBlobBucket(appId, bucketIdOrKey, payload) {
1741
+ return this.patch(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`, payload);
1742
+ }
1743
+ async deleteBlobBucket(appId, bucketIdOrKey) {
1744
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1745
+ }
1746
+ async listBucketBlobs(appId, bucketIdOrKey, params) {
1747
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
1748
+ }
1749
+ async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
1750
+ const credentials = await this.ensureAuthenticated();
1751
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
1752
+ const headers = {
1753
+ Authorization: `Bearer ${credentials.accessToken}`,
1754
+ "Content-Type": meta.contentType,
1755
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
1756
+ };
1757
+ if (meta.tags && meta.tags.length > 0) {
1758
+ headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
1759
+ }
1760
+ if (credentials.globalAdminAppId) {
1761
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1762
+ }
1763
+ const response = await fetchWithTLS(url, {
1764
+ method: "POST",
1765
+ headers,
1766
+ body: data,
1767
+ });
1768
+ if (!response.ok) {
1769
+ const text = await response.text();
1770
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
1771
+ }
1772
+ return response.json();
1773
+ }
1774
+ async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
1775
+ const credentials = await this.ensureAuthenticated();
1776
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
1777
+ const headers = {
1778
+ Authorization: `Bearer ${credentials.accessToken}`,
1779
+ };
1780
+ if (credentials.globalAdminAppId) {
1781
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1782
+ }
1783
+ const response = await fetchWithTLS(url, { headers });
1784
+ if (!response.ok) {
1785
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
1786
+ }
1787
+ const arrayBuffer = await response.arrayBuffer();
1788
+ return Buffer.from(arrayBuffer);
1789
+ }
1790
+ async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
1791
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
1792
+ }
1793
+ async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
1794
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
1795
+ expiresInSeconds: expiresInSeconds || 300,
1796
+ });
1797
+ }
766
1798
  }
767
1799
  // Export a singleton instance
768
1800
  export const apiClient = new ApiClient();