primitive-admin 1.0.61 → 1.0.62

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 (300) hide show
  1. package/README.md +125 -69
  2. package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
  3. package/dist/bin/primitive.js +25 -14
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +36 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +49 -188
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +13 -153
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/collection-type-configs.js +14 -100
  12. package/dist/src/commands/collection-type-configs.js.map +1 -1
  13. package/dist/src/commands/collections.js +47 -8
  14. package/dist/src/commands/collections.js.map +1 -1
  15. package/dist/src/commands/config.d.ts +46 -0
  16. package/dist/src/commands/config.js +479 -0
  17. package/dist/src/commands/config.js.map +1 -0
  18. package/dist/src/commands/connections.js +12 -7
  19. package/dist/src/commands/connections.js.map +1 -1
  20. package/dist/src/commands/cron-triggers.js +84 -176
  21. package/dist/src/commands/cron-triggers.js.map +1 -1
  22. package/dist/src/commands/database-type-configs.d.ts +2 -0
  23. package/dist/src/commands/database-type-configs.js +171 -0
  24. package/dist/src/commands/database-type-configs.js.map +1 -0
  25. package/dist/src/commands/database-types.js +4 -4
  26. package/dist/src/commands/database-types.js.map +1 -1
  27. package/dist/src/commands/databases.d.ts +63 -0
  28. package/dist/src/commands/databases.js +649 -87
  29. package/dist/src/commands/databases.js.map +1 -1
  30. package/dist/src/commands/documents.js +869 -12
  31. package/dist/src/commands/documents.js.map +1 -1
  32. package/dist/src/commands/email-templates.js +18 -125
  33. package/dist/src/commands/email-templates.js.map +1 -1
  34. package/dist/src/commands/env.d.ts +12 -1
  35. package/dist/src/commands/env.js +98 -25
  36. package/dist/src/commands/env.js.map +1 -1
  37. package/dist/src/commands/feature-flags.d.ts +14 -0
  38. package/dist/src/commands/feature-flags.js +116 -0
  39. package/dist/src/commands/feature-flags.js.map +1 -0
  40. package/dist/src/commands/group-type-configs.js +14 -109
  41. package/dist/src/commands/group-type-configs.js.map +1 -1
  42. package/dist/src/commands/init.d.ts +9 -1
  43. package/dist/src/commands/init.js +1056 -277
  44. package/dist/src/commands/init.js.map +1 -1
  45. package/dist/src/commands/integrations.js +136 -421
  46. package/dist/src/commands/integrations.js.map +1 -1
  47. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  48. package/dist/src/commands/metadata-category-configs.js +112 -0
  49. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  50. package/dist/src/commands/metadata.js +33 -141
  51. package/dist/src/commands/metadata.js.map +1 -1
  52. package/dist/src/commands/prompts.js +206 -563
  53. package/dist/src/commands/prompts.js.map +1 -1
  54. package/dist/src/commands/rule-sets.d.ts +1 -1
  55. package/dist/src/commands/rule-sets.js +16 -132
  56. package/dist/src/commands/rule-sets.js.map +1 -1
  57. package/dist/src/commands/scripts.js +67 -234
  58. package/dist/src/commands/scripts.js.map +1 -1
  59. package/dist/src/commands/sessions.js +17 -5
  60. package/dist/src/commands/sessions.js.map +1 -1
  61. package/dist/src/commands/sync-app-settings.d.ts +113 -70
  62. package/dist/src/commands/sync-app-settings.js +193 -242
  63. package/dist/src/commands/sync-app-settings.js.map +1 -1
  64. package/dist/src/commands/sync.d.ts +1932 -49
  65. package/dist/src/commands/sync.js +9521 -1769
  66. package/dist/src/commands/sync.js.map +1 -1
  67. package/dist/src/commands/tokens.js +8 -2
  68. package/dist/src/commands/tokens.js.map +1 -1
  69. package/dist/src/commands/users.js +92 -1
  70. package/dist/src/commands/users.js.map +1 -1
  71. package/dist/src/commands/vars.js +44 -58
  72. package/dist/src/commands/vars.js.map +1 -1
  73. package/dist/src/commands/webhooks.js +318 -156
  74. package/dist/src/commands/webhooks.js.map +1 -1
  75. package/dist/src/commands/workflows.d.ts +49 -12
  76. package/dist/src/commands/workflows.js +652 -1086
  77. package/dist/src/commands/workflows.js.map +1 -1
  78. package/dist/src/lib/access-rule-display.d.ts +21 -0
  79. package/dist/src/lib/access-rule-display.js +34 -0
  80. package/dist/src/lib/access-rule-display.js.map +1 -0
  81. package/dist/src/lib/api-client.d.ts +541 -61
  82. package/dist/src/lib/api-client.js +559 -102
  83. package/dist/src/lib/api-client.js.map +1 -1
  84. package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
  85. package/dist/src/lib/app-settings-descriptor.js +403 -82
  86. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  87. package/dist/src/lib/canonical-json.d.ts +12 -0
  88. package/dist/src/lib/canonical-json.js +35 -0
  89. package/dist/src/lib/canonical-json.js.map +1 -0
  90. package/dist/src/lib/cli-manifest.d.ts +8 -0
  91. package/dist/src/lib/cli-manifest.js +1 -0
  92. package/dist/src/lib/cli-manifest.js.map +1 -1
  93. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
  94. package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
  95. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  96. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
  97. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
  98. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  99. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  100. package/dist/src/lib/config-object-descriptor.js +658 -0
  101. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  102. package/dist/src/lib/config-payload.d.ts +85 -0
  103. package/dist/src/lib/config-payload.js +116 -0
  104. package/dist/src/lib/config-payload.js.map +1 -0
  105. package/dist/src/lib/config-surface.d.ts +130 -0
  106. package/dist/src/lib/config-surface.js +300 -0
  107. package/dist/src/lib/config-surface.js.map +1 -0
  108. package/dist/src/lib/credentials-store.d.ts +10 -0
  109. package/dist/src/lib/credentials-store.js +23 -0
  110. package/dist/src/lib/credentials-store.js.map +1 -1
  111. package/dist/src/lib/data-input.d.ts +23 -0
  112. package/dist/src/lib/data-input.js +50 -0
  113. package/dist/src/lib/data-input.js.map +1 -0
  114. package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
  115. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  116. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
  117. package/dist/src/lib/env-resolver-core.d.ts +147 -0
  118. package/dist/src/lib/env-resolver-core.js +265 -0
  119. package/dist/src/lib/env-resolver-core.js.map +1 -0
  120. package/dist/src/lib/env-resolver.d.ts +28 -6
  121. package/dist/src/lib/env-resolver.js +44 -32
  122. package/dist/src/lib/env-resolver.js.map +1 -1
  123. package/dist/src/lib/generated-allowlist.js +12 -0
  124. package/dist/src/lib/generated-allowlist.js.map +1 -1
  125. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  126. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  127. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  128. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  129. package/dist/src/lib/generated-template-lint.js +624 -0
  130. package/dist/src/lib/generated-template-lint.js.map +1 -0
  131. package/dist/src/lib/init-adopt.d.ts +16 -0
  132. package/dist/src/lib/init-adopt.js +34 -0
  133. package/dist/src/lib/init-adopt.js.map +1 -0
  134. package/dist/src/lib/init-assets.d.ts +39 -0
  135. package/dist/src/lib/init-assets.js +97 -0
  136. package/dist/src/lib/init-assets.js.map +1 -0
  137. package/dist/src/lib/init-config.d.ts +44 -5
  138. package/dist/src/lib/init-config.js +80 -7
  139. package/dist/src/lib/init-config.js.map +1 -1
  140. package/dist/src/lib/init-ios-links.d.ts +50 -0
  141. package/dist/src/lib/init-ios-links.js +153 -0
  142. package/dist/src/lib/init-ios-links.js.map +1 -0
  143. package/dist/src/lib/init-plan.d.ts +80 -0
  144. package/dist/src/lib/init-plan.js +95 -0
  145. package/dist/src/lib/init-plan.js.map +1 -0
  146. package/dist/src/lib/init-production-env.d.ts +48 -0
  147. package/dist/src/lib/init-production-env.js +59 -0
  148. package/dist/src/lib/init-production-env.js.map +1 -0
  149. package/dist/src/lib/init-schema.d.ts +74 -0
  150. package/dist/src/lib/init-schema.js +358 -0
  151. package/dist/src/lib/init-schema.js.map +1 -0
  152. package/dist/src/lib/init-xcode.d.ts +33 -0
  153. package/dist/src/lib/init-xcode.js +114 -0
  154. package/dist/src/lib/init-xcode.js.map +1 -0
  155. package/dist/src/lib/integration-request-config.d.ts +30 -0
  156. package/dist/src/lib/integration-request-config.js +145 -0
  157. package/dist/src/lib/integration-request-config.js.map +1 -0
  158. package/dist/src/lib/local-state.d.ts +55 -0
  159. package/dist/src/lib/local-state.js +167 -0
  160. package/dist/src/lib/local-state.js.map +1 -0
  161. package/dist/src/lib/log-inspection.d.ts +568 -0
  162. package/dist/src/lib/log-inspection.js +639 -0
  163. package/dist/src/lib/log-inspection.js.map +1 -0
  164. package/dist/src/lib/migration-nag.d.ts +2 -2
  165. package/dist/src/lib/migration-nag.js +3 -3
  166. package/dist/src/lib/object-status-filter.d.ts +22 -0
  167. package/dist/src/lib/object-status-filter.js +45 -0
  168. package/dist/src/lib/object-status-filter.js.map +1 -0
  169. package/dist/src/lib/output.d.ts +24 -1
  170. package/dist/src/lib/output.js +42 -1
  171. package/dist/src/lib/output.js.map +1 -1
  172. package/dist/src/lib/package-manager.d.ts +140 -0
  173. package/dist/src/lib/package-manager.js +305 -0
  174. package/dist/src/lib/package-manager.js.map +1 -0
  175. package/dist/src/lib/paginate.d.ts +50 -0
  176. package/dist/src/lib/paginate.js +53 -0
  177. package/dist/src/lib/paginate.js.map +1 -1
  178. package/dist/src/lib/platform-owned.d.ts +29 -3
  179. package/dist/src/lib/platform-owned.js +42 -3
  180. package/dist/src/lib/platform-owned.js.map +1 -1
  181. package/dist/src/lib/project-config.js +13 -5
  182. package/dist/src/lib/project-config.js.map +1 -1
  183. package/dist/src/lib/query-operators.d.ts +1 -1
  184. package/dist/src/lib/query-operators.js +1 -1
  185. package/dist/src/lib/record-filter.d.ts +18 -0
  186. package/dist/src/lib/record-filter.js +55 -0
  187. package/dist/src/lib/record-filter.js.map +1 -0
  188. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  189. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  190. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  191. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  192. package/dist/src/lib/resolve-init-server.js +77 -0
  193. package/dist/src/lib/resolve-init-server.js.map +1 -0
  194. package/dist/src/lib/resolve-platform.d.ts +43 -14
  195. package/dist/src/lib/resolve-platform.js +74 -12
  196. package/dist/src/lib/resolve-platform.js.map +1 -1
  197. package/dist/src/lib/run-status.d.ts +19 -0
  198. package/dist/src/lib/run-status.generated.d.ts +39 -0
  199. package/dist/src/lib/run-status.generated.js +66 -0
  200. package/dist/src/lib/run-status.generated.js.map +1 -0
  201. package/dist/src/lib/run-status.js +19 -0
  202. package/dist/src/lib/run-status.js.map +1 -0
  203. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  204. package/dist/src/lib/server-text-normalization.js +90 -0
  205. package/dist/src/lib/server-text-normalization.js.map +1 -0
  206. package/dist/src/lib/server-url.d.ts +22 -0
  207. package/dist/src/lib/server-url.js +33 -0
  208. package/dist/src/lib/server-url.js.map +1 -0
  209. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  210. package/dist/src/lib/signing-secret-status.js +116 -0
  211. package/dist/src/lib/signing-secret-status.js.map +1 -0
  212. package/dist/src/lib/skill-installer.js +6 -3
  213. package/dist/src/lib/skill-installer.js.map +1 -1
  214. package/dist/src/lib/snapshots.d.ts +2 -2
  215. package/dist/src/lib/snapshots.js +2 -2
  216. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
  217. package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
  218. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  219. package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
  220. package/dist/src/lib/swift-codegen/generator.js +181 -7
  221. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  222. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
  223. package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
  224. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
  225. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  226. package/dist/src/lib/sync-dir-selector.js +30 -0
  227. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  228. package/dist/src/lib/sync-paths.d.ts +39 -0
  229. package/dist/src/lib/sync-paths.js +70 -2
  230. package/dist/src/lib/sync-paths.js.map +1 -1
  231. package/dist/src/lib/sync-resource-types.d.ts +138 -31
  232. package/dist/src/lib/sync-resource-types.js +241 -47
  233. package/dist/src/lib/sync-resource-types.js.map +1 -1
  234. package/dist/src/lib/sync-selectors.d.ts +95 -0
  235. package/dist/src/lib/sync-selectors.js +228 -0
  236. package/dist/src/lib/sync-selectors.js.map +1 -0
  237. package/dist/src/lib/template.d.ts +62 -4
  238. package/dist/src/lib/template.js +147 -39
  239. package/dist/src/lib/template.js.map +1 -1
  240. package/dist/src/lib/test-case-keys.d.ts +29 -0
  241. package/dist/src/lib/test-case-keys.js +55 -0
  242. package/dist/src/lib/test-case-keys.js.map +1 -0
  243. package/dist/src/lib/test-case-variables.d.ts +15 -0
  244. package/dist/src/lib/test-case-variables.js +29 -0
  245. package/dist/src/lib/test-case-variables.js.map +1 -0
  246. package/dist/src/lib/toml-database-config.d.ts +4 -4
  247. package/dist/src/lib/toml-database-config.js +27 -34
  248. package/dist/src/lib/toml-database-config.js.map +1 -1
  249. package/dist/src/lib/toml-metadata-config.d.ts +9 -5
  250. package/dist/src/lib/toml-metadata-config.js +40 -45
  251. package/dist/src/lib/toml-metadata-config.js.map +1 -1
  252. package/dist/src/lib/toml-native-form.d.ts +1 -1
  253. package/dist/src/lib/toml-native-form.js +1 -1
  254. package/dist/src/lib/toml-params-validator.d.ts +1 -1
  255. package/dist/src/lib/toml-params-validator.js +1 -1
  256. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  257. package/dist/src/lib/toml-scalar-edit.js +283 -0
  258. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  259. package/dist/src/lib/user-selector.d.ts +24 -0
  260. package/dist/src/lib/user-selector.js +33 -0
  261. package/dist/src/lib/user-selector.js.map +1 -0
  262. package/dist/src/lib/version-check.d.ts +25 -0
  263. package/dist/src/lib/version-check.js +77 -8
  264. package/dist/src/lib/version-check.js.map +1 -1
  265. package/dist/src/lib/watch.d.ts +121 -0
  266. package/dist/src/lib/watch.js +169 -0
  267. package/dist/src/lib/watch.js.map +1 -0
  268. package/dist/src/lib/workflow-apply.d.ts +69 -45
  269. package/dist/src/lib/workflow-apply.js +99 -72
  270. package/dist/src/lib/workflow-apply.js.map +1 -1
  271. package/dist/src/lib/workflow-codegen/generator.js +4 -4
  272. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  273. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
  274. package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
  275. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  276. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  277. package/dist/src/lib/workflow-config-apply.js +137 -0
  278. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  279. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  280. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  281. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  282. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  283. package/dist/src/lib/workflow-defaults.js +41 -0
  284. package/dist/src/lib/workflow-defaults.js.map +1 -0
  285. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  286. package/dist/src/lib/workflow-include-preserve.js +286 -0
  287. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  288. package/dist/src/lib/workflow-payload.d.ts +31 -20
  289. package/dist/src/lib/workflow-payload.js +100 -110
  290. package/dist/src/lib/workflow-payload.js.map +1 -1
  291. package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
  292. package/dist/src/lib/workflow-toml-validator.js +141 -16
  293. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  294. package/dist/src/types/index.d.ts +78 -22
  295. package/dist/src/validators.d.ts +7 -7
  296. package/dist/src/validators.js +6 -6
  297. package/package.json +11 -6
  298. package/dist/src/commands/settings.d.ts +0 -15
  299. package/dist/src/commands/settings.js +0 -102
  300. package/dist/src/commands/settings.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
