primitive-admin 1.1.0-alpha.7 → 1.1.0-alpha.71

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 (438) hide show
  1. package/README.md +404 -80
  2. package/assets/skill/skills/primitive-platform/SKILL.md +808 -0
  3. package/dist/bin/primitive.d.ts +2 -0
  4. package/dist/bin/primitive.js +294 -21
  5. package/dist/bin/primitive.js.map +1 -1
  6. package/dist/src/commands/admins.d.ts +2 -0
  7. package/dist/src/commands/admins.js +138 -19
  8. package/dist/src/commands/admins.js.map +1 -1
  9. package/dist/src/commands/analytics.d.ts +2 -0
  10. package/dist/src/commands/analytics.js +544 -55
  11. package/dist/src/commands/analytics.js.map +1 -1
  12. package/dist/src/commands/apps.d.ts +2 -0
  13. package/dist/src/commands/apps.js +51 -96
  14. package/dist/src/commands/apps.js.map +1 -1
  15. package/dist/src/commands/auth.d.ts +2 -0
  16. package/dist/src/commands/auth.js +177 -7
  17. package/dist/src/commands/auth.js.map +1 -1
  18. package/dist/src/commands/blob-buckets.d.ts +2 -0
  19. package/dist/src/commands/blob-buckets.js +330 -0
  20. package/dist/src/commands/blob-buckets.js.map +1 -0
  21. package/dist/src/commands/catalog.d.ts +2 -0
  22. package/dist/src/commands/catalog.js +37 -38
  23. package/dist/src/commands/catalog.js.map +1 -1
  24. package/dist/src/commands/collection-type-configs.d.ts +2 -0
  25. package/dist/src/commands/collection-type-configs.js +92 -0
  26. package/dist/src/commands/collection-type-configs.js.map +1 -0
  27. package/dist/src/commands/collections.d.ts +2 -0
  28. package/dist/src/commands/collections.js +565 -0
  29. package/dist/src/commands/collections.js.map +1 -0
  30. package/dist/src/commands/comparisons.d.ts +2 -0
  31. package/dist/src/commands/comparisons.js +6 -6
  32. package/dist/src/commands/comparisons.js.map +1 -1
  33. package/dist/src/commands/config.d.ts +46 -0
  34. package/dist/src/commands/config.js +479 -0
  35. package/dist/src/commands/config.js.map +1 -0
  36. package/dist/src/commands/connections.d.ts +2 -0
  37. package/dist/src/commands/connections.js +100 -0
  38. package/dist/src/commands/connections.js.map +1 -0
  39. package/dist/src/commands/cron-triggers.d.ts +2 -0
  40. package/dist/src/commands/cron-triggers.js +265 -0
  41. package/dist/src/commands/cron-triggers.js.map +1 -0
  42. package/dist/src/commands/database-type-configs.d.ts +2 -0
  43. package/dist/src/commands/database-type-configs.js +171 -0
  44. package/dist/src/commands/database-type-configs.js.map +1 -0
  45. package/dist/src/commands/database-types.d.ts +2 -0
  46. package/dist/src/commands/database-types.js +471 -0
  47. package/dist/src/commands/database-types.js.map +1 -0
  48. package/dist/src/commands/databases.d.ts +65 -0
  49. package/dist/src/commands/databases.js +2140 -112
  50. package/dist/src/commands/databases.js.map +1 -1
  51. package/dist/src/commands/documents.d.ts +2 -0
  52. package/dist/src/commands/documents.js +1357 -19
  53. package/dist/src/commands/documents.js.map +1 -1
  54. package/dist/src/commands/email-templates.d.ts +2 -0
  55. package/dist/src/commands/email-templates.js +174 -0
  56. package/dist/src/commands/email-templates.js.map +1 -0
  57. package/dist/src/commands/env.d.ts +23 -0
  58. package/dist/src/commands/env.js +333 -0
  59. package/dist/src/commands/env.js.map +1 -0
  60. package/dist/src/commands/feature-flags.d.ts +14 -0
  61. package/dist/src/commands/feature-flags.js +116 -0
  62. package/dist/src/commands/feature-flags.js.map +1 -0
  63. package/dist/src/commands/group-type-configs.d.ts +2 -0
  64. package/dist/src/commands/group-type-configs.js +86 -0
  65. package/dist/src/commands/group-type-configs.js.map +1 -0
  66. package/dist/src/commands/groups.d.ts +2 -0
  67. package/dist/src/commands/groups.js +38 -99
  68. package/dist/src/commands/groups.js.map +1 -1
  69. package/dist/src/commands/guides.d.ts +223 -0
  70. package/dist/src/commands/guides.js +617 -65
  71. package/dist/src/commands/guides.js.map +1 -1
  72. package/dist/src/commands/init.d.ts +25 -0
  73. package/dist/src/commands/init.js +1605 -208
  74. package/dist/src/commands/init.js.map +1 -1
  75. package/dist/src/commands/integrations.d.ts +2 -0
  76. package/dist/src/commands/integrations.js +380 -178
  77. package/dist/src/commands/integrations.js.map +1 -1
  78. package/dist/src/commands/llm.d.ts +2 -0
  79. package/dist/src/commands/llm.js +4 -2
  80. package/dist/src/commands/llm.js.map +1 -1
  81. package/dist/src/commands/locks.d.ts +8 -0
  82. package/dist/src/commands/locks.js +160 -0
  83. package/dist/src/commands/locks.js.map +1 -0
  84. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  85. package/dist/src/commands/metadata-category-configs.js +112 -0
  86. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  87. package/dist/src/commands/metadata.d.ts +2 -0
  88. package/dist/src/commands/metadata.js +281 -0
  89. package/dist/src/commands/metadata.js.map +1 -0
  90. package/dist/src/commands/prompts.d.ts +2 -0
  91. package/dist/src/commands/prompts.js +225 -584
  92. package/dist/src/commands/prompts.js.map +1 -1
  93. package/dist/src/commands/rule-sets.d.ts +3 -0
  94. package/dist/src/commands/rule-sets.js +272 -0
  95. package/dist/src/commands/rule-sets.js.map +1 -0
  96. package/dist/src/commands/scripts.d.ts +20 -0
  97. package/dist/src/commands/scripts.js +554 -0
  98. package/dist/src/commands/scripts.js.map +1 -0
  99. package/dist/src/commands/secrets.d.ts +2 -0
  100. package/dist/src/commands/secrets.js +108 -0
  101. package/dist/src/commands/secrets.js.map +1 -0
  102. package/dist/src/commands/sessions.d.ts +2 -0
  103. package/dist/src/commands/sessions.js +75 -0
  104. package/dist/src/commands/sessions.js.map +1 -0
  105. package/dist/src/commands/skill.d.ts +2 -0
  106. package/dist/src/commands/skill.js +29 -0
  107. package/dist/src/commands/skill.js.map +1 -0
  108. package/dist/src/commands/sync-app-settings.d.ts +158 -0
  109. package/dist/src/commands/sync-app-settings.js +330 -0
  110. package/dist/src/commands/sync-app-settings.js.map +1 -0
  111. package/dist/src/commands/sync.d.ts +2323 -0
  112. package/dist/src/commands/sync.js +15065 -843
  113. package/dist/src/commands/sync.js.map +1 -1
  114. package/dist/src/commands/tokens.d.ts +2 -0
  115. package/dist/src/commands/tokens.js +130 -21
  116. package/dist/src/commands/tokens.js.map +1 -1
  117. package/dist/src/commands/users.d.ts +2 -0
  118. package/dist/src/commands/users.js +532 -23
  119. package/dist/src/commands/users.js.map +1 -1
  120. package/dist/src/commands/vars.d.ts +8 -0
  121. package/dist/src/commands/vars.js +96 -0
  122. package/dist/src/commands/vars.js.map +1 -0
  123. package/dist/src/commands/waitlist.d.ts +2 -0
  124. package/dist/src/commands/waitlist.js +10 -10
  125. package/dist/src/commands/waitlist.js.map +1 -1
  126. package/dist/src/commands/webhooks.d.ts +2 -0
  127. package/dist/src/commands/webhooks.js +562 -0
  128. package/dist/src/commands/webhooks.js.map +1 -0
  129. package/dist/src/commands/workflows.d.ts +116 -0
  130. package/dist/src/commands/workflows.js +1583 -681
  131. package/dist/src/commands/workflows.js.map +1 -1
  132. package/dist/src/lib/access-rule-display.d.ts +21 -0
  133. package/dist/src/lib/access-rule-display.js +34 -0
  134. package/dist/src/lib/access-rule-display.js.map +1 -0
  135. package/dist/src/lib/api-client.d.ts +1936 -0
  136. package/dist/src/lib/api-client.js +1826 -138
  137. package/dist/src/lib/api-client.js.map +1 -1
  138. package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
  139. package/dist/src/lib/app-settings-descriptor.js +575 -0
  140. package/dist/src/lib/app-settings-descriptor.js.map +1 -0
  141. package/dist/src/lib/auth-flow.d.ts +8 -0
  142. package/dist/src/lib/batch.d.ts +26 -0
  143. package/dist/src/lib/batch.js +32 -0
  144. package/dist/src/lib/batch.js.map +1 -0
  145. package/dist/src/lib/block-layout.d.ts +160 -0
  146. package/dist/src/lib/block-layout.js +451 -0
  147. package/dist/src/lib/block-layout.js.map +1 -0
  148. package/dist/src/lib/canonical-json.d.ts +12 -0
  149. package/dist/src/lib/canonical-json.js +35 -0
  150. package/dist/src/lib/canonical-json.js.map +1 -0
  151. package/dist/src/lib/channel.d.ts +30 -0
  152. package/dist/src/lib/channel.js +68 -0
  153. package/dist/src/lib/channel.js.map +1 -0
  154. package/dist/src/lib/cli-manifest.d.ts +68 -0
  155. package/dist/src/lib/cli-manifest.js +71 -0
  156. package/dist/src/lib/cli-manifest.js.map +1 -0
  157. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
  158. package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
  159. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
  160. package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
  161. package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
  162. package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
  163. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +68 -0
  164. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
  165. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
  166. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  167. package/dist/src/lib/config-object-descriptor.js +658 -0
  168. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  169. package/dist/src/lib/config-payload.d.ts +85 -0
  170. package/dist/src/lib/config-payload.js +116 -0
  171. package/dist/src/lib/config-payload.js.map +1 -0
  172. package/dist/src/lib/config-surface.d.ts +130 -0
  173. package/dist/src/lib/config-surface.js +300 -0
  174. package/dist/src/lib/config-surface.js.map +1 -0
  175. package/dist/src/lib/config-toml.d.ts +10 -0
  176. package/dist/src/lib/config-toml.js +42 -0
  177. package/dist/src/lib/config-toml.js.map +1 -0
  178. package/dist/src/lib/config.d.ts +53 -0
  179. package/dist/src/lib/config.js +92 -53
  180. package/dist/src/lib/config.js.map +1 -1
  181. package/dist/src/lib/confirm-prompt.d.ts +83 -0
  182. package/dist/src/lib/confirm-prompt.js +110 -0
  183. package/dist/src/lib/confirm-prompt.js.map +1 -0
  184. package/dist/src/lib/constants.d.ts +11 -0
  185. package/dist/src/lib/constants.js +12 -0
  186. package/dist/src/lib/constants.js.map +1 -0
  187. package/dist/src/lib/crash-handlers.d.ts +20 -0
  188. package/dist/src/lib/crash-handlers.js +49 -0
  189. package/dist/src/lib/crash-handlers.js.map +1 -0
  190. package/dist/src/lib/credentials-store.d.ts +89 -0
  191. package/dist/src/lib/credentials-store.js +330 -0
  192. package/dist/src/lib/credentials-store.js.map +1 -0
  193. package/dist/src/lib/csv.d.ts +47 -0
  194. package/dist/src/lib/csv.js +172 -0
  195. package/dist/src/lib/csv.js.map +1 -0
  196. package/dist/src/lib/data-input.d.ts +23 -0
  197. package/dist/src/lib/data-input.js +50 -0
  198. package/dist/src/lib/data-input.js.map +1 -0
  199. package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
  200. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  201. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  202. package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
  203. package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
  204. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  205. package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
  206. package/dist/src/lib/db-codegen/dbNaming.js +180 -0
  207. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  208. package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
  209. package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
  210. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  211. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
  212. package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
  213. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  214. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
  215. package/dist/src/lib/db-codegen/dbTypeIR.js +517 -0
  216. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
  217. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
  218. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
  219. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
  220. package/dist/src/lib/env-resolver-core.d.ts +147 -0
  221. package/dist/src/lib/env-resolver-core.js +265 -0
  222. package/dist/src/lib/env-resolver-core.js.map +1 -0
  223. package/dist/src/lib/env-resolver.d.ts +84 -0
  224. package/dist/src/lib/env-resolver.js +133 -0
  225. package/dist/src/lib/env-resolver.js.map +1 -0
  226. package/dist/src/lib/fetch.d.ts +5 -0
  227. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  228. package/dist/src/lib/generated-allowlist.js +277 -0
  229. package/dist/src/lib/generated-allowlist.js.map +1 -0
  230. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  231. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  232. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  233. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  234. package/dist/src/lib/generated-template-lint.js +624 -0
  235. package/dist/src/lib/generated-template-lint.js.map +1 -0
  236. package/dist/src/lib/init-adopt.d.ts +16 -0
  237. package/dist/src/lib/init-adopt.js +34 -0
  238. package/dist/src/lib/init-adopt.js.map +1 -0
  239. package/dist/src/lib/init-assets.d.ts +39 -0
  240. package/dist/src/lib/init-assets.js +97 -0
  241. package/dist/src/lib/init-assets.js.map +1 -0
  242. package/dist/src/lib/init-config.d.ts +98 -0
  243. package/dist/src/lib/init-config.js +186 -0
  244. package/dist/src/lib/init-config.js.map +1 -0
  245. package/dist/src/lib/init-ios-links.d.ts +50 -0
  246. package/dist/src/lib/init-ios-links.js +153 -0
  247. package/dist/src/lib/init-ios-links.js.map +1 -0
  248. package/dist/src/lib/init-plan.d.ts +80 -0
  249. package/dist/src/lib/init-plan.js +95 -0
  250. package/dist/src/lib/init-plan.js.map +1 -0
  251. package/dist/src/lib/init-production-env.d.ts +48 -0
  252. package/dist/src/lib/init-production-env.js +59 -0
  253. package/dist/src/lib/init-production-env.js.map +1 -0
  254. package/dist/src/lib/init-schema.d.ts +74 -0
  255. package/dist/src/lib/init-schema.js +358 -0
  256. package/dist/src/lib/init-schema.js.map +1 -0
  257. package/dist/src/lib/init-xcode.d.ts +33 -0
  258. package/dist/src/lib/init-xcode.js +114 -0
  259. package/dist/src/lib/init-xcode.js.map +1 -0
  260. package/dist/src/lib/integration-request-config.d.ts +30 -0
  261. package/dist/src/lib/integration-request-config.js +145 -0
  262. package/dist/src/lib/integration-request-config.js.map +1 -0
  263. package/dist/src/lib/local-state.d.ts +55 -0
  264. package/dist/src/lib/local-state.js +167 -0
  265. package/dist/src/lib/local-state.js.map +1 -0
  266. package/dist/src/lib/log-inspection.d.ts +568 -0
  267. package/dist/src/lib/log-inspection.js +639 -0
  268. package/dist/src/lib/log-inspection.js.map +1 -0
  269. package/dist/src/lib/migration-nag.d.ts +49 -0
  270. package/dist/src/lib/migration-nag.js +163 -0
  271. package/dist/src/lib/migration-nag.js.map +1 -0
  272. package/dist/src/lib/object-status-filter.d.ts +22 -0
  273. package/dist/src/lib/object-status-filter.js +45 -0
  274. package/dist/src/lib/object-status-filter.js.map +1 -0
  275. package/dist/src/lib/output.d.ts +109 -0
  276. package/dist/src/lib/output.js +191 -8
  277. package/dist/src/lib/output.js.map +1 -1
  278. package/dist/src/lib/package-manager.d.ts +140 -0
  279. package/dist/src/lib/package-manager.js +305 -0
  280. package/dist/src/lib/package-manager.js.map +1 -0
  281. package/dist/src/lib/paginate.d.ts +83 -0
  282. package/dist/src/lib/paginate.js +95 -0
  283. package/dist/src/lib/paginate.js.map +1 -0
  284. package/dist/src/lib/platform-owned.d.ts +63 -0
  285. package/dist/src/lib/platform-owned.js +85 -0
  286. package/dist/src/lib/platform-owned.js.map +1 -0
  287. package/dist/src/lib/project-config.d.ts +97 -0
  288. package/dist/src/lib/project-config.js +217 -0
  289. package/dist/src/lib/project-config.js.map +1 -0
  290. package/dist/src/lib/query-operators.d.ts +43 -0
  291. package/dist/src/lib/query-operators.js +80 -0
  292. package/dist/src/lib/query-operators.js.map +1 -0
  293. package/dist/src/lib/record-filter.d.ts +18 -0
  294. package/dist/src/lib/record-filter.js +55 -0
  295. package/dist/src/lib/record-filter.js.map +1 -0
  296. package/dist/src/lib/refresh-admin-credentials.d.ts +65 -0
  297. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  298. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  299. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  300. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  301. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  302. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  303. package/dist/src/lib/resolve-init-server.js +77 -0
  304. package/dist/src/lib/resolve-init-server.js.map +1 -0
  305. package/dist/src/lib/resolve-platform.d.ts +74 -0
  306. package/dist/src/lib/resolve-platform.js +105 -0
  307. package/dist/src/lib/resolve-platform.js.map +1 -0
  308. package/dist/src/lib/run-status.d.ts +19 -0
  309. package/dist/src/lib/run-status.generated.d.ts +39 -0
  310. package/dist/src/lib/run-status.generated.js +66 -0
  311. package/dist/src/lib/run-status.generated.js.map +1 -0
  312. package/dist/src/lib/run-status.js +19 -0
  313. package/dist/src/lib/run-status.js.map +1 -0
  314. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  315. package/dist/src/lib/server-text-normalization.js +90 -0
  316. package/dist/src/lib/server-text-normalization.js.map +1 -0
  317. package/dist/src/lib/server-url.d.ts +22 -0
  318. package/dist/src/lib/server-url.js +33 -0
  319. package/dist/src/lib/server-url.js.map +1 -0
  320. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  321. package/dist/src/lib/signing-secret-status.js +116 -0
  322. package/dist/src/lib/signing-secret-status.js.map +1 -0
  323. package/dist/src/lib/skill-installer.d.ts +25 -0
  324. package/dist/src/lib/skill-installer.js +266 -0
  325. package/dist/src/lib/skill-installer.js.map +1 -0
  326. package/dist/src/lib/snapshots.d.ts +99 -0
  327. package/dist/src/lib/snapshots.js +357 -0
  328. package/dist/src/lib/snapshots.js.map +1 -0
  329. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
  330. package/dist/src/lib/swift-codegen/dbGenerator.js +914 -0
  331. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
  332. package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
  333. package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
  334. package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
  335. package/dist/src/lib/swift-codegen/generator.d.ts +94 -0
  336. package/dist/src/lib/swift-codegen/generator.js +440 -0
  337. package/dist/src/lib/swift-codegen/generator.js.map +1 -0
  338. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
  339. package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -0
  340. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
  341. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
  342. package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
  343. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
  344. package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
  345. package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
  346. package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
  347. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  348. package/dist/src/lib/sync-dir-selector.js +30 -0
  349. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  350. package/dist/src/lib/sync-paths.d.ts +111 -0
  351. package/dist/src/lib/sync-paths.js +198 -0
  352. package/dist/src/lib/sync-paths.js.map +1 -0
  353. package/dist/src/lib/sync-resource-types.d.ts +544 -0
  354. package/dist/src/lib/sync-resource-types.js +975 -0
  355. package/dist/src/lib/sync-resource-types.js.map +1 -0
  356. package/dist/src/lib/sync-selectors.d.ts +95 -0
  357. package/dist/src/lib/sync-selectors.js +228 -0
  358. package/dist/src/lib/sync-selectors.js.map +1 -0
  359. package/dist/src/lib/template.d.ts +170 -0
  360. package/dist/src/lib/template.js +484 -68
  361. package/dist/src/lib/template.js.map +1 -1
  362. package/dist/src/lib/test-case-keys.d.ts +29 -0
  363. package/dist/src/lib/test-case-keys.js +55 -0
  364. package/dist/src/lib/test-case-keys.js.map +1 -0
  365. package/dist/src/lib/test-case-variables.d.ts +15 -0
  366. package/dist/src/lib/test-case-variables.js +29 -0
  367. package/dist/src/lib/test-case-variables.js.map +1 -0
  368. package/dist/src/lib/token-inject.d.ts +56 -0
  369. package/dist/src/lib/token-inject.js +204 -0
  370. package/dist/src/lib/token-inject.js.map +1 -0
  371. package/dist/src/lib/toml-database-config.d.ts +123 -0
  372. package/dist/src/lib/toml-database-config.js +527 -0
  373. package/dist/src/lib/toml-database-config.js.map +1 -0
  374. package/dist/src/lib/toml-metadata-config.d.ts +151 -0
  375. package/dist/src/lib/toml-metadata-config.js +476 -0
  376. package/dist/src/lib/toml-metadata-config.js.map +1 -0
  377. package/dist/src/lib/toml-native-form.d.ts +46 -0
  378. package/dist/src/lib/toml-native-form.js +78 -0
  379. package/dist/src/lib/toml-native-form.js.map +1 -0
  380. package/dist/src/lib/toml-params-validator.d.ts +129 -0
  381. package/dist/src/lib/toml-params-validator.js +298 -0
  382. package/dist/src/lib/toml-params-validator.js.map +1 -0
  383. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  384. package/dist/src/lib/toml-scalar-edit.js +283 -0
  385. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  386. package/dist/src/lib/user-selector.d.ts +24 -0
  387. package/dist/src/lib/user-selector.js +33 -0
  388. package/dist/src/lib/user-selector.js.map +1 -0
  389. package/dist/src/lib/version-check.d.ts +35 -0
  390. package/dist/src/lib/version-check.js +241 -0
  391. package/dist/src/lib/version-check.js.map +1 -0
  392. package/dist/src/lib/watch.d.ts +121 -0
  393. package/dist/src/lib/watch.js +169 -0
  394. package/dist/src/lib/watch.js.map +1 -0
  395. package/dist/src/lib/workflow-apply.d.ts +110 -0
  396. package/dist/src/lib/workflow-apply.js +164 -0
  397. package/dist/src/lib/workflow-apply.js.map +1 -0
  398. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
  399. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
  400. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
  401. package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
  402. package/dist/src/lib/workflow-codegen/generator.js +361 -0
  403. package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
  404. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
  405. package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
  406. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
  407. package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
  408. package/dist/src/lib/workflow-codegen/naming.js +81 -0
  409. package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
  410. package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
  411. package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
  412. package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
  413. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  414. package/dist/src/lib/workflow-config-apply.js +137 -0
  415. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  416. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  417. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  418. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  419. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  420. package/dist/src/lib/workflow-defaults.js +41 -0
  421. package/dist/src/lib/workflow-defaults.js.map +1 -0
  422. package/dist/src/lib/workflow-fragments.d.ts +64 -0
  423. package/dist/src/lib/workflow-fragments.js +342 -0
  424. package/dist/src/lib/workflow-fragments.js.map +1 -0
  425. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  426. package/dist/src/lib/workflow-include-preserve.js +286 -0
  427. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  428. package/dist/src/lib/workflow-payload.d.ts +98 -0
  429. package/dist/src/lib/workflow-payload.js +178 -0
  430. package/dist/src/lib/workflow-payload.js.map +1 -0
  431. package/dist/src/lib/workflow-toml-validator.d.ts +202 -0
  432. package/dist/src/lib/workflow-toml-validator.js +757 -0
  433. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  434. package/dist/src/types/index.d.ts +581 -0
  435. package/dist/src/validators.d.ts +65 -0
  436. package/dist/src/validators.js +64 -0
  437. package/dist/src/validators.js.map +1 -0
  438. package/package.json +32 -8
