primitive-admin 1.0.60 → 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 (310) 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 +29 -14
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +22 -8
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +96 -16
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +49 -188
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/blob-buckets.js +50 -155
  12. package/dist/src/commands/blob-buckets.js.map +1 -1
  13. package/dist/src/commands/collection-type-configs.js +14 -100
  14. package/dist/src/commands/collection-type-configs.js.map +1 -1
  15. package/dist/src/commands/collections.js +47 -8
  16. package/dist/src/commands/collections.js.map +1 -1
  17. package/dist/src/commands/config.d.ts +46 -0
  18. package/dist/src/commands/config.js +479 -0
  19. package/dist/src/commands/config.js.map +1 -0
  20. package/dist/src/commands/connections.d.ts +2 -0
  21. package/dist/src/commands/connections.js +100 -0
  22. package/dist/src/commands/connections.js.map +1 -0
  23. package/dist/src/commands/cron-triggers.js +84 -176
  24. package/dist/src/commands/cron-triggers.js.map +1 -1
  25. package/dist/src/commands/database-type-configs.d.ts +2 -0
  26. package/dist/src/commands/database-type-configs.js +171 -0
  27. package/dist/src/commands/database-type-configs.js.map +1 -0
  28. package/dist/src/commands/database-types.js +4 -4
  29. package/dist/src/commands/database-types.js.map +1 -1
  30. package/dist/src/commands/databases.d.ts +63 -0
  31. package/dist/src/commands/databases.js +649 -87
  32. package/dist/src/commands/databases.js.map +1 -1
  33. package/dist/src/commands/documents.js +869 -12
  34. package/dist/src/commands/documents.js.map +1 -1
  35. package/dist/src/commands/email-templates.js +18 -125
  36. package/dist/src/commands/email-templates.js.map +1 -1
  37. package/dist/src/commands/env.d.ts +12 -1
  38. package/dist/src/commands/env.js +98 -25
  39. package/dist/src/commands/env.js.map +1 -1
  40. package/dist/src/commands/feature-flags.d.ts +14 -0
  41. package/dist/src/commands/feature-flags.js +116 -0
  42. package/dist/src/commands/feature-flags.js.map +1 -0
  43. package/dist/src/commands/group-type-configs.js +14 -109
  44. package/dist/src/commands/group-type-configs.js.map +1 -1
  45. package/dist/src/commands/init.d.ts +9 -1
  46. package/dist/src/commands/init.js +1058 -279
  47. package/dist/src/commands/init.js.map +1 -1
  48. package/dist/src/commands/integrations.js +136 -421
  49. package/dist/src/commands/integrations.js.map +1 -1
  50. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  51. package/dist/src/commands/metadata-category-configs.js +112 -0
  52. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  53. package/dist/src/commands/metadata.js +33 -141
  54. package/dist/src/commands/metadata.js.map +1 -1
  55. package/dist/src/commands/prompts.js +206 -563
  56. package/dist/src/commands/prompts.js.map +1 -1
  57. package/dist/src/commands/rule-sets.d.ts +1 -1
  58. package/dist/src/commands/rule-sets.js +16 -132
  59. package/dist/src/commands/rule-sets.js.map +1 -1
  60. package/dist/src/commands/scripts.js +67 -234
  61. package/dist/src/commands/scripts.js.map +1 -1
  62. package/dist/src/commands/sessions.d.ts +2 -0
  63. package/dist/src/commands/sessions.js +75 -0
  64. package/dist/src/commands/sessions.js.map +1 -0
  65. package/dist/src/commands/skill.js +2 -2
  66. package/dist/src/commands/skill.js.map +1 -1
  67. package/dist/src/commands/sync-app-settings.d.ts +113 -70
  68. package/dist/src/commands/sync-app-settings.js +193 -242
  69. package/dist/src/commands/sync-app-settings.js.map +1 -1
  70. package/dist/src/commands/sync.d.ts +1932 -49
  71. package/dist/src/commands/sync.js +9584 -1725
  72. package/dist/src/commands/sync.js.map +1 -1
  73. package/dist/src/commands/tokens.js +8 -2
  74. package/dist/src/commands/tokens.js.map +1 -1
  75. package/dist/src/commands/users.js +92 -1
  76. package/dist/src/commands/users.js.map +1 -1
  77. package/dist/src/commands/vars.js +44 -58
  78. package/dist/src/commands/vars.js.map +1 -1
  79. package/dist/src/commands/webhooks.js +318 -156
  80. package/dist/src/commands/webhooks.js.map +1 -1
  81. package/dist/src/commands/workflows.d.ts +49 -12
  82. package/dist/src/commands/workflows.js +652 -1043
  83. package/dist/src/commands/workflows.js.map +1 -1
  84. package/dist/src/lib/access-rule-display.d.ts +21 -0
  85. package/dist/src/lib/access-rule-display.js +34 -0
  86. package/dist/src/lib/access-rule-display.js.map +1 -0
  87. package/dist/src/lib/api-client.d.ts +630 -61
  88. package/dist/src/lib/api-client.js +676 -116
  89. package/dist/src/lib/api-client.js.map +1 -1
  90. package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
  91. package/dist/src/lib/app-settings-descriptor.js +403 -82
  92. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  93. package/dist/src/lib/canonical-json.d.ts +12 -0
  94. package/dist/src/lib/canonical-json.js +35 -0
  95. package/dist/src/lib/canonical-json.js.map +1 -0
  96. package/dist/src/lib/cli-manifest.d.ts +8 -0
  97. package/dist/src/lib/cli-manifest.js +1 -0
  98. package/dist/src/lib/cli-manifest.js.map +1 -1
  99. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
  100. package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
  101. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  102. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
  103. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
  104. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  105. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  106. package/dist/src/lib/config-object-descriptor.js +658 -0
  107. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  108. package/dist/src/lib/config-payload.d.ts +85 -0
  109. package/dist/src/lib/config-payload.js +116 -0
  110. package/dist/src/lib/config-payload.js.map +1 -0
  111. package/dist/src/lib/config-surface.d.ts +130 -0
  112. package/dist/src/lib/config-surface.js +300 -0
  113. package/dist/src/lib/config-surface.js.map +1 -0
  114. package/dist/src/lib/credentials-store.d.ts +10 -0
  115. package/dist/src/lib/credentials-store.js +23 -0
  116. package/dist/src/lib/credentials-store.js.map +1 -1
  117. package/dist/src/lib/data-input.d.ts +23 -0
  118. package/dist/src/lib/data-input.js +50 -0
  119. package/dist/src/lib/data-input.js.map +1 -0
  120. package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
  121. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  122. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
  123. package/dist/src/lib/env-resolver-core.d.ts +147 -0
  124. package/dist/src/lib/env-resolver-core.js +265 -0
  125. package/dist/src/lib/env-resolver-core.js.map +1 -0
  126. package/dist/src/lib/env-resolver.d.ts +28 -6
  127. package/dist/src/lib/env-resolver.js +44 -32
  128. package/dist/src/lib/env-resolver.js.map +1 -1
  129. package/dist/src/lib/generated-allowlist.js +13 -0
  130. package/dist/src/lib/generated-allowlist.js.map +1 -1
  131. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  132. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  133. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  134. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  135. package/dist/src/lib/generated-template-lint.js +624 -0
  136. package/dist/src/lib/generated-template-lint.js.map +1 -0
  137. package/dist/src/lib/init-adopt.d.ts +16 -0
  138. package/dist/src/lib/init-adopt.js +34 -0
  139. package/dist/src/lib/init-adopt.js.map +1 -0
  140. package/dist/src/lib/init-assets.d.ts +39 -0
  141. package/dist/src/lib/init-assets.js +97 -0
  142. package/dist/src/lib/init-assets.js.map +1 -0
  143. package/dist/src/lib/init-config.d.ts +44 -5
  144. package/dist/src/lib/init-config.js +80 -7
  145. package/dist/src/lib/init-config.js.map +1 -1
  146. package/dist/src/lib/init-ios-links.d.ts +50 -0
  147. package/dist/src/lib/init-ios-links.js +153 -0
  148. package/dist/src/lib/init-ios-links.js.map +1 -0
  149. package/dist/src/lib/init-plan.d.ts +80 -0
  150. package/dist/src/lib/init-plan.js +95 -0
  151. package/dist/src/lib/init-plan.js.map +1 -0
  152. package/dist/src/lib/init-production-env.d.ts +48 -0
  153. package/dist/src/lib/init-production-env.js +59 -0
  154. package/dist/src/lib/init-production-env.js.map +1 -0
  155. package/dist/src/lib/init-schema.d.ts +74 -0
  156. package/dist/src/lib/init-schema.js +358 -0
  157. package/dist/src/lib/init-schema.js.map +1 -0
  158. package/dist/src/lib/init-xcode.d.ts +33 -0
  159. package/dist/src/lib/init-xcode.js +114 -0
  160. package/dist/src/lib/init-xcode.js.map +1 -0
  161. package/dist/src/lib/integration-request-config.d.ts +30 -0
  162. package/dist/src/lib/integration-request-config.js +145 -0
  163. package/dist/src/lib/integration-request-config.js.map +1 -0
  164. package/dist/src/lib/local-state.d.ts +55 -0
  165. package/dist/src/lib/local-state.js +167 -0
  166. package/dist/src/lib/local-state.js.map +1 -0
  167. package/dist/src/lib/log-inspection.d.ts +568 -0
  168. package/dist/src/lib/log-inspection.js +639 -0
  169. package/dist/src/lib/log-inspection.js.map +1 -0
  170. package/dist/src/lib/migration-nag.d.ts +2 -2
  171. package/dist/src/lib/migration-nag.js +3 -3
  172. package/dist/src/lib/object-status-filter.d.ts +22 -0
  173. package/dist/src/lib/object-status-filter.js +45 -0
  174. package/dist/src/lib/object-status-filter.js.map +1 -0
  175. package/dist/src/lib/output.d.ts +24 -1
  176. package/dist/src/lib/output.js +42 -1
  177. package/dist/src/lib/output.js.map +1 -1
  178. package/dist/src/lib/package-manager.d.ts +140 -0
  179. package/dist/src/lib/package-manager.js +305 -0
  180. package/dist/src/lib/package-manager.js.map +1 -0
  181. package/dist/src/lib/paginate.d.ts +50 -0
  182. package/dist/src/lib/paginate.js +53 -0
  183. package/dist/src/lib/paginate.js.map +1 -1
  184. package/dist/src/lib/platform-owned.d.ts +29 -3
  185. package/dist/src/lib/platform-owned.js +42 -3
  186. package/dist/src/lib/platform-owned.js.map +1 -1
  187. package/dist/src/lib/project-config.js +13 -5
  188. package/dist/src/lib/project-config.js.map +1 -1
  189. package/dist/src/lib/query-operators.d.ts +1 -1
  190. package/dist/src/lib/query-operators.js +1 -1
  191. package/dist/src/lib/record-filter.d.ts +18 -0
  192. package/dist/src/lib/record-filter.js +55 -0
  193. package/dist/src/lib/record-filter.js.map +1 -0
  194. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  195. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  196. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  197. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  198. package/dist/src/lib/resolve-init-server.js +77 -0
  199. package/dist/src/lib/resolve-init-server.js.map +1 -0
  200. package/dist/src/lib/resolve-platform.d.ts +43 -14
  201. package/dist/src/lib/resolve-platform.js +74 -12
  202. package/dist/src/lib/resolve-platform.js.map +1 -1
  203. package/dist/src/lib/run-status.d.ts +19 -0
  204. package/dist/src/lib/run-status.generated.d.ts +39 -0
  205. package/dist/src/lib/run-status.generated.js +66 -0
  206. package/dist/src/lib/run-status.generated.js.map +1 -0
  207. package/dist/src/lib/run-status.js +19 -0
  208. package/dist/src/lib/run-status.js.map +1 -0
  209. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  210. package/dist/src/lib/server-text-normalization.js +90 -0
  211. package/dist/src/lib/server-text-normalization.js.map +1 -0
  212. package/dist/src/lib/server-url.d.ts +22 -0
  213. package/dist/src/lib/server-url.js +33 -0
  214. package/dist/src/lib/server-url.js.map +1 -0
  215. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  216. package/dist/src/lib/signing-secret-status.js +116 -0
  217. package/dist/src/lib/signing-secret-status.js.map +1 -0
  218. package/dist/src/lib/skill-installer.d.ts +4 -2
  219. package/dist/src/lib/skill-installer.js +143 -12
  220. package/dist/src/lib/skill-installer.js.map +1 -1
  221. package/dist/src/lib/snapshots.d.ts +2 -2
  222. package/dist/src/lib/snapshots.js +2 -2
  223. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
  224. package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
  225. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  226. package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
  227. package/dist/src/lib/swift-codegen/generator.js +181 -7
  228. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  229. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
  230. package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
  231. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
  232. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  233. package/dist/src/lib/sync-dir-selector.js +30 -0
  234. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  235. package/dist/src/lib/sync-paths.d.ts +39 -0
  236. package/dist/src/lib/sync-paths.js +70 -2
  237. package/dist/src/lib/sync-paths.js.map +1 -1
  238. package/dist/src/lib/sync-resource-types.d.ts +138 -31
  239. package/dist/src/lib/sync-resource-types.js +241 -47
  240. package/dist/src/lib/sync-resource-types.js.map +1 -1
  241. package/dist/src/lib/sync-selectors.d.ts +95 -0
  242. package/dist/src/lib/sync-selectors.js +228 -0
  243. package/dist/src/lib/sync-selectors.js.map +1 -0
  244. package/dist/src/lib/template.d.ts +62 -4
  245. package/dist/src/lib/template.js +147 -39
  246. package/dist/src/lib/template.js.map +1 -1
  247. package/dist/src/lib/test-case-keys.d.ts +29 -0
  248. package/dist/src/lib/test-case-keys.js +55 -0
  249. package/dist/src/lib/test-case-keys.js.map +1 -0
  250. package/dist/src/lib/test-case-variables.d.ts +15 -0
  251. package/dist/src/lib/test-case-variables.js +29 -0
  252. package/dist/src/lib/test-case-variables.js.map +1 -0
  253. package/dist/src/lib/toml-database-config.d.ts +4 -4
  254. package/dist/src/lib/toml-database-config.js +27 -34
  255. package/dist/src/lib/toml-database-config.js.map +1 -1
  256. package/dist/src/lib/toml-metadata-config.d.ts +9 -5
  257. package/dist/src/lib/toml-metadata-config.js +40 -45
  258. package/dist/src/lib/toml-metadata-config.js.map +1 -1
  259. package/dist/src/lib/toml-native-form.d.ts +1 -1
  260. package/dist/src/lib/toml-native-form.js +1 -1
  261. package/dist/src/lib/toml-params-validator.d.ts +1 -1
  262. package/dist/src/lib/toml-params-validator.js +1 -1
  263. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  264. package/dist/src/lib/toml-scalar-edit.js +283 -0
  265. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  266. package/dist/src/lib/user-selector.d.ts +24 -0
  267. package/dist/src/lib/user-selector.js +33 -0
  268. package/dist/src/lib/user-selector.js.map +1 -0
  269. package/dist/src/lib/version-check.d.ts +25 -0
  270. package/dist/src/lib/version-check.js +77 -8
  271. package/dist/src/lib/version-check.js.map +1 -1
  272. package/dist/src/lib/watch.d.ts +121 -0
  273. package/dist/src/lib/watch.js +169 -0
  274. package/dist/src/lib/watch.js.map +1 -0
  275. package/dist/src/lib/workflow-apply.d.ts +69 -45
  276. package/dist/src/lib/workflow-apply.js +99 -72
  277. package/dist/src/lib/workflow-apply.js.map +1 -1
  278. package/dist/src/lib/workflow-codegen/generator.js +4 -4
  279. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  280. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
  281. package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
  282. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  283. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  284. package/dist/src/lib/workflow-config-apply.js +137 -0
  285. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  286. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  287. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  288. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  289. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  290. package/dist/src/lib/workflow-defaults.js +41 -0
  291. package/dist/src/lib/workflow-defaults.js.map +1 -0
  292. package/dist/src/lib/workflow-fragments.d.ts +23 -0
  293. package/dist/src/lib/workflow-fragments.js +229 -8
  294. package/dist/src/lib/workflow-fragments.js.map +1 -1
  295. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  296. package/dist/src/lib/workflow-include-preserve.js +286 -0
  297. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  298. package/dist/src/lib/workflow-payload.d.ts +31 -20
  299. package/dist/src/lib/workflow-payload.js +100 -93
  300. package/dist/src/lib/workflow-payload.js.map +1 -1
  301. package/dist/src/lib/workflow-toml-validator.d.ts +80 -3
  302. package/dist/src/lib/workflow-toml-validator.js +231 -3
  303. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  304. package/dist/src/types/index.d.ts +78 -22
  305. package/dist/src/validators.d.ts +7 -7
  306. package/dist/src/validators.js +6 -6
  307. package/package.json +11 -6
  308. package/dist/src/commands/settings.d.ts +0 -15
  309. package/dist/src/commands/settings.js +0 -102
  310. 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() {
@@ -313,7 +352,14 @@ export class ApiClient {
313
352
  if (cursor)
314
353
  qs.set("cursor", cursor);
315
354
  const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
316
- return { items: Array.isArray(resp.apps) ? resp.apps : [], nextCursor: resp.nextCursor };
355
+ // The server now emits `items` with `apps` as a deprecation-window
356
+ // alias (#1316). Read `items` first so listings keep working once the
357
+ // alias is removed; fall back to the legacy `apps` key meanwhile.
358
+ return {
359
+ items: resp.items ??
360
+ (Array.isArray(resp.apps) ? resp.apps : []),
361
+ nextCursor: resp.nextCursor ?? resp.cursor,
362
+ };
317
363
  });