2
  import { fetchWithTLS } from "./fetch.js";
3
- import { paginateAll } from "./paginate.js";
3
+ import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
4
4
  import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
5
5
  export class ApiError extends Error {
6
6
  statusCode;
@@ -162,6 +162,45 @@ export class OpsExistError extends ApiError {
162
162
  return typeof d?.opCount === "number" ? d.opCount : 0;
163
163
  }
164
164
  }
165
+ /**
166
+ * Normalize a `databases operations execute` result to the CLI's one list
167
+ * envelope — but only when the result is a Durable Object query page
168
+ * (issue #2440).
169
+ *
170
+ * A registered operation is a list only sometimes. Of the shapes the server's
171
+ * operation dispatch can return, exactly two carry a top-level `data` array: a
172
+ * bare `query` and a `pipeline` whose `returnField` names a query step. A
173
+ * `count` (`{ count }`), an `aggregate` (`{ result }`), a mutation
174
+ * (`{ results }`), an `applyToQuery` (`{ matched, affected, failed, … }`) and a
175
+ * `returnField: "all"` pipeline (`{ steps: { … } }`) do not, so blanket
176
+ * normalization would corrupt them. This helper therefore tests the shape
177
+ * first and passes everything else through byte-for-byte — including the
178
+ * nested steps of a `returnField: "all"` pipeline, which are operation-defined
179
+ * objects the CLI must not reach into.
180
+ *
181
+ * Deliberately narrow, not a general-purpose predicate: the contract is
182
+ * specifically "an operations-execute result may be a DO query page".
183
+ * `executeDatabaseOperation()` is the only caller.
184
+ *
185
+ * The conversion itself is delegated to `normalizeCliListEnvelope()` so cursor
186
+ * handling, `prevCursor` dropping and `hasMore` derivation have exactly one
187
+ * implementation. That helper throws on an unrecognized shape — correct when
188
+ * the payload is supposed to be a list, wrong here, where the payload is the
189
+ * caller's own result — which is why the shape test has to run first. `_timing`
190
+ * (`--timing`) is re-attached afterwards, since it rides alongside the envelope
191
+ * rather than inside it.
192
+ */
193
+ export function normalizeDatabaseOperationResult(raw) {
194
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
195
+ return raw;
196
+ const page = raw;
197
+ if (!Array.isArray(page.data))
198
+ return raw;
199
+ const envelope = normalizeCliListEnvelope(page);
200
+ if (page._timing !== undefined)
201
+ envelope._timing = page._timing;
202
+ return envelope;
203
+ }
165
204
  export class ApiClient {
166
205
  credentials = null;
167
206
  constructor() {
@@ -378,6 +417,40 @@ export class ApiClient {
378
417
  async removeUser(appId, userId) {
379
418
  return this.delete(`/app/${appId}/api/users/${userId}`);
380
419
  }
420
+ /**
421
+ * #2803 — take a user's access away, or give it back. Distinct from
422
+ * `removeUser`, which detaches the membership: this blocks the person's auth
423
+ * in the app, revokes their sessions and tokens, and is reversible. Both
424
+ * routes already existed; nothing on the CLI reached them.
425
+ */
426
+ async setUserAvailability(appId, userId, action) {
427
+ return this.put(`/admin/api/apps/${appId}/users/${userId}/${action}`, {});
428
+ }
429
+ // ============================================
430
+ // FEATURE FLAGS (super-admin)
431
+ // ============================================
432
+ /**
433
+ * #2803 — the platform's feature flags. A server-owned admin toggle with a
434
+ * console but, until now, no CLI: the same "one control, both surfaces" rule
435
+ * the carrying types get.
436
+ */
437
+ async listFeatureFlags() {
438
+ return this.get(`/admin/api/flags`);
439
+ }
440
+ /**
441
+ * The flag itself, not the `{ flag }` envelope the route answers with —
442
+ * `feature-flags get` prints the key, the enabled state and the per-app
443
+ * overrides, and reading them off the envelope printed a blank row.
444
+ */
445
+ async getFeatureFlag(flagKey) {
446
+ const result = await this.get(`/admin/api/flags/${flagKey}`);
447
+ return result?.flag ?? result;
448
+ }
449
+ /** Same envelope, same unwrap: `enable`/`disable` print the updated flag. */
450
+ async setFeatureFlagEnabled(flagKey, enabled) {
451
+ const result = await this.put(`/admin/api/flags/${flagKey}`, { enabled });
452
+ return result?.flag ?? result;
453
+ }
381
454
  async updateUserRole(appId, userId, role) {
382
455
  return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
383
456
  }
@@ -467,9 +540,19 @@ export class ApiClient {
467
540
  nextCursor: result?.nextCursor ?? null,
468
541
  };
469
542
  }
543
+ /** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
470
544
  async getIntegration(appId, integrationId) {
471
545
  return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
472
546
  }
547
+ /**
548
+ * An integration's named configs. Test cases pin a config by NAME so the
549
+ * sidecar is portable across apps (#2769); this is the map pull and push
550
+ * resolve that name through, the same way prompts/workflows/scripts do.
551
+ */
552
+ async listIntegrationConfigs(appId, integrationId) {
553
+ const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/configs`);
554
+ return { items: result?.items ?? result ?? [] };
555
+ }
473
556
  async createIntegration(appId, payload) {
474
557
  return this.post(`/admin/api/apps/${appId}/integrations`, payload);
475
558
  }
@@ -477,8 +560,15 @@ export class ApiClient {
477
560
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
478
561
  return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
479
562
  }
563
+ /**
564
+ * Delete an integration. Archives it by default; `{ hard: true }` destroys
565
+ * the row instead, which is what `config push --prune` passes (#2803) so a
566
+ * pruned integration stops holding its key. The positional boolean is still
567
+ * accepted for the existing callers.
568
+ */
480
569
  async deleteIntegration(appId, integrationId, hard) {
481
- const path = hard
570
+ const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
571
+ const path = isHard
482
572
  ? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
483
573
  : `/admin/api/apps/${appId}/integrations/${integrationId}`;
484
574
  return this.delete(path);
@@ -494,19 +584,6 @@ export class ApiClient {
494
584
  const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
495
585
  return result?.items ?? [];
496
586
  }
497
- async listIntegrationSecrets(appId, integrationId, params) {
498
- const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
499
- return result?.items ?? [];
500
- }
501
- async addIntegrationSecret(appId, integrationId, payload) {
502
- const result = await this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, payload);
503
- return result?.secret ?? null;
504
- }
505
- async archiveIntegrationSecret(appId, integrationId, secretId) {
506
- return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets/${secretId}`, {
507
- status: "inactive",
508
- });
509
- }
510
587
  // ============================================
511
588
  // APP SECRETS
512
589
  // ============================================
@@ -583,15 +660,45 @@ export class ApiClient {
583
660
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
584
661
  return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
585
662
  }
586
- async deleteWebhook(appId, webhookId) {
587
- return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
663
+ /**
664
+ * Delete a webhook. Archives it by default; `hard` hard-deletes the row
665
+ * instead (#2232), which is what frees a slot under the per-app webhook cap
666
+ * and releases the webhook key for reuse. Same `?hard=true` verb the admin
667
+ * integrations delete uses.
668
+ */
669
+ async deleteWebhook(appId, webhookId, options) {
670
+ const query = options?.hard ? "?hard=true" : "";
671
+ return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}${query}`);
672
+ }
673
+ /**
674
+ * #2803 — the operational verb pair for a webhook. It posts to the
675
+ * enable/disable route rather than PATCHing `status`, which is server-owned
676
+ * and refused on every configuration write path.
677
+ */
678
+ async setWebhookAvailability(appId, webhookId, action) {
679
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
680
+ }
681
+ /** #2803 — the operational verb pair for a prompt (admin API). */
682
+ async setPromptAvailability(appId, promptId, action) {
683
+ return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
684
+ }
685
+ /** #2803 — the operational verb pair for an integration (admin API). */
686
+ async setIntegrationAvailability(appId, integrationId, action) {
687
+ return this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/${action}`, {});
588
688
  }