@@ -1,25 +1,206 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
2
  import { fetchWithTLS } from "./fetch.js";
3
+ import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
4
+ import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
3
5
  export class ApiError extends Error {
4
6
  statusCode;
5
7
  code;
6
- constructor(message, statusCode, code) {
8
+ /**
9
+ * Structured `details` payload from `corsErrorResponse`.
10
+ *
11
+ * The server's error envelope can emit `details` as either:
12
+ * - an array of validation issues (e.g. `[{ path, message }, ...]`) —
13
+ * this is what most legacy endpoints produce, and what sync.ts walks
14
+ * via `Array.isArray(err.details)` / `for (const detail of ...)`.
15
+ * - a record of structured offender fields (e.g. `{ refs, operations,
16
+ * opCount, line, column, ... }`) — emitted by the issue #666 schema
17
+ * gate and consumed by the typed exception subclasses below.
18
+ *
19
+ * Callers must narrow before use: `Array.isArray(err.details)` for the
20
+ * legacy shape, otherwise treat as `Record<string, any>`.
21
+ */
22
+ details;
23
+ constructor(message, statusCode, code, details) {
7
24
  super(message);
8
25
  this.statusCode = statusCode;
9
26
  this.code = code;
10
27
  this.name = "ApiError";
28
+ if (details !== undefined) {
29
+ this.details = details;
30
+ }
11
31
  }
12
32
  }
13
33
  export class ConflictError extends ApiError {
14
34
  serverModifiedAt;
15
35
  expectedModifiedAt;
16
- constructor(message, serverModifiedAt, expectedModifiedAt) {
17
- super(message, 409, "CONFLICT");
36
+ constructor(message, serverModifiedAt, expectedModifiedAt, details) {
37
+ super(message, 409, "CONFLICT", details);
18
38
  this.serverModifiedAt = serverModifiedAt;
19
39
  this.expectedModifiedAt = expectedModifiedAt;
20
40
  this.name = "ConflictError";
21
41
  }
22
42
  }
43
+ /**
44
+ * Extract a human-readable error message + structured fields from a non-OK
45
+ * HTTP response body. Single source of truth used by every error-handler call
46
+ * site in this file (see issue #684).
47
+ */
48
+ export function parseErrorResponse(response, text, path) {
49
+ // Empty body → fall back to status code.
50
+ if (!text) {
51
+ return { message: `HTTP ${response.status}` };
52
+ }
53
+ let errorData;
54
+ try {
55
+ errorData = JSON.parse(text);
56
+ }
57
+ catch {
58
+ // Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
59
+ // 404 page from hitting the wrong path) so we don't regress the helpful
60
+ // "API endpoint not found" message at api-client.ts:343.
61
+ if (text.includes("<!DOCTYPE")) {
62
+ const where = path ? `: ${path}` : "";
63
+ return {
64
+ message: `API endpoint not found${where}. Make sure the server is running.`,
65
+ htmlNotFound: true,
66
+ };
67
+ }
68
+ // Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
69
+ // raw text so the operator at least sees what the server returned.
70
+ return { message: text };
71
+ }
72
+ // Server's standard envelope uses `error`; ConflictError + integrations
73
+ // proxy use `message`. Prefer `error` (more common), fall back to `message`.
74
+ const message = (typeof errorData?.error === "string" && errorData.error) ||
75
+ (typeof errorData?.message === "string" && errorData.message) ||
76
+ `HTTP ${response.status}`;
77
+ // Per issue #666 addendum A1, `code` may be at the top level or nested
78
+ // under `details.code` when the server's bespoke envelope didn't flatten.
79
+ const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
80
+ (typeof errorData?.details?.code === "string"
81
+ ? errorData.details.code
82
+ : undefined);
83
+ // Accept either an array (legacy) or a plain object (#666 schema gate).
84
+ const details = Array.isArray(errorData?.details)
85
+ ? errorData.details
86
+ : errorData?.details && typeof errorData.details === "object"
87
+ ? errorData.details
88
+ : undefined;
89
+ return { message, code, details, raw: errorData };
90
+ }
91
+ /**
92
+ * Typed exception classes for the database-schema feature (issue #666).
93
+ * Each maps 1:1 to a server `code` value emitted from the op-edit or
94
+ * schema-edit gate. They all extend ApiError so existing catch-all paths
95
+ * continue to work; specialized catch blocks can branch on `instanceof`.
96
+ *
97
+ * Per round-2 addendum A1, `details` is always preserved so callers can
98
+ * extract structured offender lists (refs[], operations[], etc.).
99
+ */
100
+ export class SchemaRequiredError extends ApiError {
101
+ constructor(message, details) {
102
+ super(message, 422, "SCHEMA_REQUIRED", details);
103
+ this.name = "SchemaRequiredError";
104
+ }
105
+ }
106
+ function detailsRecord(details) {
107
+ return details && !Array.isArray(details) && typeof details === "object"
108
+ ? details
109
+ : undefined;
110
+ }
111
+ export class OperationRefError extends ApiError {
112
+ constructor(message, details) {
113
+ super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
114
+ this.name = "OperationRefError";
115
+ }
116
+ get refs() {
117
+ const d = detailsRecord(this.details);
118
+ return Array.isArray(d?.refs) ? d.refs : [];
119
+ }
120
+ }
121
+ export class SchemaBreaksOpsError extends ApiError {
122
+ constructor(message, details) {
123
+ super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
124
+ this.name = "SchemaBreaksOpsError";
125
+ }
126
+ get operations() {
127
+ const d = detailsRecord(this.details);
128
+ return Array.isArray(d?.operations) ? d.operations : [];
129
+ }
130
+ }
131
+ export class SchemaHasUncheckableOpsError extends ApiError {
132
+ constructor(message, details) {
133
+ super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
134
+ this.name = "SchemaHasUncheckableOpsError";
135
+ }
136
+ get operations() {
137
+ const d = detailsRecord(this.details);
138
+ return Array.isArray(d?.operations) ? d.operations : [];
139
+ }
140
+ }
141
+ export class TomlParseError extends ApiError {
142
+ constructor(message, details) {
143
+ super(message, 400, "TOML_PARSE_ERROR", details);
144
+ this.name = "TomlParseError";
145
+ }
146
+ get line() {
147
+ const d = detailsRecord(this.details);
148
+ return typeof d?.line === "number" ? d.line : undefined;
149
+ }
150
+ get column() {
151
+ const d = detailsRecord(this.details);
152
+ return typeof d?.column === "number" ? d.column : undefined;
153
+ }
154
+ }
155
+ export class OpsExistError extends ApiError {
156
+ constructor(message, details) {
157
+ super(message, 409, "OPS_EXIST", details);
158
+ this.name = "OpsExistError";
159
+ }
160
+ get opCount() {
161
+ const d = detailsRecord(this.details);
162
+ return typeof d?.opCount === "number" ? d.opCount : 0;
163
+ }
164
+ }
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
+ }
23
204
  export class ApiClient {
24
205
  credentials = null;
25
206
  constructor() {
@@ -39,39 +220,24 @@ export class ApiClient {
39
220
  return this.credentials;
40
221
  }
41
222
  async refreshToken() {
42
- if (!this.credentials?.refreshToken) {
43
- throw new ApiError("No refresh token available. Please login again.", 401);
223
+ if (!this.credentials) {
224
+ throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
44
225
  }
45
- const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
46
226
  try {
47
- const headers = {
48
- "Content-Type": "application/json",
49
- };
50
- if (this.credentials.globalAdminAppId) {
51
- headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
52
- }
53
- const response = await fetchWithTLS(url, {
54
- method: "POST",
55
- headers,
56
- body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
57
- });
58
- if (!response.ok) {
59
- throw new ApiError("Token refresh failed. Please login again.", 401);
60
- }
61
- const data = await response.json();
62
- // Update credentials with new tokens
63
- this.credentials = {
64
- ...this.credentials,
65
- accessToken: data.accessToken || data.token,
66
- refreshToken: data.refreshToken || this.credentials.refreshToken,
67
- expiresAt: data.expiresAt,
68
- };
227
+ const updated = await refreshAdminCredentials(this.credentials);
228
+ this.credentials = updated;
69
229
  saveCredentials(this.credentials);
70
230
  }
71
- catch (error) {
72
- if (error instanceof ApiError)
73
- throw error;
74
- throw new ApiError("Token refresh failed. Please login again.", 401);
231
+ catch (err) {
232
+ if (err instanceof RefreshError) {
233
+ // Preserve historical behavior: ApiClient surfaces refresh failures
234
+ // as 401s regardless of whether the underlying cause was a network
235
+ // error or a server-side rejection. Callers that need a finer
236
+ // distinction (e.g. `primitive token`) consume RefreshError directly
237
+ // from refresh-admin-credentials.ts instead of going through here.
238
+ throw new ApiError("Token refresh failed. Please login again.", 401);
239
+ }
240
+ throw err;
75
241
  }
76
242
  }
77
243
  async request(path, options = {}) {
@@ -92,20 +258,42 @@ export class ApiClient {
92
258
  });
93
259
  const text = await response.text();
94
260
  if (!response.ok) {
95
- let errorData;
96
- try {
97
- errorData = JSON.parse(text);
261
+ const parsed = parseErrorResponse(response, text, path);
262
+ // Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
263
+ if (parsed.htmlNotFound) {
264
+ throw new ApiError(parsed.message, 404);
98
265
  }
99
- catch {
100
- if (text.includes("<!DOCTYPE")) {
101
- throw new ApiError(`API endpoint not found: ${path}. Make sure the server is running.`, 404);
102
- }
103
- errorData = { message: text || `HTTP ${response.status}` };
266
+ // Narrow details to the record shape for the issue #666 typed-exception
267
+ // dispatch below. Conflict metadata may live under `details.*` (canonical
268
+ // location per A1) or on the top-level envelope (legacy).
269
+ const detailsRecord = parsed.details && !Array.isArray(parsed.details)
270
+ ? parsed.details
271
+ : undefined;
272
+ const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
273
+ const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
274
+ // Typed exceptions for the schema-feature (issue #666).
275
+ if (response.status === 409 && parsed.code === "CONFLICT") {
276
+ throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
277
+ }
278
+ if (response.status === 409 && parsed.code === "OPS_EXIST") {
279
+ throw new OpsExistError(parsed.message, detailsRecord);
280
+ }
281
+ if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
282
+ throw new TomlParseError(parsed.message, detailsRecord);
283
+ }
284
+ if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
285
+ throw new SchemaRequiredError(parsed.message, detailsRecord);
286
+ }
287
+ if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
288
+ throw new OperationRefError(parsed.message, detailsRecord);
104
289
  }
105
- if (response.status === 409 && errorData?.code === "CONFLICT") {
106
- throw new ConflictError(errorData.message || "Resource conflict", errorData.serverModifiedAt, errorData.expectedModifiedAt);
290
+ if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
291
+ throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
107
292
  }
108
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
293
+ if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
294
+ throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
295
+ }
296
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
109
297
  }
110
298
  return text ? JSON.parse(text) : null;
111
299
  }
@@ -155,7 +343,25 @@ export class ApiClient {
155
343
  // APPS
156
344
  // ============================================
157
345
  async listApps() {
158
- return this.get("/admin/api/admins/me/apps");
346
+ // The server paginates cursor-style (default pageSize=25, cap=100). Historically
347
+ // this method issued a single un-paginated GET and silently truncated to the
348
+ // first page, which broke `primitive use <app-id>` and `primitive apps list`
349
+ // for admins with more than 25 apps (see #436). Iterate every page.
350
+ const apps = await paginateAll(async (cursor) => {
351
+ const qs = new URLSearchParams({ limit: "100" });
352
+ if (cursor)
353
+ qs.set("cursor", cursor);
354
+ const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
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
+ };
363
+ });
364
+ return { apps };
159
365
  }
160
366
  async createApp(data) {
161
367
  return this.post("/admin/api/apps", data);
@@ -181,12 +387,70 @@ export class ApiClient {
181
387
  async addUserByEmail(appId, data) {
182
388
  return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
183
389
  }
184
- async listUsers(appId) {
185
- return this.get(`/app/${appId}/api/users`);
390
+ async mintTestJwt(appId, userId, role) {
391
+ return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
392
+ }
393
+ async rebuildUserSearchText(appId) {
394
+ return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
395
+ }
396
+ async listUsers(appId, params) {
397
+ const query = new URLSearchParams();
398
+ if (params?.name)
399
+ query.set("name", params.name);
400
+ if (params?.email)
401
+ query.set("email", params.email);
402
+ if (params?.userId)
403
+ query.set("userId", params.userId);
404
+ if (params?.limit)
405
+ query.set("limit", String(params.limit));
406
+ if (params?.cursor)
407
+ query.set("cursor", params.cursor);
408
+ const path = query.toString()
409
+ ? `/app/${appId}/api/users?${query.toString()}`
410
+ : `/app/${appId}/api/users`;
411
+ const result = await this.get(path);
412
+ return {
413
+ items: result?.items ?? (Array.isArray(result) ? result : []),
414
+ nextCursor: result?.nextCursor ?? null,
415
+ };
186
416
  }
187
417
  async removeUser(appId, userId) {
188
418
  return this.delete(`/app/${appId}/api/users/${userId}`);
189
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
+ }
190
454
  async updateUserRole(appId, userId, role) {
191
455
  return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
192
456
  }
@@ -196,14 +460,39 @@ export class ApiClient {
196
460
  async transferAdminOwnership(appId, adminId) {
197
461
  return this.post(`/admin/api/apps/${appId}/admins/transfer-ownership`, { adminId });
198
462
  }
463
+ // App-level console admin management
464
+ async listAppAdmins(appId) {
465
+ return this.get(`/admin/api/apps/${appId}/admins`);
466
+ }
467
+ async addAppAdmin(appId, data) {
468
+ return this.post(`/admin/api/apps/${appId}/admins`, data);
469
+ }
470
+ async removeAppAdmin(appId, adminId) {
471
+ return this.delete(`/admin/api/apps/${appId}/admins/${adminId}`);
472
+ }
473
+ async listAppAdminInvitations(appId) {
474
+ return this.get(`/admin/api/apps/${appId}/admin-invitations`);
475
+ }
476
+ async deleteAppAdminInvitation(appId, invitationId) {
477
+ return this.delete(`/admin/api/apps/${appId}/admin-invitations/${invitationId}`);
478
+ }
199
479
  async transferDocumentOwnership(appId, documentId, newOwnerId) {
200
480
  return this.post(`/app/${appId}/api/documents/${documentId}/permissions/transfer`, { newOwnerId });
201
481
  }
202
482
  // ============================================
203
483
  // INVITATIONS
204
484
  // ============================================
205
- async listInvitations(appId) {
206
- return this.get(`/app/${appId}/api/invitations`);
485
+ async listInvitations(appId, 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
+ };
207
496
  }
208
497
  async createInvitation(appId, data) {
209
498
  return this.post(`/app/${appId}/api/invitations`, data);
@@ -228,6 +517,20 @@ export class ApiClient {
228
517
  return this.delete(`/app/${appId}/api/waitlist/${waitlistId}`);
229
518
  }
230
519
  // ============================================
520
+ // DEFERRED GRANTS
521
+ // ============================================
522
+ async listDeferredGrants(appId, params) {
523
+ const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
524
+ return {
525
+ grants: result?.grants ?? result?.items ?? [],
526
+ nextCursor: result?.nextCursor ?? null,
527
+ hasMore: result?.hasMore,
528
+ };
529
+ }
530
+ async revokeDeferredGrant(appId, deferredId, type) {
531
+ return this.delete(`/app/${appId}/api/deferred-grants/${deferredId}?type=${type}`);
532
+ }
533
+ // ============================================
231
534
  // INTEGRATIONS
232
535
  // ============================================
233
536
  async listIntegrations(appId, params) {
@@ -237,9 +540,19 @@ export class ApiClient {
237
540
  nextCursor: result?.nextCursor ?? null,
238
541
  };
239
542
  }
543
+ /** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
240
544
  async getIntegration(appId, integrationId) {
241
545
  return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
242
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
+ }
243
556
  async createIntegration(appId, payload) {
244
557
  return this.post(`/admin/api/apps/${appId}/integrations`, payload);
245
558
  }
@@ -247,8 +560,15 @@ export class ApiClient {
247
560
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
248
561
  return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
249
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
+ */
250
569
  async deleteIntegration(appId, integrationId, hard) {
251
- const path = hard
570
+ const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
571
+ const path = isHard
252
572
  ? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
253
573
  : `/admin/api/apps/${appId}/integrations/${integrationId}`;
254
574
  return this.delete(path);
@@ -260,17 +580,279 @@ export class ApiClient {
260
580
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
261
581
  return result?.items ?? [];
262
582
  }
263
- async listIntegrationSecrets(appId, integrationId, params) {
264
- const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
583
+ async listWorkflowRunIntegrationLogs(appId, runId, params) {
584
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
585
+ return result?.items ?? [];
586
+ }
587
+ // ============================================
588
+ // APP SECRETS
589
+ // ============================================
590
+ async listAppSecrets(appId) {
591
+ const result = await this.get(`/admin/api/apps/${appId}/secrets`);
592
+ return result?.items ?? [];
593
+ }
594
+ async createAppSecret(appId, payload) {
595
+ return this.post(`/admin/api/apps/${appId}/secrets`, payload);
596
+ }
597
+ async updateAppSecret(appId, secretId, payload) {
598
+ return this.put(`/admin/api/apps/${appId}/secrets/${secretId}`, payload);
599
+ }
600
+ async upsertAppSecret(appId, key, payload) {
601
+ return this.put(`/admin/api/apps/${appId}/secrets/by-key/${key}`, payload);
602
+ }
603
+ async deleteAppSecret(appId, secretId) {
604
+ return this.delete(`/admin/api/apps/${appId}/secrets/${secretId}`);
605
+ }
606
+ // ============================================
607
+ // APP CONFIG VARS (issue #1364 — non-secret twin of secrets)
608
+ // ============================================
609
+ async listAppConfigVars(appId) {
610
+ const result = await this.get(`/admin/api/apps/${appId}/vars`);
265
611
  return result?.items ?? [];
266
612
  }
267
- async addIntegrationSecret(appId, integrationId, payload) {
268
- const result = await this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, payload);
269
- return result?.secret ?? null;
613
+ async upsertAppConfigVar(appId, key, payload, expectedModifiedAt, options = {}) {
614
+ // Encode the key so a malformed one (e.g. containing "/") reaches the
615
+ // server's key validation (400) instead of producing a routing 404.
616
+ // `expectedModifiedAt` (issue #1423 review r-2 P1) is the optimistic-
617
+ // concurrency precondition for an UPDATE — the server rejects the write
618
+ // with a 409 CONFLICT (surfaced as `ConflictError`) if the var changed
619
+ // since it.
620
+ //
621
+ // `expectNotExists` (issue #1423 review r-3 P1a) is the create-only
622
+ // precondition. A create has no baseline timestamp to send, but without
623
+ // any precondition the by-key upsert silently overwrites a var created
624
+ // remotely since our snapshot. Setting `expectNotExists` makes the server
625
+ // 409 CONFLICT if the key already exists instead of overwriting it.
626
+ const body = { ...payload };
627
+ if (expectedModifiedAt !== undefined)
628
+ body.expectedModifiedAt = expectedModifiedAt;
629
+ if (options.expectNotExists)
630
+ body.expectNotExists = true;
631
+ return this.put(`/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`, body);
632
+ }
633
+ async deleteAppConfigVar(appId, key, expectedModifiedAt) {
634
+ // The DELETE carries the optimistic-concurrency precondition (issue #1423
635
+ // review r-2 P1) as a query param since it has no body; the server 409s
636
+ // (ConflictError) if the var was edited remotely since `expectedModifiedAt`.
637
+ let path = `/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`;
638
+ if (expectedModifiedAt !== undefined) {
639
+ path += `?expectedModifiedAt=${encodeURIComponent(expectedModifiedAt)}`;
640
+ }
641
+ return this.delete(path);
642
+ }
643
+ // ============================================
644
+ // WEBHOOKS
645
+ // ============================================
646
+ async listWebhooks(appId, params) {
647
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
648
+ return {
649
+ items: result?.items ?? [],
650
+ nextCursor: result?.nextCursor ?? null,
651
+ };
652
+ }
653
+ async getWebhook(appId, webhookId) {
654
+ return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
655
+ }
656
+ async createWebhook(appId, payload) {
657
+ return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
658
+ }
659
+ async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
660
+ const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
661
+ return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
662
+ }
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}`, {});
688
+ }
689
+ async rotateWebhookSecret(appId, webhookId, payload) {
690
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
691
+ }
692
+ async listWebhookEvents(appId, webhookId, params) {
693
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
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
+ };
732
+ }
733
+ async testWebhook(appId, webhookId, payload) {
734
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
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
+ }
747
+ // ============================================
748
+ // NAMED LOCKS (#1518)
749
+ // ============================================
750
+ async listLocks(appId) {
751
+ const result = await this.get(`/app/${appId}/api/locks`);
752
+ return { locks: result?.locks ?? [] };
753
+ }
754
+ async getLockStatus(appId, key) {
755
+ return this.post(`/app/${appId}/api/locks/status`, { key });
756
+ }
757
+ async acquireLock(appId, key, ttlMs) {
758
+ return this.post(`/app/${appId}/api/locks/acquire`, { key, ttlMs });
759
+ }
760
+ async releaseLock(appId, key, handleId) {
761
+ return this.post(`/app/${appId}/api/locks/release`, {
762
+ key,
763
+ handle: { handleId },
764
+ });
765
+ }
766
+ // ============================================
767
+ // CRON TRIGGERS
768
+ // ============================================
769
+ async listCronTriggers(appId) {
770
+ const result = await this.get(`/app/${appId}/api/cron-triggers`);
771
+ return { items: result?.items ?? [] };
772
+ }
773
+ async getCronTrigger(appId, triggerId) {
774
+ return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
775
+ }
776
+ async createCronTrigger(appId, payload) {
777
+ return this.post(`/app/${appId}/api/cron-triggers`, payload);
778
+ }
779
+ async updateCronTrigger(appId, triggerId, payload) {
780
+ return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
781
+ }
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}`);
270
791
  }
271
- async archiveIntegrationSecret(appId, integrationId, secretId) {
272
- return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets/${secretId}`, {
273
- status: "inactive",
792
+ async disableCronTrigger(appId, triggerId) {
793
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
794
+ }
795
+ async enableCronTrigger(appId, triggerId) {
796
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
797
+ }
798
+ async testCronTrigger(appId, triggerId) {
799
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
800
+ }
801
+ // ============================================
802
+ // ITERATIONS (iterate-users introspection / reset — #1209)
803
+ // ============================================
804
+ async listIterations(appId) {
805
+ const result = await this.get(`/app/${appId}/api/iterations`);
806
+ return { items: result?.items ?? [] };
807
+ }
808
+ async getIteration(appId, iterationName) {
809
+ return this.get(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}`);
810
+ }
811
+ async resetIteration(appId, iterationName) {
812
+ return this.post(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}/reset`, {});
813
+ }
814
+ // ============================================
815
+ // RESOURCE METADATA (values — issue #1352)
816
+ // ============================================
817
+ async readResourceMetadata(appId, resourceType, resourceId, category) {
818
+ return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
819
+ }
820
+ async writeResourceMetadata(appId, resourceType, resourceId, category, data) {
821
+ return this.put(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`, { data });
822
+ }
823
+ async batchReadResourceMetadata(appId, requests) {
824
+ return this.post(`/app/${appId}/api/resources/metadata/batch`, {
825
+ requests,
826
+ });
827
+ }
828
+ /**
829
+ * List every stored metadata category on one resource (issue #1402 — debug
830
+ * tooling). The CLI authenticates as a console admin, so the app-level
831
+ * owner/admin bypass returns every category regardless of its readRule.
832
+ */
833
+ async listResourceMetadata(appId, resourceType, resourceId) {
834
+ return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata`);
835
+ }
836
+ /**
837
+ * Delete one resource's metadata for one category (issue #1402 — debug
838
+ * tooling). Idempotent: deleting an absent item succeeds with
839
+ * `deleted: false` rather than a 404.
840
+ */
841
+ async deleteResourceMetadata(appId, resourceType, resourceId, category) {
842
+ return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
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,
274
856
  });
275
857
  }
276
858
  // ============================================
@@ -293,6 +875,12 @@ export class ApiClient {
293
875
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
294
876
  return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
295
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
+ */
296
884
  async deletePrompt(appId, promptId, hard) {
297
885
  const path = hard
298
886
  ? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
@@ -325,36 +913,135 @@ export class ApiClient {
325
913
  async duplicatePromptConfig(appId, promptId, configId, payload) {
326
914
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/configs/${configId}/duplicate`, payload || {});
327
915
  }
916
+ // ============================================
917
+ // SCRIPTS (Rhai transforms) — #1000 (prompt-model convergence)
918
+ // ============================================
919
+ //
920
+ // The `Script` model is the authoring surface for `script` workflow
921
+ // steps. A `Script` is a HEADER (name/description/activeConfigId); the
922
+ // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
923
+ // time. CLI `config push` reads `transforms/*.rhai` files and reconciles
924
+ // them: a new file → create script (mints a default config); a changed
925
+ // file → create a new config + activate it (zero fan-out — referencing
926
+ // workflows pick up the new body on their next run).
927
+ async listScripts(appId) {
928
+ const result = await this.get(`/admin/api/apps/${appId}/scripts`);
929
+ return { items: result?.items ?? [] };
930
+ }
931
+ async getScript(appId, scriptId) {
932
+ return this.get(`/admin/api/apps/${appId}/scripts/${scriptId}`);
933
+ }
934
+ /**
935
+ * List a script's versioned configs (the `ScriptConfig` rows). Each config
936
+ * is a distinct block version: `configId` is the stable version selector,
937
+ * `contentHash` is its content identity, and `status` reports whether it is
938
+ * `active`/`draft`/`archived`. The script header's `activeConfigId` names
939
+ * the live version. Used by `scripts configs list` and by the script test
940
+ * commands to run against a specific pinned version.
941
+ */
942
+ async listScriptConfigs(appId, scriptId) {
943
+ const result = await this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`);
944
+ return { items: result?.items ?? [] };
945
+ }
946
+ async createScript(appId, payload) {
947
+ // Mints the script header + a default `active` config carrying the body.
948
+ return this.post(`/admin/api/apps/${appId}/scripts`, payload);
949
+ }
950
+ /**
951
+ * Push a new body for an existing script by creating a new `ScriptConfig`
952
+ * and activating it (the prompt-model "edit → activate" flow). Referencing
953
+ * workflows pick up the new body on their next run with no fan-out. The
954
+ * config name is unique per script, so we mint a timestamped name.
955
+ */
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) {
968
+ const configName = `sync-${Date.now()}`;
969
+ const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
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);
978
+ return config;
979
+ }
980
+ /**
981
+ * Delete a script header (cascades its configs). `force` overrides the
982
+ * server's guard against deleting a script that an active workflow still
983
+ * references by name — without it the server answers 409.
984
+ */
985
+ async deleteScript(appId, scriptId, force) {
986
+ const path = force
987
+ ? `/admin/api/apps/${appId}/scripts/${scriptId}?force=true`
988
+ : `/admin/api/apps/${appId}/scripts/${scriptId}`;
989
+ return this.delete(path);
990
+ }
328
991
  async getPromptSchema(appId, promptId) {
329
992
  return this.get(`/admin/api/apps/${appId}/prompts/${promptId}/schema`);
330
993
  }
331
994
  // ============================================
332
995
  // BLOCK TEST CASES (Prompts, Integrations, Workflows)
333
996
  // ============================================
334
- async listTestCases(appId, blockType, blockId) {
335
- const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`);
336
- 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
+ };
337
1014
  }