318
364
  return { apps };
319
365
  }
@@ -371,6 +417,40 @@ export class ApiClient {
371
417
  async removeUser(appId, userId) {
372
418
  return this.delete(`/app/${appId}/api/users/${userId}`);
373
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
+ }
374
454
  async updateUserRole(appId, userId, role) {
375
455
  return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
376
456
  }
@@ -403,7 +483,16 @@ export class ApiClient {
403
483
  // INVITATIONS
404
484
  // ============================================
405
485
  async listInvitations(appId, params) {
406
- return this.get(`/app/${appId}/api/invitations`, params);
486
+ const result = await this.get(`/app/${appId}/api/invitations`, params);
487
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
488
+ // so the CLI keeps paginating against pre-#1316 servers.
489
+ const nextCursor = result?.nextCursor ?? result?.cursor;
490
+ return {
491
+ items: result?.items ?? [],
492
+ nextCursor,
493
+ hasMore: result?.hasMore ?? nextCursor != null,
494
+ cursor: nextCursor,
495
+ };
407
496
  }
408
497
  async createInvitation(appId, data) {
409
498
  return this.post(`/app/${appId}/api/invitations`, data);
@@ -433,8 +522,9 @@ export class ApiClient {
433
522
  async listDeferredGrants(appId, params) {
434
523
  const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
435
524
  return {
436
- grants: result?.grants ?? [],
525
+ grants: result?.grants ?? result?.items ?? [],
437
526
  nextCursor: result?.nextCursor ?? null,
527
+ hasMore: result?.hasMore,
438
528
  };
439
529
  }
440
530
  async revokeDeferredGrant(appId, deferredId, type) {
@@ -450,9 +540,19 @@ export class ApiClient {
450
540
  nextCursor: result?.nextCursor ?? null,
451
541
  };
452
542
  }
543
+ /** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
453
544
  async getIntegration(appId, integrationId) {
454
545
  return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
455
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
+ }
456
556
  async createIntegration(appId, payload) {
457
557
  return this.post(`/admin/api/apps/${appId}/integrations`, payload);
458
558
  }
@@ -460,8 +560,15 @@ export class ApiClient {
460
560
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
461
561
  return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
462
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
+ */
463
569
  async deleteIntegration(appId, integrationId, hard) {
464
- const path = hard
570
+ const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
571
+ const path = isHard
465
572
  ? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
466
573
  : `/admin/api/apps/${appId}/integrations/${integrationId}`;
467
574
  return this.delete(path);
@@ -477,19 +584,6 @@ export class ApiClient {
477
584
  const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
478
585
  return result?.items ?? [];
479
586
  }
480
- async listIntegrationSecrets(appId, integrationId, params) {
481
- const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
482
- return result?.items ?? [];
483
- }
484
- async addIntegrationSecret(appId, integrationId, payload) {
485
- const result = await this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, payload);
486
- return result?.secret ?? null;
487
- }
488
- async archiveIntegrationSecret(appId, integrationId, secretId) {
489
- return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets/${secretId}`, {
490
- status: "inactive",
491
- });
492
- }
493
587
  // ============================================
494
588
  // APP SECRETS
495
589
  // ============================================
@@ -566,19 +660,90 @@ export class ApiClient {
566
660
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
567
661
  return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
568
662
  }
569
- async deleteWebhook(appId, webhookId) {
570
- 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}`, {});
571
688
  }
572
689
  async rotateWebhookSecret(appId, webhookId, payload) {
573
690
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
574
691
  }
575
692
  async listWebhookEvents(appId, webhookId, params) {
576
693
  const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
577
- 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
+ };
702
+ }
703
+ // ============================================
704
+ // CONNECTIONS INSPECTION (#1968)
705
+ // ============================================
706
+ /**
707
+ * List the live connections the server believes exist for exactly one of a
708
+ * document, user, or database. Read-only inspection over admin-api. Rows are
709
+ * per-connection-×-subscription bindings, so `connectionId` is not unique
710
+ * across rows. `params` on a database-subscription row is caller-defined
711
+ * bound CEL input — typed `unknown`.
712
+ */
713
+ async listConnections(appId, selector, params) {
714
+ const result = await this.get(`/admin/api/apps/${appId}/connections`, {
715
+ ...selector,
716
+ ...params,
717
+ });
718
+ return {
719
+ items: result?.items ?? [],
720
+ nextCursor: result?.nextCursor ?? null,
721
+ };
722
+ }
723
+ async listSessions(appId, userId, params) {
724
+ const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
725
+ userId,
726
+ ...params,
727
+ });
728
+ return {
729
+ items: result?.items ?? [],
730
+ nextCursor: result?.nextCursor ?? null,
731
+ };
578
732
  }