589
689
  async rotateWebhookSecret(appId, webhookId, payload) {
590
690
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
591
691
  }
592
692
  async listWebhookEvents(appId, webhookId, params) {
593
693
  const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
594
- return result;
694
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
695
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
696
+ return {
697
+ items: result?.items ?? [],
698
+ hasMore: result?.hasMore ?? !!nextCursor,
699
+ nextCursor,
700
+ cursor: nextCursor,
701
+ };
595
702
  }
596
703
  // ============================================
597
704
  // CONNECTIONS INSPECTION (#1968)
@@ -626,6 +733,17 @@ export class ApiClient {
626
733
  async testWebhook(appId, webhookId, payload) {
627
734
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
628
735
  }
736
+ /**
737
+ * Run a webhook's configured verifier over headers and a body captured from
738
+ * a real delivery, and report whether that signature checks out (#2445).
739
+ *
740
+ * `headers` is sent as ordered `[name, value]` pairs so a capture carrying a
741
+ * field name on more than one line survives the round trip. Nothing is
742
+ * delivered: no delivery record, no workflow run.
743
+ */
744
+ async verifyWebhook(appId, webhookId, payload) {
745
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/verify`, payload);
746
+ }
629
747
  // ============================================
630
748
  // NAMED LOCKS (#1518)
631
749
  // ============================================
@@ -661,14 +779,21 @@ export class ApiClient {
661
779
  async updateCronTrigger(appId, triggerId, payload) {
662
780
  return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
663
781
  }
664
- async deleteCronTrigger(appId, triggerId) {
665
- return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}`);
782
+ /**
783
+ * Retire a cron trigger. The default soft-deletes (writes the `archived`
784
+ * tombstone); `{ hard: true }` destroys the row, which is what
785
+ * `config push --prune` passes (#2803) so a pruned trigger stops consuming
786
+ * the per-app cap and holding its key.
787
+ */
788
+ async deleteCronTrigger(appId, triggerId, options = {}) {
789
+ const query = options.hard ? "?hard=true" : "";
790
+ return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}${query}`);
666
791
  }
667
- async pauseCronTrigger(appId, triggerId) {
668
- return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/pause`, {});
792
+ async disableCronTrigger(appId, triggerId) {
793
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
669
794
  }