338
1015
  async getTestCase(appId, blockType, blockId, testCaseId) {
339
1016
  return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
340
1017
  }
341
1018
  async createTestCase(appId, blockType, blockId, payload) {
342
- // 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.
343
1023
  const serverPayload = {
344
1024
  name: payload.name,
345
- inputVariables: JSON.stringify(payload.inputVariables),
1025
+ inputVariables: payload.inputVariables,
346
1026
  configId: payload.configId,
347
1027
  evaluatorPromptId: payload.evaluatorPromptId,
348
1028
  evaluatorConfigId: payload.evaluatorConfigId,
349
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
+ }
350
1037
  if (payload.expectedOutputPattern) {
351
1038
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
352
1039
  }
353
1040
  if (payload.expectedOutputContains) {
354
- serverPayload.expectedOutputContains = JSON.stringify(payload.expectedOutputContains);
1041
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
355
1042
  }
356
1043
  if (payload.expectedJsonSubset) {
357
- serverPayload.expectedJsonSubset = JSON.stringify(payload.expectedJsonSubset);
1044
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
358
1045
  }
359
1046
  return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
360
1047
  }
@@ -362,21 +1049,23 @@ export class ApiClient {
362
1049
  const serverPayload = {};
363
1050
  if (payload.name !== undefined)
364
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.
365
1058
  if (payload.inputVariables !== undefined) {
366
- serverPayload.inputVariables = JSON.stringify(payload.inputVariables);
1059
+ serverPayload.inputVariables = payload.inputVariables;
367
1060
  }
368
1061
  if (payload.expectedOutputPattern !== undefined) {
369
1062
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
370
1063
  }
371
1064
  if (payload.expectedOutputContains !== undefined) {
372
- serverPayload.expectedOutputContains = payload.expectedOutputContains
373
- ? JSON.stringify(payload.expectedOutputContains)
374
- : null;
1065
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
375
1066
  }
376
1067
  if (payload.expectedJsonSubset !== undefined) {
377
- serverPayload.expectedJsonSubset = payload.expectedJsonSubset
378
- ? JSON.stringify(payload.expectedJsonSubset)
379
- : null;
1068
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
380
1069
  }
381
1070
  if (payload.configId !== undefined)
382
1071
  serverPayload.configId = payload.configId;
@@ -386,6 +1075,8 @@ export class ApiClient {
386
1075
  if (payload.evaluatorConfigId !== undefined) {
387
1076
  serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
388
1077
  }
1078
+ if (payload.key !== undefined)
1079
+ serverPayload.key = payload.key;
389
1080
  return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
390
1081
  }
391
1082
  async deleteTestCase(appId, blockType, blockId, testCaseId) {
@@ -415,14 +1106,8 @@ export class ApiClient {
415
1106
  });
416
1107
  const text = await response.text();
417
1108
  if (!response.ok) {
418
- let errorData;
419
- try {
420
- errorData = JSON.parse(text);
421
- }
422
- catch {
423
- errorData = { message: text || `HTTP ${response.status}` };
424
- }
425
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
1109
+ const parsed = parseErrorResponse(response, text, url);
1110
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
426
1111
  }
427
1112
  return text ? JSON.parse(text) : null;
428
1113
  }