579
733
  async testWebhook(appId, webhookId, payload) {
580
734
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
581
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
+ }
582
747
  // ============================================
583
748
  // NAMED LOCKS (#1518)
584
749
  // ============================================
@@ -614,14 +779,21 @@ export class ApiClient {
614
779
  async updateCronTrigger(appId, triggerId, payload) {
615
780
  return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
616
781
  }
617
- async deleteCronTrigger(appId, triggerId) {
618
- 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}`);
619
791
  }
620
- async pauseCronTrigger(appId, triggerId) {
621
- 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`, {});
622
794
  }
623
- async resumeCronTrigger(appId, triggerId) {
624
- 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`, {});
625
797
  }
626
798
  async testCronTrigger(appId, triggerId) {
627
799
  return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
@@ -669,6 +841,20 @@ export class ApiClient {
669
841
  async deleteResourceMetadata(appId, resourceType, resourceId, category) {
670
842
  return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
671
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
+ }
672
858
  // ============================================
673
859
  // PROMPTS
674
860
  // ============================================
@@ -689,6 +875,12 @@ export class ApiClient {
689
875
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
690
876
  return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
691
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
+ */
692
884
  async deletePrompt(appId, promptId, hard) {
693
885
  const path = hard
694
886
  ? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
@@ -728,7 +920,7 @@ export class ApiClient {
728
920
  // The `Script` model is the authoring surface for `script` workflow
729
921
  // steps. A `Script` is a HEADER (name/description/activeConfigId); the
730
922
  // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
731
- // time. CLI `sync push` reads `transforms/*.rhai` files and reconciles
923
+ // time. CLI `config push` reads `transforms/*.rhai` files and reconciles
732
924
  // them: a new file → create script (mints a default config); a changed
733
925
  // file → create a new config + activate it (zero fan-out — referencing
734
926
  // workflows pick up the new body on their next run).
@@ -761,10 +953,28 @@ export class ApiClient {
761
953
  * workflows pick up the new body on their next run with no fan-out. The
762
954
  * config name is unique per script, so we mint a timestamped name.
763
955
  */
764
- 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) {
765
968
  const configName = `sync-${Date.now()}`;
766
969
  const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
767
- 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);
768
978
  return config;