670
- async resumeCronTrigger(appId, triggerId) {
671
- return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/resume`, {});
795
+ async enableCronTrigger(appId, triggerId) {
796
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
672
797
  }
673
798
  async testCronTrigger(appId, triggerId) {
674
799
  return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
@@ -716,6 +841,20 @@ export class ApiClient {
716
841
  async deleteResourceMetadata(appId, resourceType, resourceId, category) {
717
842
  return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
718
843
  }
844
+ /**
845
+ * Reverse-resolve a resource by a category's unique metadata value (issue
846
+ * #2137). A miss is a success with `resourceId: null`, never an error. The
847
+ * CLI authenticates as a console admin, so the app-level owner/admin bypass
848
+ * skips readRule evaluation.
849
+ */
850
+ async resolveResourceMetadata(appId, resourceType, category, key, value) {
851
+ return this.post(`/app/${appId}/api/metadata/resolve`, {
852
+ resourceType,
853
+ category,
854
+ key,
855
+ value,
856
+ });
857
+ }
719
858
  // ============================================
720
859
  // PROMPTS
721
860
  // ============================================
@@ -736,6 +875,12 @@ export class ApiClient {
736
875
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
737
876
  return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
738
877
  }
878
+ /**
879
+ * Retire a prompt, or destroy it.
880
+ *
881
+ * `hard` was always sent by both first-party clients and always ignored by
882
+ * the server; #2887 made it mean something — the plain call now archives.
883
+ */
739
884
  async deletePrompt(appId, promptId, hard) {
740
885
  const path = hard
741
886
  ? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
@@ -775,7 +920,7 @@ export class ApiClient {
775
920
  // The `Script` model is the authoring surface for `script` workflow
776
921
  // steps. A `Script` is a HEADER (name/description/activeConfigId); the
777
922
  // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
778
- // time. CLI `sync push` reads `transforms/*.rhai` files and reconciles
923
+ // time. CLI `config push` reads `transforms/*.rhai` files and reconciles
779
924
  // them: a new file → create script (mints a default config); a changed
780
925
  // file → create a new config + activate it (zero fan-out — referencing
781
926
  // workflows pick up the new body on their next run).
@@ -808,10 +953,28 @@ export class ApiClient {
808
953
  * workflows pick up the new body on their next run with no fan-out. The
809
954
  * config name is unique per script, so we mint a timestamped name.
810
955
  */
811
- async pushScriptBody(appId, scriptId, body) {
956
+ async pushScriptBody(appId, scriptId, body,
957
+ /**
958
+ * The active state the caller believes holds right now (#2731 B10).
959
+ * Creating an inactive config races nothing, so the guard sits on the
960
+ * ACTIVATION step: if the active config has moved since the caller read it,
961
+ * the server rejects with a 409 instead of overwriting a concurrent script
962
+ * change. It takes BOTH halves of "what is active", because a config's body
963
+ * can be edited in place without the active id changing — the id alone
964
+ * would let such an edit through. Omitted (e.g. `--force`) means activate
965
+ * unconditionally.
966
+ */
967
+ expectedActive) {
812
968
  const configName = `sync-${Date.now()}`;
813
969
  const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
814
- await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, {});
970
+ const activateBody = {};
971
+ if (expectedActive?.configId) {
972
+ activateBody.expectedActiveConfigId = expectedActive.configId;
973
+ }
974
+ if (expectedActive?.modifiedAt) {
975
+ activateBody.expectedActiveModifiedAt = expectedActive.modifiedAt;
976
+ }
977
+ await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, activateBody);
815
978
  return config;
816
979
  }