@@ -442,14 +1127,8 @@ export class ApiClient {
442
1127
  });
443
1128
  if (!response.ok) {
444
1129
  const text = await response.text();
445
- let errorData;
446
- try {
447
- errorData = JSON.parse(text);
448
- }
449
- catch {
450
- errorData = { message: text || `HTTP ${response.status}` };
451
- }
452
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
1130
+ const parsed = parseErrorResponse(response, text, url);
1131
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
453
1132
  }
454
1133
  const arrayBuffer = await response.arrayBuffer();
455
1134
  return {
@@ -494,15 +1173,33 @@ export class ApiClient {
494
1173
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
495
1174
  return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
496
1175
  }
497
- async deleteWorkflow(appId, workflowId) {
498
- 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`, {});
499
1199
  }
500
1200
  async updateWorkflowDraft(appId, workflowId, payload) {
501
1201
  return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
502
1202
  }
503
- async publishWorkflow(appId, workflowId) {
504
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
505
- }
506
1203
  async previewWorkflow(appId, workflowId, payload) {
507
1204
  return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
508
1205
  }
@@ -511,14 +1208,43 @@ export class ApiClient {
511
1208
  }
512
1209
  async listWorkflowRuns(appId, workflowId, params) {
513
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;
514
1213
  return {
515
1214
  items: result?.items ?? [],
516
- 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,
517
1236
  };
518
1237
  }
519
1238
  async getWorkflowRunStatus(appId, workflowId, runId) {
520
1239
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
521
1240
  }
1241
+ async getWorkflowStepRuns(appId, workflowId, runId) {
1242
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
1243
+ return { items: result?.items ?? [] };
1244
+ }
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.
522
1248
  // ============================================
523
1249
  // WORKFLOW CONFIGURATIONS
524
1250
  // ============================================
@@ -547,29 +1273,68 @@ export class ApiClient {
547
1273
  // ============================================
548
1274
  // ANALYTICS
549
1275
  // ============================================
550
- async getAnalyticsOverview(appId, params) {
551
- return this.get(`/app/${appId}/api/analytics/overview`, params);
552
- }
553
1276
  async getAnalyticsTopUsers(appId, params) {
554
1277
  return this.get(`/app/${appId}/api/analytics/users/top`, params);
555
1278
  }
556
- async getAnalyticsUserTimeline(appId, userUlid, params) {
557
- return this.get(`/app/${appId}/api/analytics/users/${userUlid}/timeline`, params);
558
- }
559
- async getAnalyticsUserEvents(appId, userUlid, params) {
560
- return this.get(`/app/${appId}/api/analytics/users/${userUlid}/events`, params);
561
- }
562
1279
  async getAnalyticsIntegrationMetrics(appId, params) {
563
1280
  return this.get(`/app/${appId}/api/analytics/integrations`, params);
564
1281
  }
1282
+ async getAnalyticsOverviewDau(appId) {
1283
+ return this.get(`/app/${appId}/api/analytics/overview/dau`);
1284
+ }
1285
+ async getAnalyticsOverviewWau(appId) {
1286
+ return this.get(`/app/${appId}/api/analytics/overview/wau`);
1287
+ }
1288
+ async getAnalyticsOverviewMau(appId) {
1289
+ return this.get(`/app/${appId}/api/analytics/overview/mau`);
1290
+ }
1291
+ async getAnalyticsOverviewGrowth(appId, params) {
1292
+ return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
1293
+ }
1294
+ async getAnalyticsDailyActive(appId, params) {
1295
+ return this.get(`/app/${appId}/api/analytics/daily-active`, params);
1296
+ }
1297
+ async getAnalyticsRollingActive(appId, params) {
1298
+ return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
1299
+ }
1300
+ async getAnalyticsCohortRetention(appId) {
1301
+ return this.get(`/app/${appId}/api/analytics/cohort-retention`);
1302
+ }
1303
+ async getAnalyticsUserSearch(appId, params) {
1304
+ return this.get(`/app/${appId}/api/analytics/users/search`, params);
1305
+ }
1306
+ async getAnalyticsUserDetail(appId, userUlid) {
1307
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
1308
+ }
1309
+ async getAnalyticsUserSnapshot(appId, userUlid) {
1310
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
1311
+ }
1312
+ async getAnalyticsEvents(appId, params) {
1313
+ return this.get(`/app/${appId}/api/analytics/events`, params);
1314
+ }
1315
+ async getAnalyticsEventsGrouped(appId, params) {
1316
+ return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
1317
+ }
1318
+ async getAnalyticsErrorsGroups(appId, params) {
1319
+ return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
1320
+ }
1321
+ async getAnalyticsTopWorkflows(appId, params) {
1322
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1323
+ }
1324
+ async getAnalyticsTopPrompts(appId, params) {
1325
+ return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
1326
+ }
565
1327
  // ============================================
566
1328
  // SUPER ADMIN: ADMINS
567
1329
  // ============================================
568
1330
  async listAllAdmins(params) {
569
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;
570
1334
  return {
571
- admins: result?.admins ?? [],
572
- cursor: result?.cursor ?? null,
1335
+ admins: result?.admins ?? result?.items ?? [],
1336
+ nextCursor,
1337
+ cursor: nextCursor,
573
1338
  };
574
1339
  }
575
1340
  async searchAdminByEmail(email) {
@@ -637,21 +1402,6 @@ export class ApiClient {
637
1402
  return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
638
1403
  }
639
1404
  // ============================================
640
- // LLM UTILITIES
641
- // ============================================
642
- async listLlmModels(provider = "openrouter") {
643
- return this.get("/admin/api/llm/models", { provider });
644
- }
645
- async generatePrompt(payload) {
646
- return this.post("/admin/api/llm/generate-prompt", payload);
647
- }
648
- async generateEvaluator(payload) {
649
- return this.post("/admin/api/llm/generate-evaluator", payload);
650
- }
651
- async generateWorkflowEvaluator(payload) {
652
- return this.post("/admin/api/llm/generate-evaluator-workflow", payload);
653
- }
654
- // ============================================
655
1405
  // BATCH TEST EXECUTION
656
1406
  // ============================================
657
1407
  async startBatchTests(appId, blockType, blockId, payload) {
@@ -670,6 +1420,33 @@ export class ApiClient {
670
1420
  return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
671
1421
  }
672
1422
  // ============================================
1423
+ // EMAIL TEMPLATES
1424
+ // ============================================
1425
+ async listEmailTemplates(appId) {
1426
+ return this.get(`/admin/api/apps/${appId}/email-templates`);
1427
+ }
1428
+ async getEmailTemplate(appId, emailType) {
1429
+ return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1430
+ }
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);
1442
+ }
1443
+ async deleteEmailTemplate(appId, emailType) {
1444
+ return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1445
+ }
1446
+ async testEmailTemplate(appId, emailType, payload) {
1447
+ return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
1448
+ }
1449
+ // ============================================
673
1450
  // ACCESS TOKENS
674
1451
  // ============================================
675
1452
  async createToken(appId, data) {
@@ -688,8 +1465,42 @@ export class ApiClient {
688
1465
  // ============================================
689
1466
  // DATABASES
690
1467
  // ============================================
691
- async listDatabases(appId) {
692
- 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
+ });
693
1504
  }
694
1505
  async createDatabase(appId, data) {
695
1506
  return this.post(`/app/${appId}/api/databases`, data);
@@ -703,35 +1514,360 @@ export class ApiClient {
703
1514
  async deleteDatabase(appId, databaseId) {
704
1515
  return this.delete(`/app/${appId}/api/databases/${databaseId}`);
705
1516
  }
1517
+ /**
1518
+ * Read a database's CEL context dict.
1519
+ *
1520
+ * The HTTP path stays `/metadata` because the wire field name is still
1521
+ * `metadata`; only the client/CLI-facing helper names were reframed.
1522
+ */
1523
+ async getDatabaseCelContext(appId, databaseId) {
1524
+ return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1525
+ }
1526
+ /** Update a database's CEL context dict (merge with existing). */
1527
+ async updateDatabaseCelContext(appId, databaseId, celContext) {
1528
+ return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1529
+ }
1530
+ /** @deprecated Use {@link getDatabaseCelContext} instead. */
1531
+ async getDatabaseMetadata(appId, databaseId) {
1532
+ return this.getDatabaseCelContext(appId, databaseId);
1533
+ }
1534
+ /** @deprecated Use {@link updateDatabaseCelContext} instead. */
1535
+ async updateDatabaseMetadata(appId, databaseId, metadata) {
1536
+ return this.updateDatabaseCelContext(appId, databaseId, metadata);
1537
+ }
706
1538
  // ============================================
707
1539
  // DATABASE PERMISSIONS
708
1540
  // ============================================
709
1541
  async listDatabasePermissions(appId, databaseId) {
710
1542
  return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
711
1543
  }
1544
+ async addDatabaseManager(appId, databaseId, userId) {
1545
+ return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
1546
+ userId,
1547
+ permission: "manager",
1548
+ });
1549
+ }
1550
+ async removeDatabaseManager(appId, databaseId, userId) {
1551
+ return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
1552
+ }
1553
+ /** @deprecated Use {@link addDatabaseManager} instead. */
712
1554
  async grantDatabasePermission(appId, databaseId, data) {
713
- return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, data);
1555
+ return this.addDatabaseManager(appId, databaseId, data.userId);
714
1556
  }
1557
+ /** @deprecated Use {@link removeDatabaseManager} instead. */
715
1558
  async revokeDatabasePermission(appId, databaseId, userId) {
716
1559
  return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
717
1560
  }
718
1561
  // ============================================
719
- // DATABASE GROUP PERMISSIONS
1562
+ // DATABASE RECORDS & SCHEMA
1563
+ // ============================================
1564
+ async listDatabaseModels(appId, databaseId) {
1565
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/models`);
1566
+ }
1567
+ async describeDatabaseModel(appId, databaseId, modelName) {
1568
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
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
+ */
1578
+ async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1579
+ const body = { modelName };
1580
+ if (queryOptions?.filter)
1581
+ body.filter = queryOptions.filter;
1582
+ const options = {};
1583
+ if (queryOptions?.limit)
1584
+ options.limit = queryOptions.limit;
1585
+ if (queryOptions?.cursor)
1586
+ options.uniqueStartKey = queryOptions.cursor;
1587
+ if (Object.keys(options).length > 0)
1588
+ body.options = options;
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
+ });
1638
+ }
1639
+ // ============================================
1640
+ // DATABASE OPERATIONS (registered operations)
1641
+ // ============================================
1642
+ async listDatabaseOperations(appId, databaseId) {
1643
+ return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
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
+ */
1654
+ async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
1655
+ const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
1656
+ const extraHeaders = {};
1657
+ if (options?.timing) {
1658
+ extraHeaders["X-Timing"] = "true";
1659
+ }
1660
+ if (token) {
1661
+ return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
1662
+ method: "POST",
1663
+ body: JSON.stringify(data || {}),
1664
+ headers: extraHeaders,
1665
+ }));
1666
+ }
1667
+ return normalizeDatabaseOperationResult(await this.request(path, {
1668
+ method: "POST",
1669
+ body: JSON.stringify(data || {}),
1670
+ headers: extraHeaders,
1671
+ }));
1672
+ }
1673
+ /**
1674
+ * Execute a registered batch (bulk) database operation. Posts a chunk of
1675
+ * items to the canonical `operations/:name/batch` endpoint (the same one the
1676
+ * client library's `executeBatch` uses — NOT the deprecated `import-bulk`
1677
+ * alias). Used by `databases import-csv`.
1678
+ *
1679
+ * @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
1680
+ * validation/access failures abort the whole chunk with a 4xx (thrown).
1681
+ */
1682
+ async executeBatch(appId, databaseId, operationName, batch) {
1683
+ return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
1684
+ }
1685
+ /**
1686
+ * Make a request using a specific JWT token instead of the CLI's credentials.
1687
+ * Used for executing operations as a different user.
1688
+ */
1689
+ async requestWithToken(path, token, options = {}) {
1690
+ const credentials = await this.ensureAuthenticated();
1691
+ const url = `${credentials.serverUrl}${path}`;
1692
+ const headers = {
1693
+ Authorization: `Bearer ${token}`,
1694
+ "Content-Type": "application/json",
1695
+ ...(options.headers || {}),
1696
+ };
1697
+ const response = await fetchWithTLS(url, { ...options, headers });
1698
+ const text = await response.text();
1699
+ if (!response.ok) {
1700
+ const parsed = parseErrorResponse(response, text, path);
1701
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
1702
+ }
1703
+ return text ? JSON.parse(text) : null;
1704
+ }
1705
+ // ============================================
1706
+ // DATABASE INDEXES
720
1707
  // ============================================