769
979
  }
770
980
  /**
@@ -784,30 +994,54 @@ export class ApiClient {
784
994
  // ============================================
785
995
  // BLOCK TEST CASES (Prompts, Integrations, Workflows)
786
996
  // ============================================
787
- async listTestCases(appId, blockType, blockId) {
788
- const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`);
789
- 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
+ };
790
1014
  }
791
1015
  async getTestCase(appId, blockType, blockId, testCaseId) {
792
1016
  return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
793
1017
  }
794
1018
  async createTestCase(appId, blockType, blockId, payload) {
795
- // 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.
796
1023
  const serverPayload = {
797
1024
  name: payload.name,
798
- inputVariables: JSON.stringify(payload.inputVariables),
1025
+ inputVariables: payload.inputVariables,
799
1026
  configId: payload.configId,
800
1027
  evaluatorPromptId: payload.evaluatorPromptId,
801
1028
  evaluatorConfigId: payload.evaluatorConfigId,
802
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
+ }
803
1037
  if (payload.expectedOutputPattern) {
804
1038
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
805
1039
  }
806
1040
  if (payload.expectedOutputContains) {
807
- serverPayload.expectedOutputContains = JSON.stringify(payload.expectedOutputContains);
1041
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
808
1042
  }
809
1043
  if (payload.expectedJsonSubset) {
810
- serverPayload.expectedJsonSubset = JSON.stringify(payload.expectedJsonSubset);
1044
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
811
1045
  }
812
1046
  return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
813
1047
  }
@@ -815,21 +1049,23 @@ export class ApiClient {
815
1049
  const serverPayload = {};
816
1050
  if (payload.name !== undefined)
817
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.
818
1058
  if (payload.inputVariables !== undefined) {
819
- serverPayload.inputVariables = JSON.stringify(payload.inputVariables);
1059
+ serverPayload.inputVariables = payload.inputVariables;
820
1060
  }
821
1061
  if (payload.expectedOutputPattern !== undefined) {
822
1062
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
823
1063
  }
824
1064
  if (payload.expectedOutputContains !== undefined) {
825
- serverPayload.expectedOutputContains = payload.expectedOutputContains
826
- ? JSON.stringify(payload.expectedOutputContains)
827
- : null;
1065
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
828
1066
  }
829
1067
  if (payload.expectedJsonSubset !== undefined) {
830
- serverPayload.expectedJsonSubset = payload.expectedJsonSubset
831
- ? JSON.stringify(payload.expectedJsonSubset)
832
- : null;
1068
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
833
1069
  }
834
1070
  if (payload.configId !== undefined)
835
1071
  serverPayload.configId = payload.configId;
@@ -839,6 +1075,8 @@ export class ApiClient {
839
1075
  if (payload.evaluatorConfigId !== undefined) {
840
1076
  serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
841
1077
  }
1078
+ if (payload.key !== undefined)
1079
+ serverPayload.key = payload.key;
842
1080
  return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
843
1081
  }
844
1082
  async deleteTestCase(appId, blockType, blockId, testCaseId) {
@@ -935,15 +1173,33 @@ export class ApiClient {
935
1173
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
936
1174
  return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
937
1175
  }
938
- async deleteWorkflow(appId, workflowId) {
939
- 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`, {});
940
1199
  }
941
1200
  async updateWorkflowDraft(appId, workflowId, payload) {
942
1201
  return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
943
1202
  }
944
- async publishWorkflow(appId, workflowId) {
945
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
946
- }
947
1203
  async previewWorkflow(appId, workflowId, payload) {
948
1204
  return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
949
1205
  }
@@ -952,9 +1208,31 @@ export class ApiClient {
952
1208
  }
953
1209
  async listWorkflowRuns(appId, workflowId, params) {
954
1210
  const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
1211
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1212
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
955
1213
  return {
956
1214
  items: result?.items ?? [],
957
- cursor: result?.cursor ?? null,
1215
+ hasMore: result?.hasMore,
1216
+ nextCursor,
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,
958
1236
  };
959
1237
  }
960
1238
  async getWorkflowRunStatus(appId, workflowId, runId) {
@@ -964,12 +1242,9 @@ export class ApiClient {
964
1242
  const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
965
1243
  return { items: result?.items ?? [] };
966
1244
  }
967
- async getWorkflowAnalytics(appId, params) {
968
- return this.get(`/app/${appId}/api/analytics/workflows/overview`, params);
969
- }
970
- async getTopWorkflows(appId, params) {
971
- return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
972
- }
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.
973
1248
  // ============================================
974
1249
  // WORKFLOW CONFIGURATIONS
975
1250
  // ============================================
@@ -1040,6 +1315,9 @@ export class ApiClient {
1040
1315
  async getAnalyticsEventsGrouped(appId, params) {
1041
1316
  return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
1042
1317
  }
1318
+ async getAnalyticsErrorsGroups(appId, params) {
1319
+ return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
1320
+ }
1043
1321
  async getAnalyticsTopWorkflows(appId, params) {
1044
1322
  return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1045
1323
  }
@@ -1051,9 +1329,12 @@ export class ApiClient {
1051
1329
  // ============================================
1052
1330
  async listAllAdmins(params) {
1053
1331
  const result = await this.get("/admin/api/admins", params);
1332
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1333
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1054
1334
  return {
1055
- admins: result?.admins ?? [],
1056
- cursor: result?.cursor ?? null,
1335
+ admins: result?.admins ?? result?.items ?? [],
1336
+ nextCursor,
1337
+ cursor: nextCursor,
1057
1338
  };
1058
1339
  }
1059
1340
  async searchAdminByEmail(email) {
@@ -1121,21 +1402,6 @@ export class ApiClient {
1121
1402
  return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
1122
1403
  }
1123
1404
  // ============================================
1124
- // LLM UTILITIES
1125
- // ============================================
1126
- async listLlmModels(provider = "openrouter") {
1127
- return this.get("/admin/api/llm/models", { provider });
1128
- }
1129
- async generatePrompt(payload) {
1130
- return this.post("/admin/api/llm/generate-prompt", payload);
1131
- }
1132
- async generateEvaluator(payload) {
1133
- return this.post("/admin/api/llm/generate-evaluator", payload);
1134
- }
1135
- async generateWorkflowEvaluator(payload) {
1136
- return this.post("/admin/api/llm/generate-evaluator-workflow", payload);
1137
- }
1138
- // ============================================
1139
1405
  // BATCH TEST EXECUTION
1140
1406
  // ============================================
1141
1407
  async startBatchTests(appId, blockType, blockId, payload) {
@@ -1162,8 +1428,17 @@ export class ApiClient {
1162
1428
  async getEmailTemplate(appId, emailType) {
1163
1429
  return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1164
1430
  }
1165
- async upsertEmailTemplate(appId, emailType, payload) {
1166
- 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);
1167
1442
  }
1168
1443
  async deleteEmailTemplate(appId, emailType) {
1169
1444
  return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
@@ -1190,8 +1465,42 @@ export class ApiClient {
1190
1465
  // ============================================
1191
1466
  // DATABASES
1192
1467
  // ============================================
1193
- async listDatabases(appId) {
1194
- 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
+ });
1195
1504
  }
1196
1505
  async createDatabase(appId, data) {
1197
1506
  return this.post(`/app/${appId}/api/databases`, data);
@@ -1258,6 +1567,14 @@ export class ApiClient {
1258
1567
  async describeDatabaseModel(appId, databaseId, modelName) {
1259
1568
  return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
1260
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
+ */
1261
1578
  async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1262