817
980
  /**
@@ -831,30 +994,54 @@ export class ApiClient {
831
994
  // ============================================
832
995
  // BLOCK TEST CASES (Prompts, Integrations, Workflows)
833
996
  // ============================================
834
- async listTestCases(appId, blockType, blockId) {
835
- const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`);
836
- return { items: result?.items ?? result ?? [] };
997
+ /**
998
+ * One page of a block's test cases. The endpoint paginates (default 50, cap
999
+ * 100) and reports `nextCursor`; callers that reconcile the sync tree must
1000
+ * drain every page before deleting or classifying anything (#2769).
1001
+ */
1002
+ async listTestCases(appId, blockType, blockId, params = {}) {
1003
+ const query = new URLSearchParams();
1004
+ if (params.limit !== undefined)
1005
+ query.set("limit", String(params.limit));
1006
+ if (params.cursor)
1007
+ query.set("cursor", params.cursor);
1008
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1009
+ const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases${suffix}`);
1010
+ return {
1011
+ items: result?.items ?? result ?? [],
1012
+ nextCursor: result?.nextCursor ?? undefined,
1013
+ };
837
1014
  }
838
1015
  async getTestCase(appId, blockType, blockId, testCaseId) {
839
1016
  return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
840
1017
  }
841
1018
  async createTestCase(appId, blockType, blockId, payload) {
842
- // Server expects JSON strings for these fields
1019
+ // #2769 the JSON fields travel PARSED. The handler stringifies them for
1020
+ // storage (`createBlockTestCase`) and `serializeBlockTestCase` parses them
1021
+ // on the way back out, so a client that stringifies first stores a JSON
1022
+ // string of a JSON string and reads back a quoted blob.
843
1023
  const serverPayload = {
844
1024
  name: payload.name,
845
- inputVariables: JSON.stringify(payload.inputVariables),
1025
+ inputVariables: payload.inputVariables,
846
1026
  configId: payload.configId,
847
1027
  evaluatorPromptId: payload.evaluatorPromptId,
848
1028
  evaluatorConfigId: payload.evaluatorConfigId,
849
1029
  };
1030
+ // #2896 — only when the caller has one: an older server rejects the key
1031
+ // outright, and push reads that 400 to learn what it is talking to.
1032
+ if (payload.key !== undefined)
1033
+ serverPayload.key = payload.key;
1034
+ if (payload.description !== undefined) {
1035
+ serverPayload.description = payload.description;
1036
+ }
850
1037
  if (payload.expectedOutputPattern) {
851
1038
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
852
1039
  }
853
1040
  if (payload.expectedOutputContains) {
854
- serverPayload.expectedOutputContains = JSON.stringify(payload.expectedOutputContains);
1041
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
855
1042
  }
856
1043
  if (payload.expectedJsonSubset) {
857
- serverPayload.expectedJsonSubset = JSON.stringify(payload.expectedJsonSubset);
1044
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
858
1045
  }
859
1046
  return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
860
1047
  }
@@ -862,21 +1049,23 @@ export class ApiClient {
862
1049
  const serverPayload = {};
863
1050
  if (payload.name !== undefined)
864
1051
  serverPayload.name = payload.name;
1052
+ if (payload.description !== undefined) {
1053
+ serverPayload.description = payload.description;
1054
+ }
1055
+ // #2769 — parsed on the wire (see `createTestCase`), and an explicit `null`
1056
+ // is how the authored file clears a field: the handler only touches keys
1057
+ // that are present, so an omitted key retains the previous value.
865
1058
  if (payload.inputVariables !== undefined) {
866
- serverPayload.inputVariables = JSON.stringify(payload.inputVariables);
1059
+ serverPayload.inputVariables = payload.inputVariables;
867
1060
  }
868
1061
  if (payload.expectedOutputPattern !== undefined) {
869
1062
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
870
1063
  }
871
1064
  if (payload.expectedOutputContains !== undefined) {
872
- serverPayload.expectedOutputContains = payload.expectedOutputContains
873
- ? JSON.stringify(payload.expectedOutputContains)
874
- : null;
1065
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
875
1066
  }
876
1067
  if (payload.expectedJsonSubset !== undefined) {
877
- serverPayload.expectedJsonSubset = payload.expectedJsonSubset
878
- ? JSON.stringify(payload.expectedJsonSubset)
879
- : null;
1068
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
880
1069
  }
881
1070
  if (payload.configId !== undefined)
882
1071
  serverPayload.configId = payload.configId;
@@ -886,6 +1075,8 @@ export class ApiClient {
886
1075
  if (payload.evaluatorConfigId !== undefined) {
887
1076
  serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
888
1077
  }
1078
+ if (payload.key !== undefined)
1079
+ serverPayload.key = payload.key;
889
1080
  return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
890
1081
  }
891
1082
  async deleteTestCase(appId, blockType, blockId, testCaseId) {
@@ -982,15 +1173,33 @@ export class ApiClient {
982
1173
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
983
1174
  return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
984
1175
  }
985
- async deleteWorkflow(appId, workflowId) {
986
- return this.delete(`/admin/api/apps/${appId}/workflows/${workflowId}`);
1176
+ /**
1177
+ * Retire a workflow, or destroy it (#2887).
1178
+ *
1179
+ * The plain call ARCHIVES: the definition row stays, so its runs, revisions
1180
+ * and test cases keep resolving. `{ hard: true }` performs the full cascade
1181
+ * and is what releases the `workflowKey` — the flag `config push --prune`
1182
+ * sends, for the same reason it sends it for webhooks and integrations.
1183
+ */
1184
+ async deleteWorkflow(appId, workflowId, options) {
1185
+ const path = `/admin/api/apps/${appId}/workflows/${workflowId}`;
1186
+ return this.delete(options?.hard ? `${path}?hard=true` : path);
1187
+ }
1188
+ /**
1189
+ * #2645 criterion 9 — the operational toggle. Deliberately NOT the config
1190
+ * PATCH: `config push` owns every configuration field, and taking a workflow
1191
+ * out of service must not touch one, or an operator's action would show up as
1192
+ * drift against the repo. Same route shape as `cron-triggers pause|resume`.
1193
+ */
1194
+ async disableWorkflow(appId, workflowId) {
1195
+ return this.post(`/app/${appId}/api/workflows/${workflowId}/disable`, {});
1196
+ }
1197
+ async enableWorkflow(appId, workflowId) {
1198
+ return this.post(`/app/${appId}/api/workflows/${workflowId}/enable`, {});
987
1199
  }
988
1200
  async updateWorkflowDraft(appId, workflowId, payload) {
989
1201
  return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
990
1202
  }
991
- async publishWorkflow(appId, workflowId) {
992
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
993
- }
994
1203
  async previewWorkflow(appId, workflowId, payload) {
995
1204
  return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
996
1205
  }
@@ -1003,8 +1212,27 @@ export class ApiClient {
1003
1212
  const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1004
1213
  return {
1005
1214
  items: result?.items ?? [],
1215
+ hasMore: result?.hasMore,
1006
1216
  nextCursor,
1007
1217
  cursor: nextCursor,
1218
+ resumeAfter: result?.resumeAfter,
1219
+ scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
1220
+ };
1221
+ }
1222
+ /**
1223
+ * Workflow runs started by one app user, across every workflow (#1967).
1224
+ * Backed by `GET /admin/api/apps/{appId}/users/{userId}/workflow-runs`,
1225
+ * which resolves the app user before querying the `runsByUser` index.
1226
+ */
1227
+ async listUserWorkflowRuns(appId, userId, params) {
1228
+ const result = await this.get(`/admin/api/apps/${appId}/users/${encodeURIComponent(userId)}/workflow-runs`, params);
1229
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1230
+ return {
1231
+ items: result?.items ?? [],
1232
+ hasMore: result?.hasMore,
1233
+ nextCursor,
1234
+ cursor: nextCursor,
1235
+ scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
1008
1236
  };
1009
1237
  }
1010
1238
  async getWorkflowRunStatus(appId, workflowId, runId) {
@@ -1014,12 +1242,9 @@ export class ApiClient {
1014
1242
  const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
1015
1243
  return { items: result?.items ?? [] };
1016
1244
  }
1017
- async getWorkflowAnalytics(appId, params) {
1018
- return this.get(`/app/${appId}/api/analytics/workflows/overview`, params);
1019
- }
1020
- async getTopWorkflows(appId, params) {
1021
- return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1022
- }
1245
+ // Workflow analytics have one client path, `getAnalyticsTopWorkflows`
1246
+ // (issue #2766): `getTopWorkflows` was a second name for the same endpoint,
1247
+ // and `getWorkflowAnalytics` called a route the server never registered.
1023
1248
  // ============================================
1024
1249
  // WORKFLOW CONFIGURATIONS
1025
1250
  // ============================================
@@ -1177,21 +1402,6 @@ export class ApiClient {
1177
1402
  return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
1178
1403
  }
1179
1404
  // ============================================
1180
- // LLM UTILITIES
1181
- // ============================================
1182
- async listLlmModels(provider = "openrouter") {
1183
- return this.get("/admin/api/llm/models", { provider });
1184
- }
1185
- async generatePrompt(payload) {
1186
- return this.post("/admin/api/llm/generate-prompt", payload);
1187
- }
1188
- async generateEvaluator(payload) {
1189
- return this.post("/admin/api/llm/generate-evaluator", payload);
1190
- }
1191
- async generateWorkflowEvaluator(payload) {
1192
- return this.post("/admin/api/llm/generate-evaluator-workflow", payload);
1193
- }
1194
- // ============================================
1195
1405
  // BATCH TEST EXECUTION
1196
1406
  // ============================================
1197
1407
  async startBatchTests(appId, blockType, blockId, payload) {
@@ -1218,8 +1428,17 @@ export class ApiClient {
1218
1428
  async getEmailTemplate(appId, emailType) {
1219
1429
  return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1220
1430
  }
1221
- async upsertEmailTemplate(appId, emailType, payload) {
1222
- return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, payload);
1431
+ /**
1432
+ * Create or replace an email-template override.
1433
+ *
1434
+ * `expectedModifiedAt` is the optimistic-concurrency token every sibling
1435
+ * config endpoint already takes (#2731 B10). Optional and additive: when
1436
+ * omitted the write is unconditional, which is how `--force` and every
1437
+ * pre-existing caller keep working; when supplied and stale, the server
1438
+ * answers 409 rather than silently overwriting a concurrent edit.
1439
+ */
1440
+ async upsertEmailTemplate(appId, emailType, payload, expectedModifiedAt) {
1441
+ return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload);
1223
1442
  }
1224
1443
  async deleteEmailTemplate(appId, emailType) {
1225
1444
  return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
@@ -1246,8 +1465,42 @@ export class ApiClient {
1246
1465
  // ============================================
1247
1466
  // DATABASES
1248
1467
  // ============================================
1249
- async listDatabases(appId) {
1250
- return this.get(`/app/${appId}/api/databases`);
1468
+ /**
1469
+ * List an app's databases, following the cursor to the end.
1470
+ *
1471
+ * The endpoint returns the `{ items, hasMore, nextCursor }` envelope and
1472
+ * caps a page at 100 (#1958), so `databases list` needs the follow-up pages
1473
+ * to show every database. `paginateAll` carries the repeat-cursor and
1474
+ * max-page guards, so a server that hands out a non-advancing cursor fails
1475
+ * loudly instead of looping forever.
1476
+ *
1477
+ * `options.owner` narrows the result to the databases that user created; the
1478
+ * server resolves it through the `databasesByCreator` index (#1965).
1479
+ */
1480
+ async listDatabases(appId, options) {
1481
+ return paginateAll(async (cursor) => {
1482
+ const params = new URLSearchParams();
1483
+ if (options?.owner)
1484
+ params.set("owner", options.owner);
1485
+ if (cursor)
1486
+ params.set("cursor", cursor);
1487
+ const qs = params.toString() ? `?${params.toString()}` : "";
1488
+ const resp = await this.get(`/app/${appId}/api/databases${qs}`);
1489
+ // Tolerant read: a server older than #1958 answers with a bare array.
1490
+ // Without this a published CLI pointed at a not-yet-upgraded server
1491
+ // would print nothing at all. Such a server also predates the `owner`
1492
+ // filter (#1965) and ignores the unknown query parameter, so apply the
1493
+ // filter here — the legacy rows carry `createdBy`, so this reproduces
1494
+ // the server's own post-join filter (#2245).
1495
+ if (Array.isArray(resp)) {
1496
+ return {
1497
+ items: options?.owner
1498
+ ? resp.filter((db) => db?.createdBy === options.owner)
1499
+ : resp,
1500
+ };
1501
+ }
1502
+ return { items: resp?.items ?? [], nextCursor: resp?.nextCursor };
1503
+ });
1251
1504
  }
1252
1505
  async createDatabase(appId, data) {
1253
1506
  return this.post(`/app/${appId}/api/databases`, data);
@@ -1314,6 +1567,14 @@ export class ApiClient {
1314
1567
  async describeDatabaseModel(appId, databaseId, modelName) {
1315
1568
  return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
1316
1569
  }
1570
+ /**
1571
+ * Query records in a database model. The Durable Object answers with its own
1572
+ * `{ data, hasMore, nextCursor?, prevCursor? }` envelope; that wire shape is
1573
+ * shared with the JS client, the Swift client and web-admin, so the CLI
1574
+ * normalizes it at its own boundary instead (issue #2357) and hands callers
1575
+ * the same `{ items, hasMore, nextCursor? }` envelope as
1576
+ * {@link queryDocumentRecords}.
1577
+ */
1317
1578
  async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1318
1579
  const body = { modelName };
1319
1580
  if (queryOptions?.filter)
@@ -1325,7 +1586,55 @@ export class ApiClient {
1325
1586
  options.uniqueStartKey = queryOptions.cursor;
1326
1587
  if (Object.keys(options).length > 0)
1327
1588
  body.options = options;
1328
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/query`, body);
1589
+ const raw = await this.post(`/app/${appId}/api/databases/${databaseId}/records/query`, body);
1590
+ return normalizeCliListEnvelope(raw);
1591
+ }
1592
+ async countDatabaseRecords(appId, databaseId, modelName, filter) {
1593
+ const body = { modelName };
1594
+ if (filter)
1595
+ body.filter = filter;
1596
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/count`, body);
1597
+ }
1598
+ async aggregateDatabaseRecords(appId, databaseId, modelName, options) {
1599
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/aggregate`, {
1600
+ modelName,
1601
+ options,
1602
+ });
1603
+ }
1604
+ /**
1605
+ * Upsert one record via `records/save`: creates the record when the id is
1606
+ * new, and merges the supplied fields into it when it already exists (the DO
1607
+ * writes with `ON CONFLICT … json_patch`, so fields you leave out survive).
1608
+ * `patchDatabaseRecord` is the same merge but requires the record to exist.
1609
+ *
1610
+ * `id` is optional: when omitted the body carries no `id`, which the
1611
+ * DatabaseDO only accepts alongside an `upsertOn` field — callers that want a
1612
+ * plain create must supply an id themselves (`databases records save` mints a
1613
+ * ULID, matching the CSV import path).
1614
+ *
1615
+ * Returns the endpoint's flat `{ success, id, appliedFields }` — not the
1616
+ * saved record.
1617
+ */
1618
+ async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
1619
+ const body = { modelName, data };
1620
+ if (id !== undefined)
1621
+ body.id = id;
1622
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/save`, body);
1623
+ }
1624
+ /**
1625
+ * Merge the supplied fields into an existing record via `records/patch`.
1626
+ *
1627
+ * Fields absent from `data` are left as they are. Unlike `saveDatabaseRecord`
1628
+ * the record must already exist — the DatabaseDO answers 404 for an unknown
1629
+ * id rather than creating one. Returns the flat
1630
+ * `{ success, id, appliedFields }`.
1631
+ */
1632
+ async patchDatabaseRecord(appId, databaseId, modelName, id, data) {
1633
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/patch`, {
1634
+ modelName,
1635
+ id,
1636
+ data,
1637
+ });
1329
1638
  }