721
- async listDatabaseGroupPermissions(appId, databaseId) {
722
- return this.get(`/app/${appId}/api/databases/${databaseId}/group-permissions`);
1708
+ async listDatabaseIndexes(appId, databaseId, modelName) {
1709
+ return this.get(`/app/${appId}/api/databases/${databaseId}/records/indexes`, modelName ? { modelName } : undefined);
1710
+ }
1711
+ async registerDatabaseIndex(appId, databaseId, data) {
1712
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/register`, data);
723
1713
  }
724
- async grantDatabaseGroupPermission(appId, databaseId, data) {
725
- return this.post(`/app/${appId}/api/databases/${databaseId}/group-permissions`, data);
1714
+ async dropDatabaseIndex(appId, databaseId, data) {
1715
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
726
1716
  }
727
- async revokeDatabaseGroupPermission(appId, databaseId, groupType, groupId) {
728
- return this.delete(`/app/${appId}/api/databases/${databaseId}/group-permissions/${groupType}/${groupId}`);
1717
+ /**
1718
+ * Issue #974: back-provision an existing database instance's schema-declared
1719
+ * unique indexes by reading its type schema and registering any declared-but-
1720
+ * missing unique index. Idempotent.
1721
+ */
1722
+ async reindexDatabaseFromSchema(appId, databaseId) {
1723
+ return this.post(`/app/${appId}/api/databases/${databaseId}/reindex`, {});
1724
+ }
1725
+ // ============================================
1726
+ // DATABASE TYPE CONFIGS
1727
+ // ============================================
1728
+ async listDatabaseTypeConfigs(appId) {
1729
+ return this.get(`/app/${appId}/api/databases/types`);
1730
+ }
1731
+ async getDatabaseTypeConfig(appId, databaseType) {
1732
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1733
+ }
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);
1742
+ }
1743
+ async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1744
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1745
+ const qs = new URLSearchParams();
1746
+ if (options?.dryRun)
1747
+ qs.set("dryRun", "true");
1748
+ if (options?.acceptWarnings)
1749
+ qs.set("acceptWarnings", "true");
1750
+ const query = qs.toString();
1751
+ const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
1752
+ return this.patch(path, body);
1753
+ }
1754
+ async deleteDatabaseTypeConfig(appId, databaseType, options) {
1755
+ const query = options?.force ? "?force=true" : "";
1756
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
1757
+ }
1758
+ /**
1759
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
1760
+ * existing ops + DO field introspection. Read-only — does NOT persist.
1761
+ */
1762
+ async scaffoldDatabaseTypeSchema(appId, databaseType) {
1763
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
1764
+ }
1765
+ // ============================================
1766
+ // DATABASE TYPE OPERATIONS
1767
+ // ============================================
1768
+ async listDatabaseTypeOperations(appId, databaseType) {
1769
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
1770
+ }
1771
+ async getDatabaseTypeOperation(appId, databaseType, name) {
1772
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
1773
+ }
1774
+ async createDatabaseTypeOperation(appId, databaseType, data,
1775
+ // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
1776
+ // without persisting — used by `config push --dry-run` to surface gate
1777
+ // failures the real push would hit. `schemaOverride` lets the gate run
1778
+ // against the schema the SAME push is about to land (so an op that depends
1779
+ // on a new field isn't falsely rejected against the stale stored schema).
1780
+ //
1781
+ // Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
1782
+ // does not exist on the server yet, so the server's "access required"
1783
+ // check has no `defaultAccess` to fall back on. Thread the TOML-declared
1784
+ // type-level `defaultAccess` so an op that omits `access` (intending to
1785
+ // inherit it after the type is created) isn't falsely rejected during the
1786
+ // dry-run. Server only trusts this on the fresh-type dry-run path.
1787
+ options) {
1788
+ const query = options?.dryRun ? "?dryRun=true" : "";
1789
+ let body = data;
1790
+ if (options?.dryRun && options.schemaOverride !== undefined) {
1791
+ body = { ...body, schemaOverride: options.schemaOverride };
1792
+ }
1793
+ if (options?.dryRun && options.defaultAccess !== undefined) {
1794
+ body = { ...body, defaultAccess: options.defaultAccess };
1795
+ }
1796
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1797
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1798
+ }
1799
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
1800
+ }
1801
+ async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
1802
+ // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
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`).
1806
+ options) {
1807
+ let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
1808
+ if (options?.dryRun && options.schemaOverride !== undefined) {
1809
+ body = { ...body, schemaOverride: options.schemaOverride };
1810
+ }
1811
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1812
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1813
+ }
1814
+ const query = options?.dryRun ? "?dryRun=true" : "";
1815
+ return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
1816
+ }
1817
+ async deleteDatabaseTypeOperation(appId, databaseType, name) {
1818
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
1819
+ }
1820
+ // ============================================
1821
+ // DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
1822
+ // ============================================
1823
+ //
1824
+ // Transparent passthroughs against the server's type-scoped subscription
1825
+ // routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
1826
+ // The wire format is authoritative in the controller
1827
+ // (`database-type-subscriptions-controller.ts`): create requires
1828
+ // `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
1829
+ // (CEL); optional `description`, `select` (string[]), `emit`
1830
+ // (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
1831
+ //
1832
+ // The list endpoint wraps results in `{ items: [...] }` (unlike operations,
1833
+ // which returns a bare array). We unwrap here so callers get an array,
1834
+ // matching `listDatabaseTypeOperations`.
1835
+ async listDatabaseTypeSubscriptions(appId, databaseType) {
1836
+ const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
1837
+ if (Array.isArray(result))
1838
+ return result;
1839
+ return Array.isArray(result?.items) ? result.items : [];
1840
+ }
1841
+ async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1842
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
1843
+ }
1844
+ async createDatabaseTypeSubscription(appId, databaseType, data) {
1845
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
1846
+ }
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);
1853
+ }
1854
+ async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
1855
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
729
1856
  }