1579
  const body = { modelName };
1263
1580
  if (queryOptions?.filter)
@@ -1269,7 +1586,55 @@ export class ApiClient {
1269
1586
  options.uniqueStartKey = queryOptions.cursor;
1270
1587
  if (Object.keys(options).length > 0)
1271
1588
  body.options = options;
1272
- 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
+ });
1273
1638
  }
1274
1639
  // ============================================
1275
1640
  // DATABASE OPERATIONS (registered operations)
@@ -1277,6 +1642,15 @@ export class ApiClient {
1277
1642
  async listDatabaseOperations(appId, databaseId) {
1278
1643
  return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
1279
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
+ */
1280
1654
  async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
1281
1655
  const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
1282
1656
  const extraHeaders = {};
@@ -1284,17 +1658,17 @@ export class ApiClient {
1284
1658
  extraHeaders["X-Timing"] = "true";
1285
1659
  }
1286
1660
  if (token) {
1287
- return this.requestWithToken(path, token, {
1661
+ return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
1288
1662
  method: "POST",
1289
1663
  body: JSON.stringify(data || {}),
1290
1664
  headers: extraHeaders,
1291
- });
1665
+ }));
1292
1666
  }
1293
- return this.request(path, {
1667
+ return normalizeDatabaseOperationResult(await this.request(path, {
1294
1668
  method: "POST",
1295
1669
  body: JSON.stringify(data || {}),
1296
1670
  headers: extraHeaders,
1297
- });
1671
+ }));
1298
1672
  }