1330
1639
  // ============================================
1331
1640
  // DATABASE OPERATIONS (registered operations)
@@ -1333,6 +1642,15 @@ export class ApiClient {
1333
1642
  async listDatabaseOperations(appId, databaseId) {
1334
1643
  return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
1335
1644
  }
1645
+ /**
1646
+ * Execute a registered operation and, when the result is a query page,
1647
+ * normalize it to the CLI's one list envelope (issue #2440).
1648
+ *
1649
+ * The registered operation decides the response shape, so the normalization
1650
+ * is conditional — see {@link normalizeDatabaseOperationResult}. Everything
1651
+ * else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
1652
+ * pipeline) is returned exactly as the server sent it.
1653
+ */
1336
1654
  async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
1337
1655
  const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
1338
1656
  const extraHeaders = {};
@@ -1340,17 +1658,17 @@ export class ApiClient {
1340
1658
  extraHeaders["X-Timing"] = "true";
1341
1659
  }
1342
1660
  if (token) {
1343
- return this.requestWithToken(path, token, {
1661
+ return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
1344
1662
  method: "POST",
1345
1663
  body: JSON.stringify(data || {}),
1346
1664
  headers: extraHeaders,
1347
- });
1665
+ }));
1348
1666
  }
1349
- return this.request(path, {
1667
+ return normalizeDatabaseOperationResult(await this.request(path, {
1350
1668
  method: "POST",
1351
1669
  body: JSON.stringify(data || {}),
1352
1670
  headers: extraHeaders,
1353
- });
1671
+ }));
1354
1672
  }