730
1857
  // ============================================
731
1858
  // GROUPS
732
1859
  // ============================================
733
1860
  async listGroups(appId, params) {
734
- 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
+ };
735
1871
  }
736
1872
  async createGroup(appId, data) {
737
1873
  return this.post(`/app/${appId}/api/groups`, data);
@@ -748,8 +1884,18 @@ export class ApiClient {
748
1884
  // ============================================
749
1885
  // GROUP MEMBERS
750
1886
  // ============================================
751
- async listGroupMembers(appId, groupType, groupId) {
752
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members`);
1887
+ async listGroupMembers(appId, groupType, groupId, options) {
1888
+ const qs = options?.include ? `?include=${options.include}` : "";
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
+ };
753
1899
  }
754
1900
  async addGroupMember(appId, groupType, groupId, data) {
755
1901
  return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
@@ -757,18 +1903,12 @@ export class ApiClient {
757
1903
  async removeGroupMember(appId, groupType, groupId, userId) {
758
1904
  return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
759
1905
  }
760
- async updateGroupMemberRole(appId, groupType, groupId, userId, data) {
761
- return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`, data);
762
- }
763
1906
  // ============================================
764
1907
  // GROUP RESOURCE LISTINGS
765
1908
  // ============================================
766
1909
  async listGroupDocuments(appId, groupType, groupId) {
767
1910
  return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
768
1911
  }
769
- async listGroupDatabases(appId, groupType, groupId) {
770
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/databases`);
771
- }
772
1912
  // ============================================
773
1913
  // USER MEMBERSHIPS
774
1914
  // ============================================
@@ -787,6 +1927,554 @@ export class ApiClient {
787
1927
  async revokeDocumentGroupPermission(appId, documentId, groupType, groupId) {
788
1928
  return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
789
1929
  }
1930
+ // ============================================
1931
+ // COLLECTIONS
1932
+ // ============================================
1933
+ async listCollections(appId, params) {
1934
+ const qs = new URLSearchParams();
1935
+ if (params?.limit)
1936
+ qs.set("limit", String(params.limit));
1937
+ if (params?.cursor)
1938
+ qs.set("cursor", params.cursor);
1939
+ const q = qs.toString();
1940
+ const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1941
+ return {
1942
+ items: result?.items ?? [],
1943
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1944
+ };
1945
+ }
1946
+ async listAllCollections(appId, params) {
1947
+ const qs = new URLSearchParams();
1948
+ if (params?.limit)
1949
+ qs.set("limit", String(params.limit));
1950
+ if (params?.cursor)
1951
+ qs.set("cursor", params.cursor);
1952
+ const q = qs.toString();
1953
+ const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1954
+ return {
1955
+ items: result?.items ?? [],
1956
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1957
+ };
1958
+ }
1959
+ async createCollection(appId, data) {
1960
+ return this.post(`/app/${appId}/api/collections`, data);
1961
+ }
1962
+ async getCollection(appId, collectionId) {
1963
+ return this.get(`/app/${appId}/api/collections/${collectionId}`);
1964
+ }
1965
+ async updateCollection(appId, collectionId, data) {
1966
+ return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
1967
+ }
1968
+ async deleteCollection(appId, collectionId) {
1969
+ return this.delete(`/app/${appId}/api/collections/${collectionId}`);
1970
+ }
1971
+ // ============================================
1972
+ // COLLECTION DOCUMENTS
1973
+ // ============================================
1974
+ async listCollectionDocuments(appId, collectionId, params) {
1975
+ const qs = new URLSearchParams();
1976
+ if (params?.limit)
1977
+ qs.set("limit", String(params.limit));
1978
+ if (params?.cursor)
1979
+ qs.set("cursor", params.cursor);
1980
+ const q = qs.toString();
1981
+ const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1982
+ return {
1983
+ items: result?.items ?? [],
1984
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1985
+ };
1986
+ }
1987
+ async addCollectionDocument(appId, collectionId, data) {
1988
+ return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
1989
+ }
1990
+ async removeCollectionDocument(appId, collectionId, documentId) {
1991
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
1992
+ }
1993
+ async listCollectionsForDocument(appId, documentId, params) {
1994
+ const qs = new URLSearchParams();
1995
+ if (params?.limit)
1996
+ qs.set("limit", String(params.limit));
1997
+ if (params?.cursor)
1998
+ qs.set("cursor", params.cursor);
1999
+ const q = qs.toString();
2000
+ const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
2001
+ return {
2002
+ items: result?.items ?? [],
2003
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
2004
+ };
2005
+ }
2006
+ // ============================================
2007
+ // COLLECTION ACCESS (GROUPS + MEMBERS)
2008
+ // ============================================
2009
+ async getCollectionAccess(appId, collectionId) {
2010
+ return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
2011
+ }
2012
+ async grantCollectionGroupPermission(appId, collectionId, data) {
2013
+ return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
2014
+ }
2015
+ async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
2016
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
2017
+ }
2018
+ async addCollectionMember(appId, collectionId, data) {
2019
+ return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
2020
+ }
2021
+ async removeCollectionMember(appId, collectionId, userId) {
2022
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
2023
+ }
2024
+ // ============================================
2025
+ // COLLECTION TYPE CONFIGS
2026
+ // ============================================
2027
+ async listCollectionTypeConfigs(appId) {
2028
+ return this.get(`/app/${appId}/api/collection-type-configs`);
2029
+ }
2030
+ async getCollectionTypeConfig(appId, collectionType) {
2031
+ return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2032
+ }
2033
+ async createCollectionTypeConfig(appId, data) {
2034
+ return this.post(`/app/${appId}/api/collection-type-configs`, data);
2035
+ }
2036
+ async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
2037
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2038
+ return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
2039
+ }
2040
+ async deleteCollectionTypeConfig(appId, collectionType) {
2041
+ return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2042
+ }
2043
+ // ============================================
2044
+ // METADATA CATEGORY CONFIGS (issue #1304, P-B)
2045
+ // ============================================
2046
+ /**
2047
+ * List all metadata category configs for an app. The route returns
2048
+ * `{ configs: [...] }`; unwrap to a bare array to match the other
2049
+ * `list*Configs` helpers.
2050
+ */
2051
+ async listMetadataCategoryConfigs(appId) {
2052
+ const res = await this.get(`/app/${appId}/api/metadata-categories`);
2053
+ return Array.isArray(res?.configs) ? res.configs : [];
2054
+ }
2055
+ async getMetadataCategoryConfig(appId, resourceType, category) {
2056
+ return this.get(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
2057
+ }
2058
+ /**
2059
+ * Create or replace a metadata category config (idempotent upsert via the
2060
+ * path-addressed PUT route). `resourceType` / `category` identify the config;
2061
+ * the body carries `schema` / `readRule` / `writeRule` / `description`.
2062
+ */
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);
2067
+ }
2068
+ /**
2069
+ * Delete a metadata category config (issue #1426, admin-gated route shipped
2070
+ * in #1364). Orphan semantics: the config row is hard-deleted; any stored
2071
+ * `ResourceMetadata` value rows for the category are left behind and become
2072
+ * unreachable (no query path from a category to its values). Delete the
2073
+ * values first if you need them gone — an orphaned value row can't be
2074
+ * removed through any surface once its config is gone. Returns
2075
+ * `{ resourceType, category, deleted }`; a missing config is a 404.
2076
+ */
2077
+ async deleteMetadataCategoryConfig(appId, resourceType, category) {
2078
+ return this.delete(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
2079
+ }
2080
+ // ============================================
2081
+ // ACCESS RULE SETS
2082
+ // ============================================
2083
+ async listRuleSets(appId, params) {
2084
+ return this.get(`/app/${appId}/api/rule-sets`, params);
2085
+ }
2086
+ async createRuleSet(appId, data) {
2087
+ return this.post(`/app/${appId}/api/rule-sets`, data);
2088
+ }
2089
+ async getRuleSet(appId, ruleSetId) {
2090
+ return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
2091
+ }
2092
+ async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
2093
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2094
+ return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
2095
+ }
2096
+ async deleteRuleSet(appId, ruleSetId) {
2097
+ return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
2098
+ }
2099
+ async getRuleSetSchema(appId) {
2100
+ return this.get(`/app/${appId}/api/rule-sets/schema`);
2101
+ }
2102
+ async getRuleSetResourceTypes(appId) {
2103
+ return this.get(`/app/${appId}/api/rule-sets/resource-types`);
2104
+ }
2105
+ async testRuleSet(appId, ruleSetId, data) {
2106
+ return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
2107
+ }
2108
+ async debugRuleSet(appId, data) {
2109
+ return this.post(`/app/${appId}/api/rule-sets/debug`, data);
2110
+ }
2111
+ // ============================================
2112
+ // GROUP TYPE CONFIGS
2113
+ // ============================================
2114
+ async listGroupTypeConfigs(appId) {
2115
+ return this.get(`/app/${appId}/api/group-type-configs`);
2116
+ }
2117
+ async getGroupTypeConfig(appId, groupType) {
2118
+ return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
2119
+ }
2120
+ async createGroupTypeConfig(appId, data) {
2121
+ return this.post(`/app/${appId}/api/group-type-configs`, data);
2122
+ }
2123
+ async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
2124
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2125
+ return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
2126
+ }
2127
+ async deleteGroupTypeConfig(appId, groupType) {
2128
+ return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
2129
+ }
2130
+ // ============================================
2131
+ // DOCUMENT EXPORT / IMPORT
2132
+ // ============================================
2133
+ async exportDocumentState(appId, documentId) {
2134
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
2135
+ }
2136
+ async importDocumentState(appId, documentId, stateBase64) {
2137
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2138
+ }
2139
+ async getDocument(appId, documentId) {
2140
+ return this.get(`/app/${appId}/api/documents/${documentId}`);
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
+ }
2248
+ async createDocument(appId, data) {
2249
+ return this.post(`/app/${appId}/api/documents`, data);
2250
+ }
2251
+ async listDocumentPermissions(appId, documentId) {
2252
+ return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
2253
+ }
2254
+ async grantDocumentPermission(appId, documentId, permissions) {
2255
+ return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
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
+ }
2275
+ async getDocumentLinkAccess(appId, documentId) {
2276
+ return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
2277
+ }
2278
+ async setDocumentLinkAccess(appId, documentId, level) {
2279
+ return this.put(`/app/${appId}/api/documents/${documentId}/link-access`, { level });
2280
+ }
2281
+ async clearDocumentLinkAccess(appId, documentId) {
2282
+ return this.delete(`/app/${appId}/api/documents/${documentId}/link-access`);
2283
+ }
2284
+ async listDocumentInvitations(appId, documentId) {
2285
+ return this.get(`/app/${appId}/api/documents/${documentId}/invitations`);
2286
+ }
2287
+ async listDocumentBlobs(appId, documentId) {
2288
+ return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
2289
+ }
2290
+ async downloadBlob(appId, documentId, blobId) {
2291
+ const credentials = await this.ensureAuthenticated();
2292
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
2293
+ const headers = {
2294
+ Authorization: `Bearer ${credentials.accessToken}`,
2295
+ };
2296
+ if (credentials.globalAdminAppId) {
2297
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2298
+ }
2299
+ const response = await fetchWithTLS(url, { headers });
2300
+ if (!response.ok) {
2301
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
2302
+ }
2303
+ const arrayBuffer = await response.arrayBuffer();
2304
+ return Buffer.from(arrayBuffer);
2305
+ }
2306
+ async uploadBlob(appId, documentId, blobId, data, meta) {
2307
+ const credentials = await this.ensureAuthenticated();
2308
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
2309
+ const headers = {
2310
+ Authorization: `Bearer ${credentials.accessToken}`,
2311
+ "Content-Type": meta.contentType,
2312
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
2313
+ "X-Blob-Size": String(data.length),
2314
+ "X-Blob-Sha256": meta.sha256,
2315
+ };
2316
+ if (credentials.globalAdminAppId) {
2317
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2318
+ }
2319
+ const response = await fetchWithTLS(url, {
2320
+ method: "PUT",
2321
+ headers,
2322
+ body: data,
2323
+ });
2324
+ if (!response.ok) {
2325
+ const text = await response.text();
2326
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
2327
+ }
2328
+ return response.json();
2329
+ }
2330
+ async listDocumentAliases(appId, documentId) {
2331
+ return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
2332
+ }
2333
+ async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
2334
+ return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
2335
+ documentId,
2336
+ userId: ownerUserId,
2337
+ mustNotExist,
2338
+ });
2339
+ }
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 });
2350
+ }
2351
+ async listAdminDocuments(appId, userId) {
2352
+ const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
2353
+ return result?.documents || result || [];
2354
+ }
2355
+ // ============================================
2356
+ // DATABASE EXPORT / IMPORT
2357
+ // ============================================
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);
2375
+ }
2376
+ async deleteDatabaseRecord(appId, databaseId, modelName, id) {
2377
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, { modelName, id });
2378
+ }
2379
+ async batchDeleteDatabaseRecords(appId, databaseId, operations) {
2380
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
2381
+ }
2382
+ async listDatabaseUniqueConstraints(appId, databaseId) {
2383
+ const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
2384
+ return result?.constraints || result || [];
2385
+ }
2386
+ async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
2387
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
2388
+ }
2389
+ // ============================================
2390
+ // BLOB BUCKETS
2391
+ // ============================================
2392
+ async listBlobBuckets(appId) {
2393
+ return this.get(`/app/${appId}/api/blob-buckets`);
2394
+ }
2395
+ async createBlobBucket(appId, payload) {
2396
+ return this.post(`/app/${appId}/api/blob-buckets`, payload);
2397
+ }
2398
+ async getBlobBucket(appId, bucketIdOrKey) {
2399
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
2400
+ }
2401
+ async updateBlobBucket(appId, bucketIdOrKey, payload) {
2402
+ return this.patch(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`, payload);
2403
+ }
2404
+ async deleteBlobBucket(appId, bucketIdOrKey) {
2405
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
2406
+ }
2407
+ async listBucketBlobs(appId, bucketIdOrKey, 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
+ };
2418
+ }
2419
+ async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
2420
+ const credentials = await this.ensureAuthenticated();
2421
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
2422
+ const headers = {
2423
+ Authorization: `Bearer ${credentials.accessToken}`,
2424
+ "Content-Type": meta.contentType,
2425
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
2426
+ };
2427
+ if (meta.tags && meta.tags.length > 0) {
2428
+ headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
2429
+ }
2430
+ if (credentials.globalAdminAppId) {
2431
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2432
+ }
2433
+ const response = await fetchWithTLS(url, {
2434
+ method: "POST",
2435
+ headers,
2436
+ body: data,
2437
+ });
2438
+ if (!response.ok) {
2439
+ const text = await response.text();
2440
+ throw new ApiError(`Failed to upload blob: ${text}`, response.status);
2441
+ }
2442
+ return response.json();
2443
+ }
2444
+ async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
2445
+ const credentials = await this.ensureAuthenticated();
2446
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
2447
+ const headers = {
2448
+ Authorization: `Bearer ${credentials.accessToken}`,
2449
+ };
2450
+ if (credentials.globalAdminAppId) {
2451
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2452
+ }
2453
+ const response = await fetchWithTLS(url, { headers });
2454
+ if (!response.ok) {
2455
+ throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
2456
+ }
2457
+ const arrayBuffer = await response.arrayBuffer();
2458
+ return Buffer.from(arrayBuffer);
2459
+ }
2460
+ async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
2461
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
2462
+ }
2463
+ // Batch delete (#1455): one round-trip for N ids via POST .../blobs/delete.
2464
+ async deleteBucketBlobs(appId, bucketIdOrKey, blobIds) {
2465
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/delete`, { blobIds });
2466
+ }
2467
+ async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
2468
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
2469
+ expiresInSeconds: expiresInSeconds || 300,
2470
+ });
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
+ }
790
2478
  }
791
2479
  // Export a singleton instance
792
2480
  export const apiClient = new ApiClient();