1299
1673
  /**
1300
1674
  * Execute a registered batch (bulk) database operation. Posts a chunk of
@@ -1357,8 +1731,14 @@ export class ApiClient {
1357
1731
  async getDatabaseTypeConfig(appId, databaseType) {
1358
1732
  return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1359
1733
  }
1360
- async createDatabaseTypeConfig(appId, data) {
1361
- return this.post(`/app/${appId}/api/databases/types`, data);
1734
+ async createDatabaseTypeConfig(appId, data,
1735
+ // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
1736
+ // validation (inline CEL rule lint, manifest shape, schema TOML parse)
1737
+ // WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
1738
+ // malformed `defaultAccess`/manifest the same way the real POST would.
1739
+ options) {
1740
+ const query = options?.dryRun ? "?dryRun=true" : "";
1741
+ return this.post(`/app/${appId}/api/databases/types${query}`, data);
1362
1742
  }
1363
1743
  async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1364
1744
  const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
@@ -1393,7 +1773,7 @@ export class ApiClient {
1393
1773
  }
1394
1774
  async createDatabaseTypeOperation(appId, databaseType, data,
1395
1775
  // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
1396
- // without persisting — used by `sync push --dry-run` to surface gate
1776
+ // without persisting — used by `config push --dry-run` to surface gate
1397
1777
  // failures the real push would hit. `schemaOverride` lets the gate run
1398
1778
  // against the schema the SAME push is about to land (so an op that depends
1399
1779
  // on a new field isn't falsely rejected against the stale stored schema).
@@ -1413,16 +1793,24 @@ export class ApiClient {
1413
1793
  if (options?.dryRun && options.defaultAccess !== undefined) {
1414
1794
  body = { ...body, defaultAccess: options.defaultAccess };
1415
1795
  }
1796
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1797
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1798
+ }
1416
1799
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
1417
1800
  }
1418
1801
  async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
1419
1802
  // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
1420
1803
  // `schemaOverride` gates against the schema the same push is landing.
1804
+ // Issue #1336: `metadataManifestOverride` gates the op-access lint against
1805
+ // the manifest the same push is landing (see `createDatabaseTypeOperation`).
1421
1806
  options) {
1422
1807
  let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
1423
1808
  if (options?.dryRun && options.schemaOverride !== undefined) {
1424
1809
  body = { ...body, schemaOverride: options.schemaOverride };
1425
1810
  }
1811
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1812
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1813
+ }
1426
1814
  const query = options?.dryRun ? "?dryRun=true" : "";
1427
1815
  return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
1428
1816
  }
@@ -1456,8 +1844,12 @@ export class ApiClient {
1456
1844
  async createDatabaseTypeSubscription(appId, databaseType, data) {
1457
1845
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
1458
1846
  }
1459
- async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data) {
1460
- 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);
1461
1853
  }
1462
1854
  async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1463
1855
  return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
@@ -1466,7 +1858,16 @@ export class ApiClient {
1466
1858
  // GROUPS
1467
1859
  // ============================================
1468
1860
  async listGroups(appId, params) {
1469
- return this.get(`/app/${appId}/api/groups`, params);
1861
+ const result = await this.get(`/app/${appId}/api/groups`, params);
1862
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1863
+ // so the CLI keeps paginating against pre-#1316 servers.
1864
+ const nextCursor = result?.nextCursor ?? result?.cursor;
1865
+ return {
1866
+ items: result?.items ?? [],
1867
+ nextCursor,
1868
+ hasMore: result?.hasMore ?? nextCursor != null,
1869
+ cursor: nextCursor,
1870
+ };
1470
1871
  }
1471
1872
  async createGroup(appId, data) {
1472
1873
  return this.post(`/app/${appId}/api/groups`, data);
@@ -1485,7 +1886,16 @@ export class ApiClient {
1485
1886
  // ============================================
1486
1887
  async listGroupMembers(appId, groupType, groupId, options) {
1487
1888
  const qs = options?.include ? `?include=${options.include}` : "";
1488
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
1889
+ const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
1890
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1891
+ // so the CLI keeps paginating against pre-#1316 servers.
1892
+ const nextCursor = result?.nextCursor ?? result?.cursor;
1893
+ return {
1894
+ items: result?.items ?? [],
1895
+ nextCursor,
1896
+ hasMore: result?.hasMore ?? nextCursor != null,
1897
+ cursor: nextCursor,
1898
+ };
1489
1899
  }
1490
1900
  async addGroupMember(appId, groupType, groupId, data) {
1491
1901
  return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
@@ -1530,7 +1940,7 @@ export class ApiClient {
1530
1940
  const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1531
1941
  return {
1532
1942
  items: result?.items ?? [],
1533
- nextCursor: result?.cursor ?? null,
1943
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1534
1944
  };
1535
1945
  }
1536
1946
  async listAllCollections(appId, params) {
@@ -1543,7 +1953,7 @@ export class ApiClient {
1543
1953
  const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1544
1954
  return {
1545
1955
  items: result?.items ?? [],
1546
- nextCursor: result?.cursor ?? null,
1956
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1547
1957
  };
1548
1958
  }
1549
1959
  async createCollection(appId, data) {
@@ -1571,7 +1981,7 @@ export class ApiClient {
1571
1981
  const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1572
1982
  return {
1573
1983
  items: result?.items ?? [],
1574
- nextCursor: result?.cursor ?? null,
1984
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1575
1985
  };
1576
1986
  }
1577
1987
  async addCollectionDocument(appId, collectionId, data) {
@@ -1590,7 +2000,7 @@ export class ApiClient {
1590
2000
  const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
1591
2001
  return {
1592
2002
  items: result?.items ?? [],
1593
- nextCursor: result?.cursor ?? null,
2003
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1594
2004
  };
1595
2005
  }
1596
2006
  // ============================================
@@ -1650,8 +2060,10 @@ export class ApiClient {
1650
2060
  * path-addressed PUT route). `resourceType` / `category` identify the config;
1651
2061
  * the body carries `schema` / `readRule` / `writeRule` / `description`.
1652
2062
  */