1355
1673
  /**
1356
1674
  * Execute a registered batch (bulk) database operation. Posts a chunk of
@@ -1416,7 +1734,7 @@ export class ApiClient {
1416
1734
  async createDatabaseTypeConfig(appId, data,
1417
1735
  // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
1418
1736
  // validation (inline CEL rule lint, manifest shape, schema TOML parse)
1419
- // WITHOUT persisting, so a fresh-type `sync push --dry-run` surfaces a
1737
+ // WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
1420
1738
  // malformed `defaultAccess`/manifest the same way the real POST would.
1421
1739
  options) {
1422
1740
  const query = options?.dryRun ? "?dryRun=true" : "";
@@ -1455,7 +1773,7 @@ export class ApiClient {
1455
1773
  }
1456
1774
  async createDatabaseTypeOperation(appId, databaseType, data,
1457
1775
  // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
1458
- // without persisting — used by `sync push --dry-run` to surface gate
1776
+ // without persisting — used by `config push --dry-run` to surface gate
1459
1777
  // failures the real push would hit. `schemaOverride` lets the gate run
1460
1778
  // against the schema the SAME push is about to land (so an op that depends
1461
1779
  // on a new field isn't falsely rejected against the stale stored schema).
@@ -1526,8 +1844,12 @@ export class ApiClient {
1526
1844
  async createDatabaseTypeSubscription(appId, databaseType, data) {
1527
1845
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
1528
1846
  }
1529
- async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data) {
1530
- return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, data);
1847
+ async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
1848
+ // #2731 B10 — optimistic concurrency, as the sibling operation update has
1849
+ // had. Omitted (by `--force`, and by every caller predating it) means the
1850
+ // write stays unconditional.
1851
+ expectedModifiedAt) {
1852
+ return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
1531
1853
  }
1532
1854
  async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1533
1855
  return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
@@ -1738,8 +2060,10 @@ export class ApiClient {
1738
2060
  * path-addressed PUT route). `resourceType` / `category` identify the config;
1739
2061
  * the body carries `schema` / `readRule` / `writeRule` / `description`.
1740
2062
  */
1741
- async upsertMetadataCategoryConfig(appId, resourceType, category, data) {
1742
- return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, data);
2063
+ async upsertMetadataCategoryConfig(appId, resourceType, category, data,
2064
+ /** Optimistic-concurrency token; see `upsertEmailTemplate` (#2731 B10). */
2065
+ expectedModifiedAt) {
2066
+ return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
1743
2067
  }
1744
2068
  /**
1745
2069
  * Delete a metadata category config (issue #1426, admin-gated route shipped
@@ -1815,6 +2139,112 @@ export class ApiClient {
1815
2139
  async getDocument(appId, documentId) {
1816
2140
  return this.get(`/app/${appId}/api/documents/${documentId}`);
1817
2141
  }
2142
+ /**
2143
+ * Delete a document and everything hanging off it (#2756). The server route
2144
+ * runs the same cascade the client SDK's `documents.delete()` triggers —
2145
+ * Yjs state and update history, blob rows and objects, aliases, user/group
2146
+ * permissions, invitations, collection memberships — and returns
2147
+ * `{ success, message }`, which callers pass through under `--json`.
2148
+ */
2149
+ async deleteDocument(appId, documentId) {
2150
+ return this.delete(`/app/${appId}/api/documents/${documentId}`);
2151
+ }
2152
+ /**
2153
+ * Introspect a document's Yjs schema (model names, per-model fields and
2154
+ * indexes). Backs `documents records models` and `documents records
2155
+ * describe`. Reuses the existing `GET documents/:id/schema` endpoint.
2156
+ */
2157
+ async getDocumentSchema(appId, documentId) {
2158
+ return this.get(`/app/${appId}/api/documents/${documentId}/schema`);
2159
+ }
2160
+ /**
2161
+ * Query records in a document model (issue #1964 Phase 3). Reuses the
2162
+ * server-side `GET documents/:id/records/:model` endpoint, which runs
2163
+ * through the caller's document permission (reader+) or the app-admin arm.
2164
+ * Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
2165
+ * is an opaque token — feed `nextCursor` back as `cursor` for the next page.
2166
+ * The server also dual-emits a deprecated `cursor` alias (#1316); the
2167
+ * normalizer drops it so CLI output is already on `nextCursor` when #1982
2168
+ * removes it.
2169
+ */
2170
+ async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
2171
+ const query = {};
2172
+ if (queryOptions?.filter)
2173
+ query.filter = JSON.stringify(queryOptions.filter);
2174
+ // Forward `limit` whenever the caller supplied one (including an invalid
2175
+ // 0/negative) so the server validates and returns a clean 400 rather than
2176
+ // silently applying the default.
2177
+ if (queryOptions?.limit !== undefined)
2178
+ query.limit = queryOptions.limit;
2179
+ if (queryOptions?.cursor)
2180
+ query.cursor = queryOptions.cursor;
2181
+ const raw = await this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, query);
2182
+ return normalizeCliListEnvelope(raw);
2183
+ }
2184
+ /**
2185
+ * Count records in a document model (issue #1964 Phase 3). Returns `{ count }`.
2186
+ */
2187
+ async countDocumentRecords(appId, documentId, modelName, queryOptions) {
2188
+ const query = {};
2189
+ if (queryOptions?.filter)
2190
+ query.filter = JSON.stringify(queryOptions.filter);
2191
+ return this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/count`, query);
2192
+ }
2193
+ /**
2194
+ * Aggregate records in a document model (issue #2437) — the documents twin
2195
+ * of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
2196
+ * with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
2197
+ * in the same shape as the database endpoint.
2198
+ *
2199
+ * `groupBy` takes plain field names only: StringSet facet grouping and
2200
+ * `{ field, contains }` membership grouping are database-only, which is why
2201
+ * this signature is narrower than the databases one.
2202
+ */
2203
+ async aggregateDocumentRecords(appId, documentId, modelName, options) {
2204
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate`, { options });
2205
+ }
2206
+ /**
2207
+ * Fetch platform-vocabulary document statistics (issue #1964 Phase 3):
2208
+ * record/model/blob counts, approximate size, and last-modified timestamp.
2209
+ */
2210
+ async getDocumentStats(appId, documentId) {
2211
+ return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
2212
+ }
2213
+ /**
2214
+ * Create or replace a record in a document model (issue #1964 Phase 4).
2215
+ * `POST documents/:id/records/:model` with `{ id, data, options? }` —
2216
+ * server-side write through the document facade (Yjs-first, CRDT-safe,
2217
+ * broadcasts to connected clients). Requires read-write+ on the document
2218
+ * or the app-admin arm. Returns `{ record }`.
2219
+ */
2220
+ async saveDocumentRecord(appId, documentId, modelName, body) {
2221
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, body);
2222
+ }
2223
+ /**
2224
+ * Merge fields into an existing record (issue #1964 Phase 4).
2225
+ * `PATCH documents/:id/records/:model/:recordId` with `{ data, options? }`.
2226
+ * Returns `{ record }`.
2227
+ */
2228
+ async patchDocumentRecord(appId, documentId, modelName, recordId, body) {
2229
+ return this.patch(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`, body);
2230
+ }
2231
+ /**
2232
+ * Delete a record from a document model (issue #1964 Phase 4).
2233
+ * `DELETE documents/:id/records/:model/:recordId`. A missing record id is
2234
+ * a silent no-op per the facade contract. Returns `{ deleted: true }`.
2235
+ */
2236
+ async deleteDocumentRecord(appId, documentId, modelName, recordId) {
2237
+ return this.delete(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`);
2238
+ }
2239
+ /**
2240
+ * Apply an ordered multi-model create/patch/delete blob atomically (issue
2241
+ * #1964 Phase 4, wrapping the #1517 `bulkUpdate` facade). All-or-nothing:
2242
+ * a validation failure commits nothing. Returns the facade's
2243
+ * `{ applied, added, updated, deleted }`.
2244
+ */
2245
+ async bulkDocumentRecords(appId, documentId, operations) {
2246
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk`, { operations });
2247
+ }
1818
2248
  async createDocument(appId, data) {
1819
2249
  return this.post(`/app/${appId}/api/documents`, data);
1820
2250
  }
@@ -1824,6 +2254,24 @@ export class ApiClient {
1824
2254
  async grantDocumentPermission(appId, documentId, permissions) {
1825
2255
  return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
1826
2256
  }
2257
+ /**
2258
+ * Revoke a user's permission on a document. Mirrors the published client's
2259
+ * `documents.removePermission` wire behavior (`src/client/api/documentsApi.ts`):
2260
+ * a userId targets the path route `DELETE .../permissions/:userId`, while an
2261
+ * email targets the query-param route `DELETE .../permissions?email=...`
2262
+ * (unified email removal, #452). This duplication of the client's wire shape
2263
+ * is deliberate and parity-tested (issue #1964 amendment 8).
2264
+ */
2265
+ async revokeDocumentPermission(appId, documentId, target) {
2266
+ if (target.email) {
2267
+ const email = encodeURIComponent(target.email);
2268
+ return this.delete(`/app/${appId}/api/documents/${documentId}/permissions?email=${email}`);
2269
+ }
2270
+ if (!target.userId) {
2271
+ throw new Error("revokeDocumentPermission: userId or email is required");
2272
+ }
2273
+ return this.delete(`/app/${appId}/api/documents/${documentId}/permissions/${target.userId}`);
2274
+ }
1827
2275
  async getDocumentLinkAccess(appId, documentId) {
1828
2276
  return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
1829
2277
  }
@@ -1889,15 +2337,16 @@ export class ApiClient {
1889
2337
  mustNotExist,
1890
2338
  });
1891
2339
  }
1892
- async findUserByEmail(appId, email) {
1893
- try {
1894
- const result = await this.get(`/app/${appId}/api/users`, { email });
1895
- const items = result?.items || (Array.isArray(result) ? result : []);
1896
- return items[0] || null;
1897
- }
1898
- catch {
1899
- return null;
1900
- }
2340
+ /**
2341
+ * Resolve an app user by exact email — the single email → userId lookup in
2342
+ * the CLI (issue #2763). It reads the member-accessible `users/lookup`
2343
+ * endpoint rather than the admin-only `GET /users` list, and returns the
2344
+ * server's `{ exists, user? }` envelope as-is: a 403 or a transport failure
2345
+ * rejects instead of being flattened into "no such user", which is exactly
2346
+ * how the deleted admin-list helper misreported permission problems.
2347
+ */
2348
+ async lookupUserByEmail(appId, email) {
2349
+ return this.get(`/app/${appId}/api/users/lookup`, { email });
1901
2350
  }
1902
2351
  async listAdminDocuments(appId, userId) {
1903
2352
  const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
@@ -1906,21 +2355,29 @@ export class ApiClient {
1906
2355
  // ============================================
1907
2356
  // DATABASE EXPORT / IMPORT
1908
2357
  // ============================================
1909
- async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
1910
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/save`, {
1911
- modelName,
1912
- id,
1913
- data,
1914
- });
1915
- }
1916
- async batchDatabaseRecords(appId, databaseId, operations) {
1917
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
2358
+ // `saveDatabaseRecord` lives with the other record verbs under
2359
+ // "DATABASE RECORDS & SCHEMA" above.
2360
+ /**
2361
+ * Apply an ordered batch of record operations via `records/batch`.
2362
+ *
2363
+ * `atomic` (default off, matching the DatabaseDO) makes the whole batch
2364
+ * all-or-nothing: the DO rolls every write back at the first failed
2365
+ * operation and answers with that operation's status. `databases records
2366
+ * bulk` sends `atomic: true` so it matches the documents twin's
2367
+ * all-or-nothing contract (#2437); the CSV/import paths keep the default
2368
+ * partial-success behavior.
2369
+ */
2370
+ async batchDatabaseRecords(appId, databaseId, operations, options) {
2371
+ const body = { operations };
2372
+ if (options?.atomic)
2373
+ body.atomic = true;
2374
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, body);
1918
2375
  }
1919
2376
  async deleteDatabaseRecord(appId, databaseId, modelName, id) {
1920
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/delete`, { modelName, id });
2377
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, { modelName, id });
1921
2378
  }
1922
2379
  async batchDeleteDatabaseRecords(appId, databaseId, operations) {
1923
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
2380
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
1924
2381
  }
1925
2382
  async listDatabaseUniqueConstraints(appId, databaseId) {
1926
2383
  const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);