1653
- async upsertMetadataCategoryConfig(appId, resourceType, category, data) {
1654
- 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);
1655
2067
  }
1656
2068
  /**
1657
2069
  * Delete a metadata category config (issue #1426, admin-gated route shipped
@@ -1727,6 +2139,112 @@ export class ApiClient {
1727
2139
  async getDocument(appId, documentId) {
1728
2140
  return this.get(`/app/${appId}/api/documents/${documentId}`);
1729
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
+ }
1730
2248
  async createDocument(appId, data) {
1731
2249
  return this.post(`/app/${appId}/api/documents`, data);
1732
2250
  }
@@ -1736,6 +2254,24 @@ export class ApiClient {
1736
2254
  async grantDocumentPermission(appId, documentId, permissions) {
1737
2255
  return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
1738
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
+ }
1739
2275
  async getDocumentLinkAccess(appId, documentId) {
1740
2276
  return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
1741
2277
  }
@@ -1801,15 +2337,16 @@ export class ApiClient {
1801
2337
  mustNotExist,
1802
2338
  });
1803
2339
  }
1804
- async findUserByEmail(appId, email) {
1805
- try {
1806
- const result = await this.get(`/app/${appId}/api/users`, { email });
1807
- const items = result?.items || (Array.isArray(result) ? result : []);
1808
- return items[0] || null;
1809
- }
1810
- catch {
1811
- return null;
1812
- }
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 });
1813
2350
  }
1814
2351
  async listAdminDocuments(appId, userId) {
1815
2352
  const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
@@ -1818,21 +2355,29 @@ export class ApiClient {
1818
2355
  // ============================================
1819
2356
  // DATABASE EXPORT / IMPORT
1820
2357
  // ============================================
1821
- async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
1822
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/save`, {
1823
- modelName,
1824
- id,
1825
- data,
1826
- });
1827
- }
1828
- async batchDatabaseRecords(appId, databaseId, operations) {
1829
- 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);
1830
2375
  }
1831
2376
  async deleteDatabaseRecord(appId, databaseId, modelName, id) {
1832
- 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 });
1833
2378
  }
1834
2379
  async batchDeleteDatabaseRecords(appId, databaseId, operations) {
1835
- return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
2380
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
1836
2381
  }
1837
2382
  async listDatabaseUniqueConstraints(appId, databaseId) {
1838
2383
  const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
@@ -1860,7 +2405,16 @@ export class ApiClient {
1860
2405
  return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1861
2406
  }
1862
2407
  async listBucketBlobs(appId, bucketIdOrKey, params) {
1863
- return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
2408
+ const result = await this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
2409
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
2410
+ // so the CLI keeps paginating against pre-#1316 servers.
2411
+ const nextCursor = result?.nextCursor ?? result?.cursor;
2412
+ return {
2413
+ items: result?.items ?? [],
2414
+ nextCursor,
2415
+ hasMore: result?.hasMore ?? nextCursor != null,
2416
+ cursor: nextCursor,
2417
+ };
1864
2418
  }
1865
2419
  async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
1866
2420
  const credentials = await this.ensureAuthenticated();
@@ -1915,6 +2469,12 @@ export class ApiClient {
1915
2469
  expiresInSeconds: expiresInSeconds || 300,
1916
2470
  });
1917
2471
  }
2472
+ // Blob metadata (#1966): head a single blob without downloading its bytes.
2473
+ // Mirrors listBucketBlobs/downloadBucketBlob addressing; the server heads the
2474
+ // R2 object and returns the serialized BlobInfo shape.
2475
+ async getBucketBlobMetadata(appId, bucketIdOrKey, blobId) {
2476
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
2477
+ }
1918
2478
  }
1919
2479
  // Export a singleton instance
1920
2480
  export const apiClient = new ApiClient();