primitive-admin 1.1.0-alpha.9 → 1.1.0

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 (591) hide show
  1. package/README.md +523 -109
  2. package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
  3. package/dist/bin/primitive.d.ts +2 -0
  4. package/dist/bin/primitive.js +372 -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 +136 -30
  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 +713 -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 +65 -100
  14. package/dist/src/commands/apps.js.map +1 -1
  15. package/dist/src/commands/auth-sessions.d.ts +7 -0
  16. package/dist/src/commands/auth-sessions.js +144 -0
  17. package/dist/src/commands/auth-sessions.js.map +1 -0
  18. package/dist/src/commands/auth.d.ts +2 -0
  19. package/dist/src/commands/auth.js +238 -108
  20. package/dist/src/commands/auth.js.map +1 -1
  21. package/dist/src/commands/blob-buckets.d.ts +2 -0
  22. package/dist/src/commands/blob-buckets.js +331 -0
  23. package/dist/src/commands/blob-buckets.js.map +1 -0
  24. package/dist/src/commands/catalog.d.ts +2 -0
  25. package/dist/src/commands/catalog.js +63 -48
  26. package/dist/src/commands/catalog.js.map +1 -1
  27. package/dist/src/commands/collection-type-configs.d.ts +2 -0
  28. package/dist/src/commands/collection-type-configs.js +85 -0
  29. package/dist/src/commands/collection-type-configs.js.map +1 -0
  30. package/dist/src/commands/collections.d.ts +2 -0
  31. package/dist/src/commands/collections.js +1282 -0
  32. package/dist/src/commands/collections.js.map +1 -0
  33. package/dist/src/commands/comparisons.d.ts +2 -0
  34. package/dist/src/commands/comparisons.js +6 -6
  35. package/dist/src/commands/comparisons.js.map +1 -1
  36. package/dist/src/commands/config.d.ts +46 -0
  37. package/dist/src/commands/config.js +465 -0
  38. package/dist/src/commands/config.js.map +1 -0
  39. package/dist/src/commands/connections.d.ts +2 -0
  40. package/dist/src/commands/connections.js +99 -0
  41. package/dist/src/commands/connections.js.map +1 -0
  42. package/dist/src/commands/cron-triggers.d.ts +2 -0
  43. package/dist/src/commands/cron-triggers.js +266 -0
  44. package/dist/src/commands/cron-triggers.js.map +1 -0
  45. package/dist/src/commands/database-type-configs.d.ts +2 -0
  46. package/dist/src/commands/database-type-configs.js +164 -0
  47. package/dist/src/commands/database-type-configs.js.map +1 -0
  48. package/dist/src/commands/database-types.d.ts +2 -0
  49. package/dist/src/commands/database-types.js +471 -0
  50. package/dist/src/commands/database-types.js.map +1 -0
  51. package/dist/src/commands/databases.d.ts +65 -0
  52. package/dist/src/commands/databases.js +1887 -243
  53. package/dist/src/commands/databases.js.map +1 -1
  54. package/dist/src/commands/documents.d.ts +60 -0
  55. package/dist/src/commands/documents.js +2888 -22
  56. package/dist/src/commands/documents.js.map +1 -1
  57. package/dist/src/commands/email-templates.d.ts +2 -0
  58. package/dist/src/commands/email-templates.js +175 -0
  59. package/dist/src/commands/email-templates.js.map +1 -0
  60. package/dist/src/commands/env.d.ts +23 -0
  61. package/dist/src/commands/env.js +395 -0
  62. package/dist/src/commands/env.js.map +1 -0
  63. package/dist/src/commands/feature-flags.d.ts +14 -0
  64. package/dist/src/commands/feature-flags.js +117 -0
  65. package/dist/src/commands/feature-flags.js.map +1 -0
  66. package/dist/src/commands/functions.d.ts +20 -0
  67. package/dist/src/commands/functions.js +1630 -0
  68. package/dist/src/commands/functions.js.map +1 -0
  69. package/dist/src/commands/group-type-configs.d.ts +2 -0
  70. package/dist/src/commands/group-type-configs.js +87 -0
  71. package/dist/src/commands/group-type-configs.js.map +1 -0
  72. package/dist/src/commands/groups.d.ts +2 -0
  73. package/dist/src/commands/groups.js +65 -113
  74. package/dist/src/commands/groups.js.map +1 -1
  75. package/dist/src/commands/guides.d.ts +223 -0
  76. package/dist/src/commands/guides.js +627 -69
  77. package/dist/src/commands/guides.js.map +1 -1
  78. package/dist/src/commands/init.d.ts +37 -0
  79. package/dist/src/commands/init.js +1693 -208
  80. package/dist/src/commands/init.js.map +1 -1
  81. package/dist/src/commands/integrations.d.ts +2 -0
  82. package/dist/src/commands/integrations.js +428 -187
  83. package/dist/src/commands/integrations.js.map +1 -1
  84. package/dist/src/commands/llm.d.ts +2 -0
  85. package/dist/src/commands/llm.js +4 -2
  86. package/dist/src/commands/llm.js.map +1 -1
  87. package/dist/src/commands/locks.d.ts +8 -0
  88. package/dist/src/commands/locks.js +175 -0
  89. package/dist/src/commands/locks.js.map +1 -0
  90. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  91. package/dist/src/commands/metadata-category-configs.js +113 -0
  92. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  93. package/dist/src/commands/metadata.d.ts +2 -0
  94. package/dist/src/commands/metadata.js +288 -0
  95. package/dist/src/commands/metadata.js.map +1 -0
  96. package/dist/src/commands/prompts.d.ts +2 -0
  97. package/dist/src/commands/prompts.js +322 -634
  98. package/dist/src/commands/prompts.js.map +1 -1
  99. package/dist/src/commands/rule-sets.d.ts +3 -0
  100. package/dist/src/commands/rule-sets.js +139 -148
  101. package/dist/src/commands/rule-sets.js.map +1 -1
  102. package/dist/src/commands/scripts.d.ts +30 -0
  103. package/dist/src/commands/scripts.js +688 -0
  104. package/dist/src/commands/scripts.js.map +1 -0
  105. package/dist/src/commands/secrets.d.ts +2 -0
  106. package/dist/src/commands/secrets.js +109 -0
  107. package/dist/src/commands/secrets.js.map +1 -0
  108. package/dist/src/commands/sessions.d.ts +2 -0
  109. package/dist/src/commands/sessions.js +76 -0
  110. package/dist/src/commands/sessions.js.map +1 -0
  111. package/dist/src/commands/skill.d.ts +2 -0
  112. package/dist/src/commands/skill.js +29 -0
  113. package/dist/src/commands/skill.js.map +1 -0
  114. package/dist/src/commands/sync-app-settings.d.ts +158 -0
  115. package/dist/src/commands/sync-app-settings.js +328 -0
  116. package/dist/src/commands/sync-app-settings.js.map +1 -0
  117. package/dist/src/commands/sync.d.ts +2670 -0
  118. package/dist/src/commands/sync.js +17144 -834
  119. package/dist/src/commands/sync.js.map +1 -1
  120. package/dist/src/commands/tokens.d.ts +2 -0
  121. package/dist/src/commands/tokens.js +132 -22
  122. package/dist/src/commands/tokens.js.map +1 -1
  123. package/dist/src/commands/users.d.ts +2 -0
  124. package/dist/src/commands/users.js +542 -24
  125. package/dist/src/commands/users.js.map +1 -1
  126. package/dist/src/commands/vars.d.ts +8 -0
  127. package/dist/src/commands/vars.js +97 -0
  128. package/dist/src/commands/vars.js.map +1 -0
  129. package/dist/src/commands/waitlist.d.ts +2 -0
  130. package/dist/src/commands/waitlist.js +12 -11
  131. package/dist/src/commands/waitlist.js.map +1 -1
  132. package/dist/src/commands/webhooks.d.ts +31 -0
  133. package/dist/src/commands/webhooks.js +633 -0
  134. package/dist/src/commands/webhooks.js.map +1 -0
  135. package/dist/src/commands/workflows.d.ts +88 -0
  136. package/dist/src/commands/workflows.js +1568 -742
  137. package/dist/src/commands/workflows.js.map +1 -1
  138. package/dist/src/lib/access-rule-display.d.ts +21 -0
  139. package/dist/src/lib/access-rule-display.js +34 -0
  140. package/dist/src/lib/access-rule-display.js.map +1 -0
  141. package/dist/src/lib/api-client.d.ts +2550 -0
  142. package/dist/src/lib/api-client.js +2503 -160
  143. package/dist/src/lib/api-client.js.map +1 -1
  144. package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
  145. package/dist/src/lib/app-settings-descriptor.js +583 -0
  146. package/dist/src/lib/app-settings-descriptor.js.map +1 -0
  147. package/dist/src/lib/auth-flow.d.ts +8 -0
  148. package/dist/src/lib/batch.d.ts +26 -0
  149. package/dist/src/lib/batch.js +32 -0
  150. package/dist/src/lib/batch.js.map +1 -0
  151. package/dist/src/lib/block-layout.d.ts +160 -0
  152. package/dist/src/lib/block-layout.js +451 -0
  153. package/dist/src/lib/block-layout.js.map +1 -0
  154. package/dist/src/lib/block-selector.d.ts +58 -0
  155. package/dist/src/lib/block-selector.js +92 -0
  156. package/dist/src/lib/block-selector.js.map +1 -0
  157. package/dist/src/lib/canonical-json.d.ts +12 -0
  158. package/dist/src/lib/canonical-json.js +35 -0
  159. package/dist/src/lib/canonical-json.js.map +1 -0
  160. package/dist/src/lib/channel.d.ts +30 -0
  161. package/dist/src/lib/channel.js +68 -0
  162. package/dist/src/lib/channel.js.map +1 -0
  163. package/dist/src/lib/cli-manifest.d.ts +68 -0
  164. package/dist/src/lib/cli-manifest.js +71 -0
  165. package/dist/src/lib/cli-manifest.js.map +1 -0
  166. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
  167. package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
  168. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
  169. package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
  170. package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
  171. package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
  172. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
  173. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
  174. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
  175. package/dist/src/lib/collection-export.d.ts +184 -0
  176. package/dist/src/lib/collection-export.js +252 -0
  177. package/dist/src/lib/collection-export.js.map +1 -0
  178. package/dist/src/lib/config-json-field.d.ts +28 -0
  179. package/dist/src/lib/config-json-field.js +56 -0
  180. package/dist/src/lib/config-json-field.js.map +1 -0
  181. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  182. package/dist/src/lib/config-object-descriptor.js +740 -0
  183. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  184. package/dist/src/lib/config-payload.d.ts +92 -0
  185. package/dist/src/lib/config-payload.js +161 -0
  186. package/dist/src/lib/config-payload.js.map +1 -0
  187. package/dist/src/lib/config-surface.d.ts +141 -0
  188. package/dist/src/lib/config-surface.js +368 -0
  189. package/dist/src/lib/config-surface.js.map +1 -0
  190. package/dist/src/lib/config-toml.d.ts +10 -0
  191. package/dist/src/lib/config-toml.js +42 -0
  192. package/dist/src/lib/config-toml.js.map +1 -0
  193. package/dist/src/lib/config.d.ts +71 -0
  194. package/dist/src/lib/config.js +71 -68
  195. package/dist/src/lib/config.js.map +1 -1
  196. package/dist/src/lib/confirm-prompt.d.ts +83 -0
  197. package/dist/src/lib/confirm-prompt.js +110 -0
  198. package/dist/src/lib/confirm-prompt.js.map +1 -0
  199. package/dist/src/lib/constants.d.ts +11 -0
  200. package/dist/src/lib/constants.js +12 -0
  201. package/dist/src/lib/constants.js.map +1 -0
  202. package/dist/src/lib/crash-handlers.d.ts +20 -0
  203. package/dist/src/lib/crash-handlers.js +49 -0
  204. package/dist/src/lib/crash-handlers.js.map +1 -0
  205. package/dist/src/lib/credentials-store.d.ts +104 -0
  206. package/dist/src/lib/credentials-store.js +336 -0
  207. package/dist/src/lib/credentials-store.js.map +1 -0
  208. package/dist/src/lib/csv.d.ts +47 -0
  209. package/dist/src/lib/csv.js +172 -0
  210. package/dist/src/lib/csv.js.map +1 -0
  211. package/dist/src/lib/data-input.d.ts +23 -0
  212. package/dist/src/lib/data-input.js +50 -0
  213. package/dist/src/lib/data-input.js.map +1 -0
  214. package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
  215. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  216. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  217. package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
  218. package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
  219. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  220. package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
  221. package/dist/src/lib/db-codegen/dbNaming.js +180 -0
  222. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  223. package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
  224. package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
  225. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  226. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
  227. package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
  228. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  229. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
  230. package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
  231. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
  232. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
  233. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
  234. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
  235. package/dist/src/lib/deprecation.d.ts +22 -0
  236. package/dist/src/lib/deprecation.js +43 -0
  237. package/dist/src/lib/deprecation.js.map +1 -0
  238. package/dist/src/lib/document-export-permissions.d.ts +30 -0
  239. package/dist/src/lib/document-export-permissions.js +54 -0
  240. package/dist/src/lib/document-export-permissions.js.map +1 -0
  241. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  242. package/dist/src/lib/document-ingest-artifact.js +505 -0
  243. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  244. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  245. package/dist/src/lib/document-ingest-input.js +132 -0
  246. package/dist/src/lib/document-ingest-input.js.map +1 -0
  247. package/dist/src/lib/document-ingest-rows.d.ts +137 -0
  248. package/dist/src/lib/document-ingest-rows.js +181 -0
  249. package/dist/src/lib/document-ingest-rows.js.map +1 -0
  250. package/dist/src/lib/document-ingest.d.ts +101 -0
  251. package/dist/src/lib/document-ingest.js +384 -0
  252. package/dist/src/lib/document-ingest.js.map +1 -0
  253. package/dist/src/lib/env-resolver-core.d.ts +258 -0
  254. package/dist/src/lib/env-resolver-core.js +447 -0
  255. package/dist/src/lib/env-resolver-core.js.map +1 -0
  256. package/dist/src/lib/env-resolver.d.ts +99 -0
  257. package/dist/src/lib/env-resolver.js +153 -0
  258. package/dist/src/lib/env-resolver.js.map +1 -0
  259. package/dist/src/lib/fetch.d.ts +5 -0
  260. package/dist/src/lib/function-bundle.d.ts +147 -0
  261. package/dist/src/lib/function-bundle.js +341 -0
  262. package/dist/src/lib/function-bundle.js.map +1 -0
  263. package/dist/src/lib/function-collect.d.ts +123 -0
  264. package/dist/src/lib/function-collect.js +610 -0
  265. package/dist/src/lib/function-collect.js.map +1 -0
  266. package/dist/src/lib/function-db-types.d.ts +202 -0
  267. package/dist/src/lib/function-db-types.js +870 -0
  268. package/dist/src/lib/function-db-types.js.map +1 -0
  269. package/dist/src/lib/function-document-types.d.ts +144 -0
  270. package/dist/src/lib/function-document-types.js +370 -0
  271. package/dist/src/lib/function-document-types.js.map +1 -0
  272. package/dist/src/lib/function-grants-preflight.d.ts +64 -0
  273. package/dist/src/lib/function-grants-preflight.js +105 -0
  274. package/dist/src/lib/function-grants-preflight.js.map +1 -0
  275. package/dist/src/lib/function-log-lines.d.ts +76 -0
  276. package/dist/src/lib/function-log-lines.js +160 -0
  277. package/dist/src/lib/function-log-lines.js.map +1 -0
  278. package/dist/src/lib/function-log-row.d.ts +29 -0
  279. package/dist/src/lib/function-log-row.js +73 -0
  280. package/dist/src/lib/function-log-row.js.map +1 -0
  281. package/dist/src/lib/function-log-tail.d.ts +132 -0
  282. package/dist/src/lib/function-log-tail.js +262 -0
  283. package/dist/src/lib/function-log-tail.js.map +1 -0
  284. package/dist/src/lib/function-run.d.ts +289 -0
  285. package/dist/src/lib/function-run.js +389 -0
  286. package/dist/src/lib/function-run.js.map +1 -0
  287. package/dist/src/lib/function-schema-codegen.d.ts +143 -0
  288. package/dist/src/lib/function-schema-codegen.js +420 -0
  289. package/dist/src/lib/function-schema-codegen.js.map +1 -0
  290. package/dist/src/lib/function-sync.d.ts +459 -0
  291. package/dist/src/lib/function-sync.js +1258 -0
  292. package/dist/src/lib/function-sync.js.map +1 -0
  293. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  294. package/dist/src/lib/function-trigger-listing.js +70 -0
  295. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  296. package/dist/src/lib/function-typecheck.d.ts +86 -0
  297. package/dist/src/lib/function-typecheck.js +370 -0
  298. package/dist/src/lib/function-typecheck.js.map +1 -0
  299. package/dist/src/lib/function-versions.d.ts +122 -0
  300. package/dist/src/lib/function-versions.js +182 -0
  301. package/dist/src/lib/function-versions.js.map +1 -0
  302. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  303. package/dist/src/lib/generated-allowlist.js +281 -0
  304. package/dist/src/lib/generated-allowlist.js.map +1 -0
  305. package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
  306. package/dist/src/lib/generated-config-surfaces.js +10569 -0
  307. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  308. package/dist/src/lib/generated-sdk-types.d.ts +12 -0
  309. package/dist/src/lib/generated-sdk-types.js +13 -0
  310. package/dist/src/lib/generated-sdk-types.js.map +1 -0
  311. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  312. package/dist/src/lib/generated-template-lint.js +624 -0
  313. package/dist/src/lib/generated-template-lint.js.map +1 -0
  314. package/dist/src/lib/init-adopt.d.ts +16 -0
  315. package/dist/src/lib/init-adopt.js +34 -0
  316. package/dist/src/lib/init-adopt.js.map +1 -0
  317. package/dist/src/lib/init-assets.d.ts +39 -0
  318. package/dist/src/lib/init-assets.js +97 -0
  319. package/dist/src/lib/init-assets.js.map +1 -0
  320. package/dist/src/lib/init-client-platforms.d.ts +14 -0
  321. package/dist/src/lib/init-client-platforms.js +71 -0
  322. package/dist/src/lib/init-client-platforms.js.map +1 -0
  323. package/dist/src/lib/init-config.d.ts +98 -0
  324. package/dist/src/lib/init-config.js +186 -0
  325. package/dist/src/lib/init-config.js.map +1 -0
  326. package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
  327. package/dist/src/lib/init-email-redirect-uris.js +46 -0
  328. package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
  329. package/dist/src/lib/init-ios-links.d.ts +91 -0
  330. package/dist/src/lib/init-ios-links.js +219 -0
  331. package/dist/src/lib/init-ios-links.js.map +1 -0
  332. package/dist/src/lib/init-plan.d.ts +80 -0
  333. package/dist/src/lib/init-plan.js +95 -0
  334. package/dist/src/lib/init-plan.js.map +1 -0
  335. package/dist/src/lib/init-production-env.d.ts +48 -0
  336. package/dist/src/lib/init-production-env.js +59 -0
  337. package/dist/src/lib/init-production-env.js.map +1 -0
  338. package/dist/src/lib/init-schema.d.ts +74 -0
  339. package/dist/src/lib/init-schema.js +358 -0
  340. package/dist/src/lib/init-schema.js.map +1 -0
  341. package/dist/src/lib/init-xcode.d.ts +34 -0
  342. package/dist/src/lib/init-xcode.js +138 -0
  343. package/dist/src/lib/init-xcode.js.map +1 -0
  344. package/dist/src/lib/integration-request-config.d.ts +30 -0
  345. package/dist/src/lib/integration-request-config.js +145 -0
  346. package/dist/src/lib/integration-request-config.js.map +1 -0
  347. package/dist/src/lib/integration-selector.d.ts +42 -0
  348. package/dist/src/lib/integration-selector.js +46 -0
  349. package/dist/src/lib/integration-selector.js.map +1 -0
  350. package/dist/src/lib/ios-app-id.d.ts +34 -0
  351. package/dist/src/lib/ios-app-id.js +69 -0
  352. package/dist/src/lib/ios-app-id.js.map +1 -0
  353. package/dist/src/lib/list-options.d.ts +68 -0
  354. package/dist/src/lib/list-options.js +89 -0
  355. package/dist/src/lib/list-options.js.map +1 -0
  356. package/dist/src/lib/local-state.d.ts +55 -0
  357. package/dist/src/lib/local-state.js +167 -0
  358. package/dist/src/lib/local-state.js.map +1 -0
  359. package/dist/src/lib/local-test-cases.d.ts +63 -0
  360. package/dist/src/lib/local-test-cases.js +136 -0
  361. package/dist/src/lib/local-test-cases.js.map +1 -0
  362. package/dist/src/lib/log-inspection.d.ts +715 -0
  363. package/dist/src/lib/log-inspection.js +816 -0
  364. package/dist/src/lib/log-inspection.js.map +1 -0
  365. package/dist/src/lib/logout-admin-session.d.ts +33 -0
  366. package/dist/src/lib/logout-admin-session.js +70 -0
  367. package/dist/src/lib/logout-admin-session.js.map +1 -0
  368. package/dist/src/lib/migration-nag.d.ts +49 -0
  369. package/dist/src/lib/migration-nag.js +163 -0
  370. package/dist/src/lib/migration-nag.js.map +1 -0
  371. package/dist/src/lib/object-status-filter.d.ts +22 -0
  372. package/dist/src/lib/object-status-filter.js +45 -0
  373. package/dist/src/lib/object-status-filter.js.map +1 -0
  374. package/dist/src/lib/output.d.ts +124 -0
  375. package/dist/src/lib/output.js +219 -8
  376. package/dist/src/lib/output.js.map +1 -1
  377. package/dist/src/lib/package-manager.d.ts +140 -0
  378. package/dist/src/lib/package-manager.js +305 -0
  379. package/dist/src/lib/package-manager.js.map +1 -0
  380. package/dist/src/lib/paginate.d.ts +98 -0
  381. package/dist/src/lib/paginate.js +112 -0
  382. package/dist/src/lib/paginate.js.map +1 -0
  383. package/dist/src/lib/platform-owned.d.ts +63 -0
  384. package/dist/src/lib/platform-owned.js +85 -0
  385. package/dist/src/lib/platform-owned.js.map +1 -0
  386. package/dist/src/lib/project-config.d.ts +122 -0
  387. package/dist/src/lib/project-config.js +244 -0
  388. package/dist/src/lib/project-config.js.map +1 -0
  389. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  390. package/dist/src/lib/prompt-cost-format.js +41 -0
  391. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  392. package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
  393. package/dist/src/lib/prompt-schema-codegen.js +462 -0
  394. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  395. package/dist/src/lib/query-operators.d.ts +43 -0
  396. package/dist/src/lib/query-operators.js +80 -0
  397. package/dist/src/lib/query-operators.js.map +1 -0
  398. package/dist/src/lib/record-filter.d.ts +18 -0
  399. package/dist/src/lib/record-filter.js +55 -0
  400. package/dist/src/lib/record-filter.js.map +1 -0
  401. package/dist/src/lib/refresh-admin-credentials.d.ts +73 -0
  402. package/dist/src/lib/refresh-admin-credentials.js +123 -0
  403. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  404. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  405. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  406. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  407. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  408. package/dist/src/lib/resolve-init-server.js +77 -0
  409. package/dist/src/lib/resolve-init-server.js.map +1 -0
  410. package/dist/src/lib/resolve-owner.d.ts +19 -0
  411. package/dist/src/lib/resolve-owner.js +20 -0
  412. package/dist/src/lib/resolve-owner.js.map +1 -0
  413. package/dist/src/lib/resolve-platform.d.ts +74 -0
  414. package/dist/src/lib/resolve-platform.js +105 -0
  415. package/dist/src/lib/resolve-platform.js.map +1 -0
  416. package/dist/src/lib/run-status.d.ts +19 -0
  417. package/dist/src/lib/run-status.generated.d.ts +39 -0
  418. package/dist/src/lib/run-status.generated.js +66 -0
  419. package/dist/src/lib/run-status.generated.js.map +1 -0
  420. package/dist/src/lib/run-status.js +19 -0
  421. package/dist/src/lib/run-status.js.map +1 -0
  422. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  423. package/dist/src/lib/server-text-normalization.js +90 -0
  424. package/dist/src/lib/server-text-normalization.js.map +1 -0
  425. package/dist/src/lib/server-url.d.ts +22 -0
  426. package/dist/src/lib/server-url.js +33 -0
  427. package/dist/src/lib/server-url.js.map +1 -0
  428. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  429. package/dist/src/lib/signing-secret-status.js +116 -0
  430. package/dist/src/lib/signing-secret-status.js.map +1 -0
  431. package/dist/src/lib/skill-installer.d.ts +71 -0
  432. package/dist/src/lib/skill-installer.js +441 -0
  433. package/dist/src/lib/skill-installer.js.map +1 -0
  434. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  435. package/dist/src/lib/snapshot-audit-source.js +58 -0
  436. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  437. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  438. package/dist/src/lib/snapshot-audit-store.js +196 -0
  439. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  440. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  441. package/dist/src/lib/snapshot-audit.js +431 -0
  442. package/dist/src/lib/snapshot-audit.js.map +1 -0
  443. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  444. package/dist/src/lib/snapshot-build-rows.js +87 -0
  445. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  446. package/dist/src/lib/snapshot-build.d.ts +50 -0
  447. package/dist/src/lib/snapshot-build.js +111 -0
  448. package/dist/src/lib/snapshot-build.js.map +1 -0
  449. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  450. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  451. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  452. package/dist/src/lib/snapshots.d.ts +98 -0
  453. package/dist/src/lib/snapshots.js +294 -0
  454. package/dist/src/lib/snapshots.js.map +1 -0
  455. package/dist/src/lib/step-run-table.d.ts +43 -0
  456. package/dist/src/lib/step-run-table.js +129 -0
  457. package/dist/src/lib/step-run-table.js.map +1 -0
  458. package/dist/src/lib/storage-pending-retry.d.ts +26 -0
  459. package/dist/src/lib/storage-pending-retry.js +42 -0
  460. package/dist/src/lib/storage-pending-retry.js.map +1 -0
  461. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  462. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  463. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  464. package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
  465. package/dist/src/lib/swift-codegen/banners.js +25 -0
  466. package/dist/src/lib/swift-codegen/banners.js.map +1 -0
  467. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
  468. package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
  469. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
  470. package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
  471. package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
  472. package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
  473. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
  474. package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
  475. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
  476. package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
  477. package/dist/src/lib/swift-codegen/generator.js +457 -0
  478. package/dist/src/lib/swift-codegen/generator.js.map +1 -0
  479. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
  480. package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
  481. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
  482. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
  483. package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
  484. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
  485. package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
  486. package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
  487. package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
  488. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  489. package/dist/src/lib/sync-dir-selector.js +30 -0
  490. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  491. package/dist/src/lib/sync-paths.d.ts +128 -0
  492. package/dist/src/lib/sync-paths.js +195 -0
  493. package/dist/src/lib/sync-paths.js.map +1 -0
  494. package/dist/src/lib/sync-resource-types.d.ts +563 -0
  495. package/dist/src/lib/sync-resource-types.js +1073 -0
  496. package/dist/src/lib/sync-resource-types.js.map +1 -0
  497. package/dist/src/lib/sync-selectors.d.ts +138 -0
  498. package/dist/src/lib/sync-selectors.js +289 -0
  499. package/dist/src/lib/sync-selectors.js.map +1 -0
  500. package/dist/src/lib/template.d.ts +170 -0
  501. package/dist/src/lib/template.js +484 -68
  502. package/dist/src/lib/template.js.map +1 -1
  503. package/dist/src/lib/test-case-file-names.d.ts +40 -0
  504. package/dist/src/lib/test-case-file-names.js +91 -0
  505. package/dist/src/lib/test-case-file-names.js.map +1 -0
  506. package/dist/src/lib/test-case-keys.d.ts +29 -0
  507. package/dist/src/lib/test-case-keys.js +55 -0
  508. package/dist/src/lib/test-case-keys.js.map +1 -0
  509. package/dist/src/lib/test-case-variables.d.ts +29 -0
  510. package/dist/src/lib/test-case-variables.js +71 -0
  511. package/dist/src/lib/test-case-variables.js.map +1 -0
  512. package/dist/src/lib/token-inject.d.ts +56 -0
  513. package/dist/src/lib/token-inject.js +204 -0
  514. package/dist/src/lib/token-inject.js.map +1 -0
  515. package/dist/src/lib/toml-database-config.d.ts +123 -0
  516. package/dist/src/lib/toml-database-config.js +544 -0
  517. package/dist/src/lib/toml-database-config.js.map +1 -0
  518. package/dist/src/lib/toml-metadata-config.d.ts +151 -0
  519. package/dist/src/lib/toml-metadata-config.js +476 -0
  520. package/dist/src/lib/toml-metadata-config.js.map +1 -0
  521. package/dist/src/lib/toml-native-form.d.ts +46 -0
  522. package/dist/src/lib/toml-native-form.js +78 -0
  523. package/dist/src/lib/toml-native-form.js.map +1 -0
  524. package/dist/src/lib/toml-params-validator.d.ts +129 -0
  525. package/dist/src/lib/toml-params-validator.js +298 -0
  526. package/dist/src/lib/toml-params-validator.js.map +1 -0
  527. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  528. package/dist/src/lib/toml-scalar-edit.js +283 -0
  529. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  530. package/dist/src/lib/user-selector.d.ts +24 -0
  531. package/dist/src/lib/user-selector.js +33 -0
  532. package/dist/src/lib/user-selector.js.map +1 -0
  533. package/dist/src/lib/version-check.d.ts +35 -0
  534. package/dist/src/lib/version-check.js +241 -0
  535. package/dist/src/lib/version-check.js.map +1 -0
  536. package/dist/src/lib/watch.d.ts +121 -0
  537. package/dist/src/lib/watch.js +169 -0
  538. package/dist/src/lib/watch.js.map +1 -0
  539. package/dist/src/lib/web-url.d.ts +40 -0
  540. package/dist/src/lib/web-url.js +76 -0
  541. package/dist/src/lib/web-url.js.map +1 -0
  542. package/dist/src/lib/webhook-deliver.d.ts +209 -0
  543. package/dist/src/lib/webhook-deliver.js +519 -0
  544. package/dist/src/lib/webhook-deliver.js.map +1 -0
  545. package/dist/src/lib/workflow-apply.d.ts +110 -0
  546. package/dist/src/lib/workflow-apply.js +164 -0
  547. package/dist/src/lib/workflow-apply.js.map +1 -0
  548. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
  549. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
  550. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
  551. package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
  552. package/dist/src/lib/workflow-codegen/generator.js +361 -0
  553. package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
  554. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
  555. package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
  556. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
  557. package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
  558. package/dist/src/lib/workflow-codegen/naming.js +81 -0
  559. package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
  560. package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
  561. package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
  562. package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
  563. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  564. package/dist/src/lib/workflow-config-apply.js +137 -0
  565. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  566. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  567. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  568. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  569. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  570. package/dist/src/lib/workflow-defaults.js +41 -0
  571. package/dist/src/lib/workflow-defaults.js.map +1 -0
  572. package/dist/src/lib/workflow-fragments.d.ts +64 -0
  573. package/dist/src/lib/workflow-fragments.js +342 -0
  574. package/dist/src/lib/workflow-fragments.js.map +1 -0
  575. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  576. package/dist/src/lib/workflow-include-preserve.js +286 -0
  577. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  578. package/dist/src/lib/workflow-payload.d.ts +98 -0
  579. package/dist/src/lib/workflow-payload.js +178 -0
  580. package/dist/src/lib/workflow-payload.js.map +1 -0
  581. package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
  582. package/dist/src/lib/workflow-toml-validator.js +770 -0
  583. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  584. package/dist/src/lib/workflow-usage.d.ts +196 -0
  585. package/dist/src/lib/workflow-usage.js +310 -0
  586. package/dist/src/lib/workflow-usage.js.map +1 -0
  587. package/dist/src/types/index.d.ts +591 -0
  588. package/dist/src/validators.d.ts +65 -0
  589. package/dist/src/validators.js +64 -0
  590. package/dist/src/validators.js.map +1 -0
  591. package/package.json +34 -9
@@ -1,36 +1,266 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
+ import { getCurrentEnvNameSafe } from "./env-resolver.js";
2
3
  import { fetchWithTLS } from "./fetch.js";
4
+ import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
5
+ import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
6
+ import { drainAppsUntilConverged, } from "./workflow-usage.js";
7
+ /**
8
+ * Page size the admin per-user inventories are asked for (#3399).
9
+ *
10
+ * `MAX_LIST_LIMIT` on the server, so a heavy user's inventory is walked in the
11
+ * fewest round trips the contract allows; a larger value is clamped there.
12
+ */
13
+ const ADMIN_INVENTORY_PAGE_SIZE = 100;
3
14
  export class ApiError extends Error {
4
15
  statusCode;
5
16
  code;
6
- constructor(message, statusCode, code) {
17
+ /**
18
+ * Structured `details` payload from `corsErrorResponse`.
19
+ *
20
+ * The server's error envelope can emit `details` as either:
21
+ * - an array of validation issues (e.g. `[{ path, message }, ...]`) —
22
+ * this is what most legacy endpoints produce, and what sync.ts walks
23
+ * via `Array.isArray(err.details)` / `for (const detail of ...)`.
24
+ * - a record of structured offender fields (e.g. `{ refs, operations,
25
+ * opCount, line, column, ... }`) — emitted by the issue #666 schema
26
+ * gate and consumed by the typed exception subclasses below.
27
+ *
28
+ * Callers must narrow before use: `Array.isArray(err.details)` for the
29
+ * legacy shape, otherwise treat as `Record<string, any>`.
30
+ */
31
+ details;
32
+ constructor(message, statusCode, code, details) {
7
33
  super(message);
8
34
  this.statusCode = statusCode;
9
35
  this.code = code;
10
36
  this.name = "ApiError";
37
+ if (details !== undefined) {
38
+ this.details = details;
39
+ }
11
40
  }
12
41
  }
13
42
  export class ConflictError extends ApiError {
14
43
  serverModifiedAt;
15
44
  expectedModifiedAt;
16
- constructor(message, serverModifiedAt, expectedModifiedAt) {
17
- super(message, 409, "CONFLICT");
45
+ constructor(message, serverModifiedAt, expectedModifiedAt, details) {
46
+ super(message, 409, "CONFLICT", details);
18
47
  this.serverModifiedAt = serverModifiedAt;
19
48
  this.expectedModifiedAt = expectedModifiedAt;
20
49
  this.name = "ConflictError";
21
50
  }
22
51
  }
52
+ /**
53
+ * The `ApiError` for a non-OK response read by a raw `fetch` call (#3403).
54
+ *
55
+ * The blob methods and the two document-artifact transfers build their own
56
+ * `fetch` call, so they never reached `parseErrorResponse` and never carried a
57
+ * `code` — a download refusal surfaced as `response.statusText` and an upload
58
+ * refusal as the raw JSON body shown to the operator as the message. This
59
+ * routes them through the same parser every other call site uses while keeping
60
+ * their human prefixes.
61
+ *
62
+ * Named for the blob paths it was written for; every raw-`fetch` path in this
63
+ * file answers through it now.
64
+ */
65
+ export async function blobFailure(response, prefix) {
66
+ const text = await response.text().catch(() => "");
67
+ const parsed = parseErrorResponse(response, text);
68
+ return new ApiError(`${prefix}${parsed.message}`, response.status, parsed.code, parsed.details);
69
+ }
70
+ /**
71
+ * Extract a human-readable error message + structured fields from a non-OK
72
+ * HTTP response body. Single source of truth used by every error-handler call
73
+ * site in this file (see issue #684).
74
+ */
75
+ export function parseErrorResponse(response, text, path) {
76
+ // Empty body → fall back to status code.
77
+ if (!text) {
78
+ return { message: `HTTP ${response.status}` };
79
+ }
80
+ let errorData;
81
+ try {
82
+ errorData = JSON.parse(text);
83
+ }
84
+ catch {
85
+ // Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
86
+ // 404 page from hitting the wrong path) so we don't regress the helpful
87
+ // "API endpoint not found" message at api-client.ts:343.
88
+ if (text.includes("<!DOCTYPE")) {
89
+ const where = path ? `: ${path}` : "";
90
+ return {
91
+ message: `API endpoint not found${where}. Make sure the server is running.`,
92
+ htmlNotFound: true,
93
+ };
94
+ }
95
+ // Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
96
+ // raw text so the operator at least sees what the server returned.
97
+ return { message: text };
98
+ }
99
+ // Server's standard envelope uses `error`; ConflictError + integrations
100
+ // proxy use `message`. Prefer `error` (more common), fall back to `message`.
101
+ const message = (typeof errorData?.error === "string" && errorData.error) ||
102
+ (typeof errorData?.message === "string" && errorData.message) ||
103
+ `HTTP ${response.status}`;
104
+ // Per issue #666 addendum A1, `code` may be at the top level or nested
105
+ // under `details.code` when the server's bespoke envelope didn't flatten.
106
+ const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
107
+ (typeof errorData?.details?.code === "string"
108
+ ? errorData.details.code
109
+ : undefined);
110
+ // Accept either an array (legacy) or a plain object (#666 schema gate).
111
+ const details = Array.isArray(errorData?.details)
112
+ ? errorData.details
113
+ : errorData?.details && typeof errorData.details === "object"
114
+ ? errorData.details
115
+ : undefined;
116
+ return { message, code, details, raw: errorData };
117
+ }
118
+ /**
119
+ * Typed exception classes for the database-schema feature (issue #666).
120
+ * Each maps 1:1 to a server `code` value emitted from the op-edit or
121
+ * schema-edit gate. They all extend ApiError so existing catch-all paths
122
+ * continue to work; specialized catch blocks can branch on `instanceof`.
123
+ *
124
+ * Per round-2 addendum A1, `details` is always preserved so callers can
125
+ * extract structured offender lists (refs[], operations[], etc.).
126
+ */
127
+ export class SchemaRequiredError extends ApiError {
128
+ constructor(message, details) {
129
+ super(message, 422, "SCHEMA_REQUIRED", details);
130
+ this.name = "SchemaRequiredError";
131
+ }
132
+ }
133
+ function detailsRecord(details) {
134
+ return details && !Array.isArray(details) && typeof details === "object"
135
+ ? details
136
+ : undefined;
137
+ }
138
+ export class OperationRefError extends ApiError {
139
+ constructor(message, details) {
140
+ super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
141
+ this.name = "OperationRefError";
142
+ }
143
+ get refs() {
144
+ const d = detailsRecord(this.details);
145
+ return Array.isArray(d?.refs) ? d.refs : [];
146
+ }
147
+ }
148
+ export class SchemaBreaksOpsError extends ApiError {
149
+ constructor(message, details) {
150
+ super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
151
+ this.name = "SchemaBreaksOpsError";
152
+ }
153
+ get operations() {
154
+ const d = detailsRecord(this.details);
155
+ return Array.isArray(d?.operations) ? d.operations : [];
156
+ }
157
+ }
158
+ export class SchemaHasUncheckableOpsError extends ApiError {
159
+ constructor(message, details) {
160
+ super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
161
+ this.name = "SchemaHasUncheckableOpsError";
162
+ }
163
+ get operations() {
164
+ const d = detailsRecord(this.details);
165
+ return Array.isArray(d?.operations) ? d.operations : [];
166
+ }
167
+ }
168
+ export class TomlParseError extends ApiError {
169
+ constructor(message, details) {
170
+ super(message, 400, "TOML_PARSE_ERROR", details);
171
+ this.name = "TomlParseError";
172
+ }
173
+ get line() {
174
+ const d = detailsRecord(this.details);
175
+ return typeof d?.line === "number" ? d.line : undefined;
176
+ }
177
+ get column() {
178
+ const d = detailsRecord(this.details);
179
+ return typeof d?.column === "number" ? d.column : undefined;
180
+ }
181
+ }
182
+ export class OpsExistError extends ApiError {
183
+ constructor(message, details) {
184
+ super(message, 409, "OPS_EXIST", details);
185
+ this.name = "OpsExistError";
186
+ }
187
+ get opCount() {
188
+ const d = detailsRecord(this.details);
189
+ return typeof d?.opCount === "number" ? d.opCount : 0;
190
+ }
191
+ }
192
+ /**
193
+ * Normalize a `databases operations execute` result to the CLI's one list
194
+ * envelope — but only when the result is a Durable Object query page
195
+ * (issue #2440).
196
+ *
197
+ * A registered operation is a list only sometimes. Of the shapes the server's
198
+ * operation dispatch can return, exactly two carry a top-level `data` array: a
199
+ * bare `query` and a `pipeline` whose `returnField` names a query step. A
200
+ * `count` (`{ count }`), an `aggregate` (`{ result }`), a mutation
201
+ * (`{ results }`), an `applyToQuery` (`{ matched, affected, failed, … }`) and a
202
+ * `returnField: "all"` pipeline (`{ steps: { … } }`) do not, so blanket
203
+ * normalization would corrupt them. This helper therefore tests the shape
204
+ * first and passes everything else through byte-for-byte — including the
205
+ * nested steps of a `returnField: "all"` pipeline, which are operation-defined
206
+ * objects the CLI must not reach into.
207
+ *
208
+ * Deliberately narrow, not a general-purpose predicate: the contract is
209
+ * specifically "an operations-execute result may be a DO query page".
210
+ * `executeDatabaseOperation()` is the only caller.
211
+ *
212
+ * The conversion itself is delegated to `normalizeCliListEnvelope()` so cursor
213
+ * handling, `prevCursor` dropping and `hasMore` derivation have exactly one
214
+ * implementation. That helper throws on an unrecognized shape — correct when
215
+ * the payload is supposed to be a list, wrong here, where the payload is the
216
+ * caller's own result — which is why the shape test has to run first. `_timing`
217
+ * (`--timing`) is re-attached afterwards, since it rides alongside the envelope
218
+ * rather than inside it.
219
+ */
220
+ export function normalizeDatabaseOperationResult(raw) {
221
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
222
+ return raw;
223
+ const page = raw;
224
+ if (!Array.isArray(page.data))
225
+ return raw;
226
+ const envelope = normalizeCliListEnvelope(page);
227
+ if (page._timing !== undefined)
228
+ envelope._timing = page._timing;
229
+ return envelope;
230
+ }
23
231
  export class ApiClient {
232
+ /**
233
+ * Loaded on first use (`ensureAuthenticated`), never in a constructor.
234
+ *
235
+ * There used to be an `apiClient` singleton constructed at MODULE scope, so
236
+ * a constructor body ran while `bin/primitive.ts` was still being imported —
237
+ * before its first `try`, and before any command had said whether it needed
238
+ * the project at all. `loadCredentials()` resolves the environment, so on a
239
+ * project still holding the pre-#3153 layout that read threw the stale-layout
240
+ * refusal out of module evaluation, where not even the `uncaughtException`
241
+ * handler can reach it: the user got Node's raw stack trace, and `guides
242
+ * get` — which reads nothing from the project — died with it (#3273). The
243
+ * singleton is gone (#3154, it had no callers) and the read stays lazy,
244
+ * which is what keeps that shape from coming back.
245
+ *
246
+ * Nothing is lost by waiting, and one thing is gained: a `login` earlier in
247
+ * the same process is visible to a later request.
248
+ */
24
249
  credentials = null;
25
- constructor() {
26
- this.credentials = loadCredentials();
27
- }
28
250
  async ensureAuthenticated() {
29
251
  if (!this.credentials) {
30
252
  this.credentials = loadCredentials();
31
253
  }
32
254
  if (!this.credentials) {
33
- throw new ApiError("Not logged in. Run 'bao-admin login' first.", 401);
255
+ // Name the environment when there is one (#1293). Before #3152 this case
256
+ // surfaced from `resolveAppId`, because an unauthenticated environment
257
+ // resolved no app at all; now the environment always names its app, so
258
+ // "not logged in" is only ever an auth failure, and it is reported here,
259
+ // once, for every command.
260
+ const envName = getCurrentEnvNameSafe();
261
+ throw new ApiError(envName
262
+ ? `Not logged in to environment "${envName}". Run 'primitive -e ${envName} login'.`
263
+ : "Not logged in. Run 'primitive login' first.", 401);
34
264
  }
35
265
  // Check if token needs refresh
36
266
  if (isTokenExpiringSoon(this.credentials)) {
@@ -39,39 +269,27 @@ export class ApiClient {
39
269
  return this.credentials;
40
270
  }
41
271
  async refreshToken() {
42
- if (!this.credentials?.refreshToken) {
43
- throw new ApiError("No refresh token available. Please login again.", 401);
272
+ if (!this.credentials) {
273
+ throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
44
274
  }
45
- const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
46
275
  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
- };
276
+ const updated = await refreshAdminCredentials(this.credentials);
277
+ this.credentials = updated;
69
278
  saveCredentials(this.credentials);
70
279
  }
71
- catch (error) {
72
- if (error instanceof ApiError)
73
- throw error;
74
- throw new ApiError("Token refresh failed. Please login again.", 401);
280
+ catch (err) {
281
+ if (err instanceof RefreshError) {
282
+ // Preserve historical behavior: ApiClient surfaces refresh failures
283
+ // as 401s regardless of whether the underlying cause was a network
284
+ // error or a server-side rejection. Callers that need a finer
285
+ // distinction (e.g. `primitive token`) consume RefreshError directly
286
+ // from refresh-admin-credentials.ts instead of going through here.
287
+ // A revoked session (#3885) says so.
288
+ throw new ApiError(err.code === "SESSION_REVOKED"
289
+ ? err.message
290
+ : "Token refresh failed. Please login again.", 401, err.code);
291
+ }
292
+ throw err;
75
293
  }
76
294
  }
77
295
  async request(path, options = {}) {
@@ -92,20 +310,42 @@ export class ApiClient {
92
310
  });
93
311
  const text = await response.text();
94
312
  if (!response.ok) {
95
- let errorData;
96
- try {
97
- errorData = JSON.parse(text);
313
+ const parsed = parseErrorResponse(response, text, path);
314
+ // Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
315
+ if (parsed.htmlNotFound) {
316
+ throw new ApiError(parsed.message, 404);
98
317
  }
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}` };
318
+ // Narrow details to the record shape for the issue #666 typed-exception
319
+ // dispatch below. Conflict metadata may live under `details.*` (canonical
320
+ // location per A1) or on the top-level envelope (legacy).
321
+ const detailsRecord = parsed.details && !Array.isArray(parsed.details)
322
+ ? parsed.details
323
+ : undefined;
324
+ const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
325
+ const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
326
+ // Typed exceptions for the schema-feature (issue #666).
327
+ if (response.status === 409 && parsed.code === "CONFLICT") {
328
+ throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
329
+ }
330
+ if (response.status === 409 && parsed.code === "OPS_EXIST") {
331
+ throw new OpsExistError(parsed.message, detailsRecord);
332
+ }
333
+ if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
334
+ throw new TomlParseError(parsed.message, detailsRecord);
104
335
  }
105
- if (response.status === 409 && errorData?.code === "CONFLICT") {
106
- throw new ConflictError(errorData.message || "Resource conflict", errorData.serverModifiedAt, errorData.expectedModifiedAt);
336
+ if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
337
+ throw new SchemaRequiredError(parsed.message, detailsRecord);
107
338
  }
108
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
339
+ if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
340
+ throw new OperationRefError(parsed.message, detailsRecord);
341
+ }
342
+ if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
343
+ throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
344
+ }
345
+ if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
346
+ throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
347
+ }
348
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
109
349
  }
110
350
  return text ? JSON.parse(text) : null;
111
351
  }
@@ -154,8 +394,79 @@ export class ApiClient {
154
394
  // ============================================
155
395
  // APPS
156
396
  // ============================================
397
+ /**
398
+ * One page of the apps the caller can reach.
399
+ *
400
+ * The route paginates cursor-style (default pageSize=25, cap=100). This
401
+ * method used to walk the whole chain so `primitive apps list` could not
402
+ * truncate at 25 (#436); since #3646 the page boundary belongs to the
403
+ * caller instead — `apps list` declares `--limit`/`--cursor` and prints the
404
+ * `hasMore`/`nextCursor` the server reported, and the aggregate readers that
405
+ * really do mean "every app" call `listAllApps()`.
406
+ */
407
+ async listAppsPage(params) {
408
+ const qs = new URLSearchParams();
409
+ if (params?.limit !== undefined)
410
+ qs.set("limit", String(params.limit));
411
+ if (params?.cursor)
412
+ qs.set("cursor", params.cursor);
413
+ const q = qs.toString();
414
+ const resp = await this.get(`/admin/api/admins/me/apps${q ? `?${q}` : ""}`);
415
+ // The server now emits `items` with `apps` as a deprecation-window alias
416
+ // (#1316). Read `items` first so listings keep working once the alias is
417
+ // removed; fall back to the legacy `apps` key meanwhile.
418
+ return {
419
+ items: resp?.items ?? (Array.isArray(resp?.apps) ? resp.apps : []),
420
+ hasMore: resp?.hasMore,
421
+ nextCursor: resp?.nextCursor ?? resp?.cursor ?? null,
422
+ };
423
+ }
424
+ /**
425
+ * Every app the caller can reach, cursor chain walked (#436).
426
+ *
427
+ * For readers that genuinely need the whole set — an app picker, a sweep —
428
+ * rather than the one page `apps list` prints.
429
+ */
157
430
  async listApps() {
158
- return this.get("/admin/api/admins/me/apps");
431
+ const apps = await paginateAll(async (cursor) => {
432
+ const page = await this.listAppsPage({ limit: 100, cursor });
433
+ return { items: page.items, nextCursor: page.nextCursor };
434
+ });
435
+ return { apps };
436
+ }
437
+ /**
438
+ * Every app on the server, not just the caller's own.
439
+ *
440
+ * Deliberately separate from `listApps()`, whose contract is "apps assigned
441
+ * to me" (`GET /admin/api/admins/me/apps`, backed by `admin.queryApps()`) —
442
+ * the contract every other command depends on. `analytics workflow-usage
443
+ * --all-apps` has to reach apps nobody is assigned to, which is the global
444
+ * route `GET /admin/api/apps` (`App.iterateAll()`), and that route
445
+ * identifies an app as `appId` rather than `id` (D3130-001).
446
+ *
447
+ * One drain of that route is lossy — its cursor re-scans `App.iterateAll()`
448
+ * from the start of each page, so a list that changes under it both repeats
449
+ * and drops rows — so the walk is the union of repeated drains
450
+ * (`drainAppsUntilConverged`, which carries the measurement).
451
+ */
452
+ async listAllAppsGlobal() {
453
+ const drainOnce = () => paginateAll(async (cursor) => {
454
+ const qs = new URLSearchParams({ limit: "100" });
455
+ if (cursor)
456
+ qs.set("cursor", cursor);
457
+ const resp = await this.get(`/admin/api/apps?${qs.toString()}`);
458
+ // `items` first, with the `apps` alias (#1316) as the fallback,
459
+ // exactly as `listApps()` reads its own envelope.
460
+ const rows = resp.items ?? (Array.isArray(resp.apps) ? resp.apps : []);
461
+ return {
462
+ items: rows.map((row) => ({
463
+ appId: String(row.appId ?? row.id ?? ""),
464
+ name: String(row.name ?? ""),
465
+ })),
466
+ nextCursor: resp.nextCursor ?? resp.cursor,
467
+ };
468
+ });
469
+ return drainAppsUntilConverged(drainOnce);
159
470
  }
160
471
  async createApp(data) {
161
472
  return this.post("/admin/api/apps", data);
@@ -181,12 +492,77 @@ export class ApiClient {
181
492
  async addUserByEmail(appId, data) {
182
493
  return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
183
494
  }
184
- async listUsers(appId) {
185
- return this.get(`/app/${appId}/api/users`);
495
+ async mintTestJwt(appId, userId, role) {
496
+ return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
497
+ }
498
+ async rebuildUserSearchText(appId) {
499
+ return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
500
+ }
501
+ async listUsers(appId, params) {
502
+ const query = new URLSearchParams();
503
+ if (params?.name)
504
+ query.set("name", params.name);
505
+ if (params?.email)
506
+ query.set("email", params.email);
507
+ if (params?.userId)
508
+ query.set("userId", params.userId);
509
+ if (params?.limit)
510
+ query.set("limit", String(params.limit));
511
+ if (params?.cursor)
512
+ query.set("cursor", params.cursor);
513
+ const path = query.toString()
514
+ ? `/app/${appId}/api/users?${query.toString()}`
515
+ : `/app/${appId}/api/users`;
516
+ const result = await this.get(path);
517
+ return {
518
+ items: result?.items ?? (Array.isArray(result) ? result : []),
519
+ // `hasMore` travels with the page so the caller prints the server's own
520
+ // page boundary rather than inferring one from the cursor (#3646).
521
+ hasMore: result?.hasMore,
522
+ nextCursor: result?.nextCursor ?? null,
523
+ };
186
524
  }
525
+ /**
526
+ * @deprecated Use {@link setUserAvailability} with `"disable"`
527
+ * (`PUT /admin/api/apps/:appId/users/:userId/disable`) instead (#518).
528
+ */
187
529
  async removeUser(appId, userId) {
188
530
  return this.delete(`/app/${appId}/api/users/${userId}`);
189
531
  }
532
+ /**
533
+ * #2803 — take a user's access away, or give it back. Distinct from
534
+ * `removeUser`, which detaches the membership: this blocks the person's auth
535
+ * in the app, revokes their sessions and tokens, and is reversible. Both
536
+ * routes already existed; nothing on the CLI reached them.
537
+ */
538
+ async setUserAvailability(appId, userId, action) {
539
+ return this.put(`/admin/api/apps/${appId}/users/${userId}/${action}`, {});
540
+ }
541
+ // ============================================
542
+ // FEATURE FLAGS (super-admin)
543
+ // ============================================
544
+ /**
545
+ * #2803 — the platform's feature flags. A server-owned admin toggle with a
546
+ * console but, until now, no CLI: the same "one control, both surfaces" rule
547
+ * the carrying types get.
548
+ */
549
+ async listFeatureFlags() {
550
+ return this.get(`/admin/api/flags`);
551
+ }
552
+ /**
553
+ * The flag itself, not the `{ flag }` envelope the route answers with —
554
+ * `feature-flags get` prints the key, the enabled state and the per-app
555
+ * overrides, and reading them off the envelope printed a blank row.
556
+ */
557
+ async getFeatureFlag(flagKey) {
558
+ const result = await this.get(`/admin/api/flags/${flagKey}`);
559
+ return result?.flag ?? result;
560
+ }
561
+ /** Same envelope, same unwrap: `enable`/`disable` print the updated flag. */
562
+ async setFeatureFlagEnabled(flagKey, enabled) {
563
+ const result = await this.put(`/admin/api/flags/${flagKey}`, { enabled });
564
+ return result?.flag ?? result;
565
+ }
190
566
  async updateUserRole(appId, userId, role) {
191
567
  return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
192
568
  }
@@ -196,14 +572,39 @@ export class ApiClient {
196
572
  async transferAdminOwnership(appId, adminId) {
197
573
  return this.post(`/admin/api/apps/${appId}/admins/transfer-ownership`, { adminId });
198
574
  }
575
+ // App-level console admin management
576
+ async listAppAdmins(appId) {
577
+ return this.get(`/admin/api/apps/${appId}/admins`);
578
+ }
579
+ async addAppAdmin(appId, data) {
580
+ return this.post(`/admin/api/apps/${appId}/admins`, data);
581
+ }
582
+ async removeAppAdmin(appId, adminId) {
583
+ return this.delete(`/admin/api/apps/${appId}/admins/${adminId}`);
584
+ }
585
+ async listAppAdminInvitations(appId) {
586
+ return this.get(`/admin/api/apps/${appId}/admin-invitations`);
587
+ }
588
+ async deleteAppAdminInvitation(appId, invitationId) {
589
+ return this.delete(`/admin/api/apps/${appId}/admin-invitations/${invitationId}`);
590
+ }
199
591
  async transferDocumentOwnership(appId, documentId, newOwnerId) {
200
592
  return this.post(`/app/${appId}/api/documents/${documentId}/permissions/transfer`, { newOwnerId });
201
593
  }
202
594
  // ============================================
203
595
  // INVITATIONS
204
596
  // ============================================
205
- async listInvitations(appId) {
206
- return this.get(`/app/${appId}/api/invitations`);
597
+ async listInvitations(appId, params) {
598
+ const result = await this.get(`/app/${appId}/api/invitations`, params);
599
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
600
+ // so the CLI keeps paginating against pre-#1316 servers.
601
+ const nextCursor = result?.nextCursor ?? result?.cursor;
602
+ return {
603
+ items: result?.items ?? [],
604
+ nextCursor,
605
+ hasMore: result?.hasMore ?? nextCursor != null,
606
+ cursor: nextCursor,
607
+ };
207
608
  }
208
609
  async createInvitation(appId, data) {
209
610
  return this.post(`/app/${appId}/api/invitations`, data);
@@ -228,18 +629,47 @@ export class ApiClient {
228
629
  return this.delete(`/app/${appId}/api/waitlist/${waitlistId}`);
229
630
  }
230
631
  // ============================================
632
+ // DEFERRED GRANTS
633
+ // ============================================
634
+ async listDeferredGrants(appId, params) {
635
+ const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
636
+ // `items` is the key every list envelope is built from (#3646, #1982);
637
+ // `grants` is the deprecated legacy name, read only as a fallback.
638
+ const items = result?.items ?? result?.grants ?? [];
639
+ return {
640
+ items,
641
+ grants: items,
642
+ nextCursor: result?.nextCursor ?? null,
643
+ hasMore: result?.hasMore,
644
+ };
645
+ }
646
+ async revokeDeferredGrant(appId, deferredId, type) {
647
+ return this.delete(`/app/${appId}/api/deferred-grants/${deferredId}?type=${type}`);
648
+ }
649
+ // ============================================
231
650
  // INTEGRATIONS
232
651
  // ============================================
233
652
  async listIntegrations(appId, params) {
234
653
  const result = await this.get(`/admin/api/apps/${appId}/integrations`, params);
235
654
  return {
236
655
  items: result?.items ?? [],
656
+ hasMore: result?.hasMore,
237
657
  nextCursor: result?.nextCursor ?? null,
238
658
  };
239
659
  }
660
+ /** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
240
661
  async getIntegration(appId, integrationId) {
241
662
  return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
242
663
  }
664
+ /**
665
+ * An integration's named configs. Test cases pin a config by NAME so the
666
+ * sidecar is portable across apps (#2769); this is the map pull and push
667
+ * resolve that name through, the same way prompts/workflows/scripts do.
668
+ */
669
+ async listIntegrationConfigs(appId, integrationId) {
670
+ const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/configs`);
671
+ return { items: result?.items ?? result ?? [] };
672
+ }
243
673
  async createIntegration(appId, payload) {
244
674
  return this.post(`/admin/api/apps/${appId}/integrations`, payload);
245
675
  }
@@ -247,8 +677,15 @@ export class ApiClient {
247
677
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
248
678
  return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
249
679
  }
680
+ /**
681
+ * Delete an integration. Archives it by default; `{ hard: true }` destroys
682
+ * the row instead, which is what `config push --prune` passes (#2803) so a
683
+ * pruned integration stops holding its key. The positional boolean is still
684
+ * accepted for the existing callers.
685
+ */
250
686
  async deleteIntegration(appId, integrationId, hard) {
251
- const path = hard
687
+ const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
688
+ const path = isHard
252
689
  ? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
253
690
  : `/admin/api/apps/${appId}/integrations/${integrationId}`;
254
691
  return this.delete(path);
@@ -260,17 +697,517 @@ export class ApiClient {
260
697
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
261
698
  return result?.items ?? [];
262
699
  }
263
- async listIntegrationSecrets(appId, integrationId, params) {
264
- const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
700
+ async listWorkflowRunIntegrationLogs(appId, runId, params) {
701
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
265
702
  return result?.items ?? [];
266
703
  }
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;
704
+ // ============================================
705
+ // APP SECRETS
706
+ // ============================================
707
+ async listAppSecrets(appId) {
708
+ const result = await this.get(`/admin/api/apps/${appId}/secrets`);
709
+ return result?.items ?? [];
710
+ }
711
+ async createAppSecret(appId, payload) {
712
+ return this.post(`/admin/api/apps/${appId}/secrets`, payload);
713
+ }
714
+ async updateAppSecret(appId, secretId, payload) {
715
+ return this.put(`/admin/api/apps/${appId}/secrets/${secretId}`, payload);
716
+ }
717
+ async upsertAppSecret(appId, key, payload) {
718
+ return this.put(`/admin/api/apps/${appId}/secrets/by-key/${key}`, payload);
719
+ }
720
+ async deleteAppSecret(appId, secretId) {
721
+ return this.delete(`/admin/api/apps/${appId}/secrets/${secretId}`);
722
+ }
723
+ // ============================================
724
+ // APP CONFIG VARS (issue #1364 — non-secret twin of secrets)
725
+ // ============================================
726
+ async listAppConfigVars(appId) {
727
+ const result = await this.get(`/admin/api/apps/${appId}/vars`);
728
+ return result?.items ?? [];
729
+ }
730
+ async upsertAppConfigVar(appId, key, payload, expectedModifiedAt, options = {}) {
731
+ // Encode the key so a malformed one (e.g. containing "/") reaches the
732
+ // server's key validation (400) instead of producing a routing 404.
733
+ // `expectedModifiedAt` (issue #1423 review r-2 P1) is the optimistic-
734
+ // concurrency precondition for an UPDATE — the server rejects the write
735
+ // with a 409 CONFLICT (surfaced as `ConflictError`) if the var changed
736
+ // since it.
737
+ //
738
+ // `expectNotExists` (issue #1423 review r-3 P1a) is the create-only
739
+ // precondition. A create has no baseline timestamp to send, but without
740
+ // any precondition the by-key upsert silently overwrites a var created
741
+ // remotely since our snapshot. Setting `expectNotExists` makes the server
742
+ // 409 CONFLICT if the key already exists instead of overwriting it.
743
+ const body = { ...payload };
744
+ if (expectedModifiedAt !== undefined)
745
+ body.expectedModifiedAt = expectedModifiedAt;
746
+ if (options.expectNotExists)
747
+ body.expectNotExists = true;
748
+ return this.put(`/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`, body);
749
+ }
750
+ async deleteAppConfigVar(appId, key, expectedModifiedAt) {
751
+ // The DELETE carries the optimistic-concurrency precondition (issue #1423
752
+ // review r-2 P1) as a query param since it has no body; the server 409s
753
+ // (ConflictError) if the var was edited remotely since `expectedModifiedAt`.
754
+ let path = `/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`;
755
+ if (expectedModifiedAt !== undefined) {
756
+ path += `?expectedModifiedAt=${encodeURIComponent(expectedModifiedAt)}`;
757
+ }
758
+ return this.delete(path);
759
+ }
760
+ // ============================================
761
+ // WEBHOOKS
762
+ // ============================================
763
+ async listWebhooks(appId, params) {
764
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
765
+ return {
766
+ items: result?.items ?? [],
767
+ hasMore: result?.hasMore,
768
+ nextCursor: result?.nextCursor ?? null,
769
+ };
770
+ }
771
+ async getWebhook(appId, webhookId) {
772
+ return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
773
+ }
774
+ async createWebhook(appId, payload) {
775
+ return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
776
+ }
777
+ async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
778
+ const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
779
+ return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
780
+ }
781
+ /**
782
+ * Delete a webhook. Archives it by default; `hard` hard-deletes the row
783
+ * instead (#2232), which is what frees a slot under the per-app webhook cap
784
+ * and releases the webhook key for reuse. Same `?hard=true` verb the admin
785
+ * integrations delete uses.
786
+ */
787
+ async deleteWebhook(appId, webhookId, options) {
788
+ const query = options?.hard ? "?hard=true" : "";
789
+ return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}${query}`);
790
+ }
791
+ /**
792
+ * #2803 — the operational verb pair for a webhook. It posts to the
793
+ * enable/disable route rather than PATCHing `status`, which is server-owned
794
+ * and refused on every configuration write path.
795
+ */
796
+ async setWebhookAvailability(appId, webhookId, action) {
797
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
798
+ }
799
+ // ── Server functions (#3179) ─────────────────────────────────────────
800
+ //
801
+ // The `functions` family mirrors workflows and scripts: a header with the
802
+ // gate and the entry, and append-only immutable config versions carrying the
803
+ // authored TOML bytes, the sources and the built bundle. The listing drains
804
+ // server-side, so it has no cursor.
805
+ async listFunctions(appId) {
806
+ const result = await this.get(`/admin/api/apps/${appId}/functions`);
807
+ return { items: result?.items ?? [] };
808
+ }
809
+ async getFunction(appId, functionId) {
810
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}`);
811
+ }
812
+ async createFunction(appId, payload) {
813
+ return this.post(`/admin/api/apps/${appId}/functions`, payload);
814
+ }
815
+ async updateFunction(appId, functionId, payload) {
816
+ return this.patch(`/admin/api/apps/${appId}/functions/${functionId}`, payload);
817
+ }
818
+ /**
819
+ * Archive a function, or destroy it with `hard`. Archiving KEEPS the key
820
+ * reserved in the app's single key namespace; the hard delete is what
821
+ * releases it, destroys every config version and removes the R2 objects.
822
+ */
823
+ async deleteFunction(appId, functionId, options) {
824
+ const query = options?.hard ? "?hard=true" : "";
825
+ return this.delete(`/admin/api/apps/${appId}/functions/${functionId}${query}`);
826
+ }
827
+ /** #2803 — the operational verb pair for a server function. */
828
+ async setFunctionAvailability(appId, functionId, action) {
829
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/${action}`, {});
830
+ }
831
+ /** Every config version of a function, oldest first. */
832
+ /**
833
+ * A function's runs, newest first (#3181).
834
+ *
835
+ * A function run has no `workflowId`, so no workflow runs listing can see
836
+ * it — this is the only surface that lists one. Cursor-paginated like the
837
+ * other admin listings.
838
+ */
839
+ async listFunctionRuns(appId, functionId, params) {
840
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs`, params);
841
+ return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
842
+ }
843
+ /**
844
+ * A function run's step trace — #3348.
845
+ *
846
+ * The function-scoped twin of `getWorkflowStepRuns`, and it answers with the
847
+ * same rows in the same shape: `workflows/:idOrKey/runs/...` resolves its
848
+ * first argument to a workflow DEFINITION, which a function id and a function
849
+ * key alike miss, so a durable run's steps needed a route that addresses the
850
+ * run by the function that owns it.
851
+ */
852
+ async getFunctionStepRuns(appId, functionId, runId) {
853
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs/${runId}/steps`);
854
+ return { items: result?.items ?? [] };
855
+ }
856
+ /**
857
+ * End a function run that will not settle — #3348.
858
+ *
859
+ * `terminated: false` means the run had already settled and the route
860
+ * reported its outcome instead of overwriting it.
861
+ */
862
+ async terminateFunctionRun(appId, functionId, runId) {
863
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/runs/${runId}/terminate`, {});
864
+ }
865
+ /**
866
+ * A function's invocation records, newest first — #3287.
867
+ *
868
+ * The ADMIN route, beside the runs listing above and for the same reason:
869
+ * the CLI acts as an operator, not as a member of the app. The app-API twin
870
+ * (`functions.logs`) is what a FUNCTION reads through the SDK profile.
871
+ */
872
+ async listFunctionLogs(appId, functionId, params) {
873
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/logs`, params);
874
+ return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
875
+ }
876
+ /**
877
+ * One invocation record by its id — #3448, behind `functions logs
878
+ * --invocation <id>`.
879
+ *
880
+ * The id the invoke envelope answered with. A record that never existed,
881
+ * one of ANOTHER function, and one the seven-day TTL has expired all answer
882
+ * the same 404, which is what the listing's own filter promises.
883
+ */
884
+ async getFunctionLog(appId, functionId, invocationId) {
885
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/logs/${invocationId}`);
886
+ }
887
+ /**
888
+ * One function run, RECONCILED against the engine — #3448, what
889
+ * `functions runs wait` polls.
890
+ *
891
+ * NOT the runs listing: that reports the STORED status, and a durable run's
892
+ * row stays `running` until something asks the engine, so a wait built on it
893
+ * would never settle.
894
+ */
895
+ async getFunctionRun(appId, functionId, runId) {
896
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs/${runId}`);
897
+ }
898
+ /**
899
+ * The operator's own APP user — #3448 D3448-009.
900
+ *
901
+ * `getMe()` reads the ADMIN profile (`/admin/api/me`); this is the
902
+ * app-scoped route, which answers the app user `requireAppPermission`
903
+ * provisions for an admin and the `appRole` it gave them. That is the
904
+ * identity a `functions invoke` runs under by default, and printing it is
905
+ * how an operator knows their call bypassed the function's `access` gate.
906
+ */
907
+ async getAppProfile(appId) {
908
+ return this.get(`/app/${appId}/api/me`);
909
+ }
910
+ /**
911
+ * Invoke a function and hand back the ANSWER, whatever its HTTP status —
912
+ * #3448 D3448-010.
913
+ *
914
+ * `request` throws an `ApiError` on any non-2xx, which is right for every
915
+ * other verb and wrong for this one: a 429 carries a `Retry-After` an
916
+ * operator needs to see, and a 409 mode mismatch carries the version's real
917
+ * mode. Rendering those from an exception means re-deriving what the server
918
+ * already said. So this returns the envelope with its status and its retry
919
+ * hint, and throws only when the request did not complete at all.
920
+ *
921
+ * `bearer` is the `--user` path's minted token; absent, the CLI's own
922
+ * credentials. `mode` is ALWAYS sent (D3448-012): it is what makes the
923
+ * server, not the CLI, the authority on a wrong verb.
924
+ */
925
+ async invokeFunction(appId, functionKey, body, options) {
926
+ const credentials = await this.ensureAuthenticated();
927
+ const base = `/app/${appId}/api/functions/${encodeURIComponent(functionKey)}`;
928
+ const path = options?.asSystem
929
+ ? `${base}/invoke-as-system`
930
+ : options?.runtime === "task"
931
+ ? `${base}/start`
932
+ : base;
933
+ const headers = {
934
+ Authorization: `Bearer ${options?.bearer ?? credentials.accessToken}`,
935
+ "Content-Type": "application/json",
936
+ };
937
+ // The CLI's own credentials reach an app-scoped route through the
938
+ // global-admin header; a minted user token does not need it and must not
939
+ // carry it.
940
+ if (!options?.bearer && credentials.globalAdminAppId) {
941
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
942
+ }
943
+ const response = await fetchWithTLS(`${credentials.serverUrl}${path}`, {
944
+ method: "POST",
945
+ headers,
946
+ body: JSON.stringify(body),
947
+ });
948
+ const text = await response.text();
949
+ let parsed = null;
950
+ try {
951
+ parsed = text ? JSON.parse(text) : null;
952
+ }
953
+ catch {
954
+ // A body that is not JSON is still an answer; keep it readable.
955
+ parsed = { error: text };
956
+ }
957
+ const retryAfter = response.headers.get("retry-after");
958
+ const retryAfterSeconds = retryAfter && Number.isFinite(Number(retryAfter)) ? Number(retryAfter) : null;
959
+ return {
960
+ httpStatus: response.status,
961
+ retryAfterSeconds: retryAfterSeconds ??
962
+ (typeof parsed?.retryAfter === "number" ? parsed.retryAfter : null),
963
+ body: parsed,
964
+ };
965
+ }
966
+ /**
967
+ * A function's versions, newest first (#3462).
968
+ *
969
+ * Paging is opt-in on the server: with no `limit` and no `cursor` every
970
+ * version comes back, which is what this method did before paging existed and
971
+ * what an already-installed CLI still asks for. A caller that passes either
972
+ * gets the shared `{ items, hasMore, nextCursor }` page.
973
+ */
974
+ async listFunctionConfigs(appId, functionId, options = {}) {
975
+ const query = new URLSearchParams();
976
+ if (options.limit !== undefined)
977
+ query.set("limit", String(options.limit));
978
+ if (options.cursor)
979
+ query.set("cursor", options.cursor);
980
+ const suffix = query.toString() ? `?${query.toString()}` : "";
981
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs${suffix}`);
982
+ return {
983
+ items: result?.items ?? [],
984
+ hasMore: result?.hasMore === true,
985
+ nextCursor: result?.nextCursor ?? null,
986
+ };
987
+ }
988
+ /**
989
+ * Point the function at one of its OWN existing versions (#3462). Creates no
990
+ * version; the activated version's capabilities, limits and triggers apply
991
+ * from the next call.
992
+ */
993
+ async activateFunctionConfig(appId, functionId, configId) {
994
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}/activate`, {});
995
+ }
996
+ async getFunctionConfig(appId, functionId, configId) {
997
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}`);
998
+ }
999
+ /**
1000
+ * The stored envelope: the authored TOML bytes, every source file and the
1001
+ * built bundle, all base64. This is what `config pull` writes back verbatim.
1002
+ */
1003
+ async getFunctionConfigEnvelope(appId, functionId, configId) {
1004
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}/envelope`);
1005
+ }
1006
+ /** Push a config version. The server verifies `contentHash` itself. */
1007
+ async pushFunctionConfig(appId, functionId, payload) {
1008
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/configs`, payload);
1009
+ }
1010
+ /** #2803 — the operational verb pair for a prompt (admin API). */
1011
+ async setPromptAvailability(appId, promptId, action) {
1012
+ return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
1013
+ }
1014
+ /** #2803 — the operational verb pair for an integration (admin API). */
1015
+ async setIntegrationAvailability(appId, integrationId, action) {
1016
+ return this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/${action}`, {});
1017
+ }
1018
+ async rotateWebhookSecret(appId, webhookId, payload) {
1019
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
1020
+ }
1021
+ async listWebhookEvents(appId, webhookId, params) {
1022
+ const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
1023
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1024
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1025
+ return {
1026
+ items: result?.items ?? [],
1027
+ hasMore: result?.hasMore ?? !!nextCursor,
1028
+ nextCursor,
1029
+ cursor: nextCursor,
1030
+ };
1031
+ }
1032
+ // ============================================
1033
+ // CONNECTIONS INSPECTION (#1968)
1034
+ // ============================================
1035
+ /**
1036
+ * List the live connections the server believes exist for exactly one of a
1037
+ * document, user, or database. Read-only inspection over admin-api. Rows are
1038
+ * per-connection-×-subscription bindings, so `connectionId` is not unique
1039
+ * across rows. `params` on a database-subscription row is caller-defined
1040
+ * bound CEL input — typed `unknown`.
1041
+ */
1042
+ async listConnections(appId, selector, params) {
1043
+ const result = await this.get(`/admin/api/apps/${appId}/connections`, {
1044
+ ...selector,
1045
+ ...params,
1046
+ });
1047
+ return {
1048
+ items: result?.items ?? [],
1049
+ hasMore: result?.hasMore,
1050
+ nextCursor: result?.nextCursor ?? null,
1051
+ };
1052
+ }
1053
+ async listSessions(appId, userId, params) {
1054
+ const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
1055
+ userId,
1056
+ ...params,
1057
+ });
1058
+ return {
1059
+ items: result?.items ?? [],
1060
+ hasMore: result?.hasMore,
1061
+ nextCursor: result?.nextCursor ?? null,
1062
+ };
1063
+ }
1064
+ /**
1065
+ * One page of admin sessions (#3885): the caller's own, or — for a
1066
+ * super-admin — the admin `adminId` names.
1067
+ */
1068
+ async listAdminSessions(params) {
1069
+ const result = await this.get("/admin/api/auth/sessions", params);
1070
+ return {
1071
+ items: result?.items ?? [],
1072
+ hasMore: result?.hasMore,
1073
+ nextCursor: result?.nextCursor ?? null,
1074
+ };
1075
+ }
1076
+ /** Revoke an admin session (idempotent). */
1077
+ async revokeAdminSession(sessionId) {
1078
+ return this.delete(`/admin/api/auth/sessions/${encodeURIComponent(sessionId)}`);
1079
+ }
1080
+ async testWebhook(appId, webhookId, payload) {
1081
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
1082
+ }
1083
+ /**
1084
+ * Run a webhook's configured verifier over headers and a body captured from
1085
+ * a real delivery, and report whether that signature checks out (#2445).
1086
+ *
1087
+ * `headers` is sent as ordered `[name, value]` pairs so a capture carrying a
1088
+ * field name on more than one line survives the round trip. Nothing is
1089
+ * delivered: no delivery record, no workflow run.
1090
+ */
1091
+ async verifyWebhook(appId, webhookId, payload) {
1092
+ return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/verify`, payload);
1093
+ }
1094
+ // ============================================
1095
+ // NAMED LOCKS (#1518)
1096
+ // ============================================
1097
+ async listLocks(appId) {
1098
+ const result = await this.get(`/app/${appId}/api/locks`);
1099
+ return { locks: result?.locks ?? [] };
1100
+ }
1101
+ async getLockStatus(appId, key) {
1102
+ return this.post(`/app/${appId}/api/locks/status`, { key });
1103
+ }
1104
+ /**
1105
+ * #3562 — `owner` is sent only when one was given, so a call that names none
1106
+ * composes byte for byte the body it always did.
1107
+ */
1108
+ async acquireLock(appId, key, ttlMs, owner) {
1109
+ return this.post(`/app/${appId}/api/locks/acquire`, {
1110
+ key,
1111
+ ttlMs,
1112
+ ...(owner !== undefined ? { owner } : {}),
1113
+ });
1114
+ }
1115
+ async releaseLock(appId, key, handleId) {
1116
+ return this.post(`/app/${appId}/api/locks/release`, {
1117
+ key,
1118
+ handle: { handleId },
1119
+ });
1120
+ }
1121
+ // ============================================
1122
+ // CRON TRIGGERS
1123
+ // ============================================
1124
+ async listCronTriggers(appId) {
1125
+ const result = await this.get(`/app/${appId}/api/cron-triggers`);
1126
+ return { items: result?.items ?? [] };
1127
+ }
1128
+ async getCronTrigger(appId, triggerId) {
1129
+ return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
1130
+ }
1131
+ async createCronTrigger(appId, payload) {
1132
+ return this.post(`/app/${appId}/api/cron-triggers`, payload);
1133
+ }
1134
+ async updateCronTrigger(appId, triggerId, payload) {
1135
+ return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
1136
+ }
1137
+ /**
1138
+ * Retire a cron trigger. The default soft-deletes (writes the `archived`
1139
+ * tombstone); `{ hard: true }` destroys the row, which is what
1140
+ * `config push --prune` passes (#2803) so a pruned trigger stops consuming
1141
+ * the per-app cap and holding its key.
1142
+ */
1143
+ async deleteCronTrigger(appId, triggerId, options = {}) {
1144
+ const query = options.hard ? "?hard=true" : "";
1145
+ return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}${query}`);
1146
+ }
1147
+ async disableCronTrigger(appId, triggerId) {
1148
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
1149
+ }
1150
+ async enableCronTrigger(appId, triggerId) {
1151
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
1152
+ }
1153
+ async testCronTrigger(appId, triggerId) {
1154
+ return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
1155
+ }
1156
+ // ============================================
1157
+ // ITERATIONS (iterate-users introspection / reset — #1209)
1158
+ // ============================================
1159
+ async listIterations(appId) {
1160
+ const result = await this.get(`/app/${appId}/api/iterations`);
1161
+ return { items: result?.items ?? [] };
1162
+ }
1163
+ async getIteration(appId, iterationName) {
1164
+ return this.get(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}`);
1165
+ }
1166
+ async resetIteration(appId, iterationName) {
1167
+ return this.post(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}/reset`, {});
1168
+ }
1169
+ // ============================================
1170
+ // RESOURCE METADATA (values — issue #1352)
1171
+ // ============================================
1172
+ async readResourceMetadata(appId, resourceType, resourceId, category) {
1173
+ return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
1174
+ }
1175
+ async writeResourceMetadata(appId, resourceType, resourceId, category, data) {
1176
+ return this.put(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`, { data });
1177
+ }
1178
+ async batchReadResourceMetadata(appId, requests) {
1179
+ return this.post(`/app/${appId}/api/resources/metadata/batch`, {
1180
+ requests,
1181
+ });
270
1182
  }
271
- async archiveIntegrationSecret(appId, integrationId, secretId) {
272
- return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets/${secretId}`, {
273
- status: "inactive",
1183
+ /**
1184
+ * List every stored metadata category on one resource (issue #1402 — debug
1185
+ * tooling). The CLI authenticates as a console admin, so the app-level
1186
+ * owner/admin bypass returns every category regardless of its readRule.
1187
+ */
1188
+ async listResourceMetadata(appId, resourceType, resourceId) {
1189
+ return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata`);
1190
+ }
1191
+ /**
1192
+ * Delete one resource's metadata for one category (issue #1402 — debug
1193
+ * tooling). Idempotent: deleting an absent item succeeds with
1194
+ * `deleted: false` rather than a 404.
1195
+ */
1196
+ async deleteResourceMetadata(appId, resourceType, resourceId, category) {
1197
+ return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
1198
+ }
1199
+ /**
1200
+ * Reverse-resolve a resource by a category's unique metadata value (issue
1201
+ * #2137). A miss is a success with `resourceId: null`, never an error. The
1202
+ * CLI authenticates as a console admin, so the app-level owner/admin bypass
1203
+ * skips readRule evaluation.
1204
+ */
1205
+ async resolveResourceMetadata(appId, resourceType, category, key, value) {
1206
+ return this.post(`/app/${appId}/api/metadata/resolve`, {
1207
+ resourceType,
1208
+ category,
1209
+ key,
1210
+ value,
274
1211
  });
275
1212
  }
276
1213
  // ============================================
@@ -280,6 +1217,7 @@ export class ApiClient {
280
1217
  const result = await this.get(`/admin/api/apps/${appId}/prompts`, params);
281
1218
  return {
282
1219
  items: result?.items ?? [],
1220
+ hasMore: result?.hasMore,
283
1221
  nextCursor: result?.nextCursor ?? null,
284
1222
  };
285
1223
  }
@@ -293,6 +1231,12 @@ export class ApiClient {
293
1231
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
294
1232
  return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
295
1233
  }
1234
+ /**
1235
+ * Retire a prompt, or destroy it.
1236
+ *
1237
+ * `hard` was always sent by both first-party clients and always ignored by
1238
+ * the server; #2887 made it mean something — the plain call now archives.
1239
+ */
296
1240
  async deletePrompt(appId, promptId, hard) {
297
1241
  const path = hard
298
1242
  ? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
@@ -325,36 +1269,136 @@ export class ApiClient {
325
1269
  async duplicatePromptConfig(appId, promptId, configId, payload) {
326
1270
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/configs/${configId}/duplicate`, payload || {});
327
1271
  }
1272
+ // ============================================
1273
+ // SCRIPTS (Rhai transforms) — #1000 (prompt-model convergence)
1274
+ // ============================================
1275
+ //
1276
+ // The `Script` model is the authoring surface for `script` workflow
1277
+ // steps. A `Script` is a HEADER (name/description/activeConfigId); the
1278
+ // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
1279
+ // time. CLI `config push` reads `transforms/*.rhai` files and reconciles
1280
+ // them: a new file → create script (mints a default config); a changed
1281
+ // file → create a new config + activate it (zero fan-out — referencing
1282
+ // workflows pick up the new body on their next run).
1283
+ async listScripts(appId) {
1284
+ const result = await this.get(`/admin/api/apps/${appId}/scripts`);
1285
+ return { items: result?.items ?? [] };
1286
+ }
1287
+ async getScript(appId, scriptId) {
1288
+ return this.get(`/admin/api/apps/${appId}/scripts/${scriptId}`);
1289
+ }
1290
+ /**
1291
+ * List a script's versioned configs (the `ScriptConfig` rows). Each config
1292
+ * is a distinct block version: `configId` is the stable version selector,
1293
+ * `contentHash` is its content identity, and `status` reports whether it is
1294
+ * `active`/`draft`/`archived`. The script header's `activeConfigId` names
1295
+ * the live version. Used by `scripts configs list` and by the script test
1296
+ * commands to run against a specific pinned version.
1297
+ */
1298
+ async listScriptConfigs(appId, scriptId) {
1299
+ const result = await this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`);
1300
+ return { items: result?.items ?? [] };
1301
+ }
1302
+ async createScript(appId, payload) {
1303
+ // Mints the script header + a default `active` config carrying the body.
1304
+ return this.post(`/admin/api/apps/${appId}/scripts`, payload);
1305
+ }
1306
+ /**
1307
+ * Push a new body for an existing script by creating a new `ScriptConfig`
1308
+ * and activating it (the prompt-model "edit → activate" flow). Referencing
1309
+ * workflows pick up the new body on their next run with no fan-out. The
1310
+ * config name is unique per script, so we mint a timestamped name.
1311
+ */
1312
+ async pushScriptBody(appId, scriptId, body,
1313
+ /**
1314
+ * The active state the caller believes holds right now (#2731 B10).
1315
+ * Creating an inactive config races nothing, so the guard sits on the
1316
+ * ACTIVATION step: if the active config has moved since the caller read it,
1317
+ * the server rejects with a 409 instead of overwriting a concurrent script
1318
+ * change. It takes BOTH halves of "what is active", because a config's body
1319
+ * can be edited in place without the active id changing — the id alone
1320
+ * would let such an edit through. Omitted (e.g. `--force`) means activate
1321
+ * unconditionally.
1322
+ */
1323
+ expectedActive) {
1324
+ const configName = `sync-${Date.now()}`;
1325
+ const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
1326
+ const activateBody = {};
1327
+ if (expectedActive?.configId) {
1328
+ activateBody.expectedActiveConfigId = expectedActive.configId;
1329
+ }
1330
+ if (expectedActive?.modifiedAt) {
1331
+ activateBody.expectedActiveModifiedAt = expectedActive.modifiedAt;
1332
+ }
1333
+ await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, activateBody);
1334
+ return config;
1335
+ }
1336
+ /**
1337
+ * Delete a script header (cascades its configs). `force` overrides the
1338
+ * server's guard against deleting a script that an active workflow still
1339
+ * references by name — without it the server answers 409.
1340
+ */
1341
+ async deleteScript(appId, scriptId, force) {
1342
+ const path = force
1343
+ ? `/admin/api/apps/${appId}/scripts/${scriptId}?force=true`
1344
+ : `/admin/api/apps/${appId}/scripts/${scriptId}`;
1345
+ return this.delete(path);
1346
+ }
328
1347
  async getPromptSchema(appId, promptId) {
329
1348
  return this.get(`/admin/api/apps/${appId}/prompts/${promptId}/schema`);
330
1349
  }
331
1350
  // ============================================
332
1351
  // BLOCK TEST CASES (Prompts, Integrations, Workflows)
333
1352
  // ============================================
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 ?? [] };
1353
+ /**
1354
+ * One page of a block's test cases. The endpoint paginates (default 50, cap
1355
+ * 100) and reports `nextCursor`; callers that reconcile the sync tree must
1356
+ * drain every page before deleting or classifying anything (#2769).
1357
+ */
1358
+ async listTestCases(appId, blockType, blockId, params = {}) {
1359
+ const query = new URLSearchParams();
1360
+ if (params.limit !== undefined)
1361
+ query.set("limit", String(params.limit));
1362
+ if (params.cursor)
1363
+ query.set("cursor", params.cursor);
1364
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1365
+ const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases${suffix}`);
1366
+ return {
1367
+ items: result?.items ?? result ?? [],
1368
+ hasMore: result?.hasMore,
1369
+ nextCursor: result?.nextCursor ?? undefined,
1370
+ };
337
1371
  }
338
1372
  async getTestCase(appId, blockType, blockId, testCaseId) {
339
1373
  return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
340
1374
  }
341
1375
  async createTestCase(appId, blockType, blockId, payload) {
342
- // Server expects JSON strings for these fields
1376
+ // #2769 — the JSON fields travel PARSED. The handler stringifies them for
1377
+ // storage (`createBlockTestCase`) and `serializeBlockTestCase` parses them
1378
+ // on the way back out, so a client that stringifies first stores a JSON
1379
+ // string of a JSON string and reads back a quoted blob.
343
1380
  const serverPayload = {
344
1381
  name: payload.name,
345
- inputVariables: JSON.stringify(payload.inputVariables),
1382
+ inputVariables: payload.inputVariables,
346
1383
  configId: payload.configId,
347
1384
  evaluatorPromptId: payload.evaluatorPromptId,
348
1385
  evaluatorConfigId: payload.evaluatorConfigId,
349
1386
  };
1387
+ // #2896 — only when the caller has one: an older server rejects the key
1388
+ // outright, and push reads that 400 to learn what it is talking to.
1389
+ if (payload.key !== undefined)
1390
+ serverPayload.key = payload.key;
1391
+ if (payload.description !== undefined) {
1392
+ serverPayload.description = payload.description;
1393
+ }
350
1394
  if (payload.expectedOutputPattern) {
351
1395
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
352
1396
  }
353
1397
  if (payload.expectedOutputContains) {
354
- serverPayload.expectedOutputContains = JSON.stringify(payload.expectedOutputContains);
1398
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
355
1399
  }
356
1400
  if (payload.expectedJsonSubset) {
357
- serverPayload.expectedJsonSubset = JSON.stringify(payload.expectedJsonSubset);
1401
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
358
1402
  }
359
1403
  return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
360
1404
  }
@@ -362,21 +1406,23 @@ export class ApiClient {
362
1406
  const serverPayload = {};
363
1407
  if (payload.name !== undefined)
364
1408
  serverPayload.name = payload.name;
1409
+ if (payload.description !== undefined) {
1410
+ serverPayload.description = payload.description;
1411
+ }
1412
+ // #2769 — parsed on the wire (see `createTestCase`), and an explicit `null`
1413
+ // is how the authored file clears a field: the handler only touches keys
1414
+ // that are present, so an omitted key retains the previous value.
365
1415
  if (payload.inputVariables !== undefined) {
366
- serverPayload.inputVariables = JSON.stringify(payload.inputVariables);
1416
+ serverPayload.inputVariables = payload.inputVariables;
367
1417
  }
368
1418
  if (payload.expectedOutputPattern !== undefined) {
369
1419
  serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
370
1420
  }
371
1421
  if (payload.expectedOutputContains !== undefined) {
372
- serverPayload.expectedOutputContains = payload.expectedOutputContains
373
- ? JSON.stringify(payload.expectedOutputContains)
374
- : null;
1422
+ serverPayload.expectedOutputContains = payload.expectedOutputContains;
375
1423
  }
376
1424
  if (payload.expectedJsonSubset !== undefined) {
377
- serverPayload.expectedJsonSubset = payload.expectedJsonSubset
378
- ? JSON.stringify(payload.expectedJsonSubset)
379
- : null;
1425
+ serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
380
1426
  }
381
1427
  if (payload.configId !== undefined)
382
1428
  serverPayload.configId = payload.configId;
@@ -386,6 +1432,8 @@ export class ApiClient {
386
1432
  if (payload.evaluatorConfigId !== undefined) {
387
1433
  serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
388
1434
  }
1435
+ if (payload.key !== undefined)
1436
+ serverPayload.key = payload.key;
389
1437
  return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
390
1438
  }
391
1439
  async deleteTestCase(appId, blockType, blockId, testCaseId) {
@@ -415,14 +1463,8 @@ export class ApiClient {
415
1463
  });
416
1464
  const text = await response.text();
417
1465
  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);
1466
+ const parsed = parseErrorResponse(response, text, url);
1467
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
426
1468
  }
427
1469
  return text ? JSON.parse(text) : null;
428
1470
  }
@@ -442,14 +1484,8 @@ export class ApiClient {
442
1484
  });
443
1485
  if (!response.ok) {
444
1486
  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);
1487
+ const parsed = parseErrorResponse(response, text, url);
1488
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
453
1489
  }
454
1490
  const arrayBuffer = await response.arrayBuffer();
455
1491
  return {
@@ -481,12 +1517,23 @@ export class ApiClient {
481
1517
  const result = await this.get(`/admin/api/apps/${appId}/workflows`, params);
482
1518
  return {
483
1519
  items: result?.items ?? [],
1520
+ hasMore: result?.hasMore,
484
1521
  nextCursor: result?.nextCursor ?? null,
485
1522
  };
486
1523
  }
487
1524
  async getWorkflow(appId, workflowId) {
488
1525
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}`);
489
1526
  }
1527
+ /**
1528
+ * One app's workflow step usage report (#3132).
1529
+ *
1530
+ * `windowDays` is omitted from the query when the caller did not ask for a
1531
+ * window, so the server applies its own default rather than the CLI
1532
+ * restating it on the wire.
1533
+ */
1534
+ async getWorkflowUsage(appId, params) {
1535
+ return this.get(`/admin/api/apps/${appId}/workflows/usage`, params);
1536
+ }
490
1537
  async createWorkflow(appId, payload) {
491
1538
  return this.post(`/admin/api/apps/${appId}/workflows`, payload);
492
1539
  }
@@ -494,15 +1541,33 @@ export class ApiClient {
494
1541
  const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
495
1542
  return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
496
1543
  }
497
- async deleteWorkflow(appId, workflowId) {
498
- return this.delete(`/admin/api/apps/${appId}/workflows/${workflowId}`);
1544
+ /**
1545
+ * Retire a workflow, or destroy it (#2887).
1546
+ *
1547
+ * The plain call ARCHIVES: the definition row stays, so its runs, revisions
1548
+ * and test cases keep resolving. `{ hard: true }` performs the full cascade
1549
+ * and is what releases the `workflowKey` — the flag `config push --prune`
1550
+ * sends, for the same reason it sends it for webhooks and integrations.
1551
+ */
1552
+ async deleteWorkflow(appId, workflowId, options) {
1553
+ const path = `/admin/api/apps/${appId}/workflows/${workflowId}`;
1554
+ return this.delete(options?.hard ? `${path}?hard=true` : path);
1555
+ }
1556
+ /**
1557
+ * #2645 criterion 9 — the operational toggle. Deliberately NOT the config
1558
+ * PATCH: `config push` owns every configuration field, and taking a workflow
1559
+ * out of service must not touch one, or an operator's action would show up as
1560
+ * drift against the repo. Same route shape as `cron-triggers pause|resume`.
1561
+ */
1562
+ async disableWorkflow(appId, workflowId) {
1563
+ return this.post(`/app/${appId}/api/workflows/${workflowId}/disable`, {});
1564
+ }
1565
+ async enableWorkflow(appId, workflowId) {
1566
+ return this.post(`/app/${appId}/api/workflows/${workflowId}/enable`, {});
499
1567
  }
500
1568
  async updateWorkflowDraft(appId, workflowId, payload) {
501
1569
  return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
502
1570
  }
503
- async publishWorkflow(appId, workflowId) {
504
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
505
- }
506
1571
  async previewWorkflow(appId, workflowId, payload) {
507
1572
  return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
508
1573
  }
@@ -511,14 +1576,43 @@ export class ApiClient {
511
1576
  }
512
1577
  async listWorkflowRuns(appId, workflowId, params) {
513
1578
  const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
1579
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1580
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
514
1581
  return {
515
1582
  items: result?.items ?? [],
516
- cursor: result?.cursor ?? null,
1583
+ hasMore: result?.hasMore,
1584
+ nextCursor,
1585
+ cursor: nextCursor,
1586
+ resumeAfter: result?.resumeAfter,
1587
+ scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
1588
+ };
1589
+ }
1590
+ /**
1591
+ * Workflow runs started by one app user, across every workflow (#1967).
1592
+ * Backed by `GET /admin/api/apps/{appId}/users/{userId}/workflow-runs`,
1593
+ * which resolves the app user before querying the `runsByUser` index.
1594
+ */
1595
+ async listUserWorkflowRuns(appId, userId, params) {
1596
+ const result = await this.get(`/admin/api/apps/${appId}/users/${encodeURIComponent(userId)}/workflow-runs`, params);
1597
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1598
+ return {
1599
+ items: result?.items ?? [],
1600
+ hasMore: result?.hasMore,
1601
+ nextCursor,
1602
+ cursor: nextCursor,
1603
+ scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
517
1604
  };
518
1605
  }
519
1606
  async getWorkflowRunStatus(appId, workflowId, runId) {
520
1607
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
521
1608
  }
1609
+ async getWorkflowStepRuns(appId, workflowId, runId) {
1610
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
1611
+ return { items: result?.items ?? [] };
1612
+ }
1613
+ // Workflow analytics have one client path, `getAnalyticsTopWorkflows`
1614
+ // (issue #2766): `getTopWorkflows` was a second name for the same endpoint,
1615
+ // and `getWorkflowAnalytics` called a route the server never registered.
522
1616
  // ============================================
523
1617
  // WORKFLOW CONFIGURATIONS
524
1618
  // ============================================
@@ -547,29 +1641,83 @@ export class ApiClient {
547
1641
  // ============================================
548
1642
  // ANALYTICS
549
1643
  // ============================================
550
- async getAnalyticsOverview(appId, params) {
551
- return this.get(`/app/${appId}/api/analytics/overview`, params);
552
- }
553
1644
  async getAnalyticsTopUsers(appId, params) {
554
1645
  return this.get(`/app/${appId}/api/analytics/users/top`, params);
555
1646
  }
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
1647
  async getAnalyticsIntegrationMetrics(appId, params) {
563
1648
  return this.get(`/app/${appId}/api/analytics/integrations`, params);
564
1649
  }
1650
+ async getAnalyticsOverviewDau(appId) {
1651
+ return this.get(`/app/${appId}/api/analytics/overview/dau`);
1652
+ }
1653
+ async getAnalyticsOverviewWau(appId) {
1654
+ return this.get(`/app/${appId}/api/analytics/overview/wau`);
1655
+ }
1656
+ async getAnalyticsOverviewMau(appId) {
1657
+ return this.get(`/app/${appId}/api/analytics/overview/mau`);
1658
+ }
1659
+ async getAnalyticsOverviewGrowth(appId, params) {
1660
+ return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
1661
+ }
1662
+ async getAnalyticsDailyActive(appId, params) {
1663
+ return this.get(`/app/${appId}/api/analytics/daily-active`, params);
1664
+ }
1665
+ async getAnalyticsRollingActive(appId, params) {
1666
+ return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
1667
+ }
1668
+ async getAnalyticsCohortRetention(appId) {
1669
+ return this.get(`/app/${appId}/api/analytics/cohort-retention`);
1670
+ }
1671
+ async getAnalyticsUserSearch(appId, params) {
1672
+ return this.get(`/app/${appId}/api/analytics/users/search`, params);
1673
+ }
1674
+ async getAnalyticsUserDetail(appId, userUlid) {
1675
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
1676
+ }
1677
+ async getAnalyticsUserSnapshot(appId, userUlid) {
1678
+ return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
1679
+ }
1680
+ async getAnalyticsEvents(appId, params) {
1681
+ return this.get(`/app/${appId}/api/analytics/events`, params);
1682
+ }
1683
+ async getAnalyticsEventsGrouped(appId, params) {
1684
+ return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
1685
+ }
1686
+ async getAnalyticsErrorsGroups(appId, params) {
1687
+ return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
1688
+ }
1689
+ async getAnalyticsTopWorkflows(appId, params) {
1690
+ return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1691
+ }
1692
+ async getAnalyticsTopPrompts(appId, params) {
1693
+ return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
1694
+ }
565
1695
  // ============================================
566
1696
  // SUPER ADMIN: ADMINS
567
1697
  // ============================================
1698
+ /**
1699
+ * The signed-in admin's own profile, `role` included.
1700
+ *
1701
+ * The role is read from the server rather than from the cached credentials
1702
+ * file: a role changed after login would otherwise let a stale local record
1703
+ * decide an authorization question.
1704
+ */
1705
+ async getMe() {
1706
+ return this.get("/admin/api/me");
1707
+ }
568
1708
  async listAllAdmins(params) {
569
1709
  const result = await this.get("/admin/api/admins", params);
1710
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1711
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1712
+ const items = result?.items ?? result?.admins ?? [];
570
1713
  return {
571
- admins: result?.admins ?? [],
572
- cursor: result?.cursor ?? null,
1714
+ // `items` is the key every list envelope is built from (#3646, #1982);
1715
+ // `admins` is the deprecated legacy name, read only as a fallback.
1716
+ items,
1717
+ admins: items,
1718
+ hasMore: result?.hasMore,
1719
+ nextCursor,
1720
+ cursor: nextCursor,
573
1721
  };
574
1722
  }
575
1723
  async searchAdminByEmail(email) {
@@ -602,6 +1750,7 @@ export class ApiClient {
602
1750
  const result = await this.get("/admin/api/catalog/prompts", params);
603
1751
  return {
604
1752
  items: result?.items ?? [],
1753
+ hasMore: result?.hasMore,
605
1754
  nextCursor: result?.nextCursor ?? null,
606
1755
  };
607
1756
  }
@@ -621,6 +1770,7 @@ export class ApiClient {
621
1770
  const result = await this.get("/admin/api/catalog/integrations", params);
622
1771
  return {
623
1772
  items: result?.items ?? [],
1773
+ hasMore: result?.hasMore,
624
1774
  nextCursor: result?.nextCursor ?? null,
625
1775
  };
626
1776
  }
@@ -637,21 +1787,6 @@ export class ApiClient {
637
1787
  return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
638
1788
  }
639
1789
  // ============================================
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
1790
  // BATCH TEST EXECUTION
656
1791
  // ============================================
657
1792
  async startBatchTests(appId, blockType, blockId, payload) {
@@ -670,6 +1805,33 @@ export class ApiClient {
670
1805
  return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
671
1806
  }
672
1807
  // ============================================
1808
+ // EMAIL TEMPLATES
1809
+ // ============================================
1810
+ async listEmailTemplates(appId) {
1811
+ return this.get(`/admin/api/apps/${appId}/email-templates`);
1812
+ }
1813
+ async getEmailTemplate(appId, emailType) {
1814
+ return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1815
+ }
1816
+ /**
1817
+ * Create or replace an email-template override.
1818
+ *
1819
+ * `expectedModifiedAt` is the optimistic-concurrency token every sibling
1820
+ * config endpoint already takes (#2731 B10). Optional and additive: when
1821
+ * omitted the write is unconditional, which is how `--force` and every
1822
+ * pre-existing caller keep working; when supplied and stale, the server
1823
+ * answers 409 rather than silently overwriting a concurrent edit.
1824
+ */
1825
+ async upsertEmailTemplate(appId, emailType, payload, expectedModifiedAt) {
1826
+ return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload);
1827
+ }
1828
+ async deleteEmailTemplate(appId, emailType) {
1829
+ return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
1830
+ }
1831
+ async testEmailTemplate(appId, emailType, payload) {
1832
+ return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
1833
+ }
1834
+ // ============================================
673
1835
  // ACCESS TOKENS
674
1836
  // ============================================
675
1837
  async createToken(appId, data) {
@@ -688,8 +1850,54 @@ export class ApiClient {
688
1850
  // ============================================
689
1851
  // DATABASES
690
1852
  // ============================================
691
- async listDatabases(appId) {
692
- return this.get(`/app/${appId}/api/databases`);
1853
+ /**
1854
+ * List an app's databases, following the cursor to the end.
1855
+ *
1856
+ * The endpoint returns the `{ items, hasMore, nextCursor }` envelope and
1857
+ * caps a page at 100 (#1958), so `databases list` needs the follow-up pages
1858
+ * to show every database. `paginateAll` carries the repeat-cursor and
1859
+ * max-page guards, so a server that hands out a non-advancing cursor fails
1860
+ * loudly instead of looping forever.
1861
+ *
1862
+ * `options.owner` narrows the result to the databases that user created; the
1863
+ * server resolves it through the `databasesByCreator` index (#1965).
1864
+ */
1865
+ /**
1866
+ * One page of an app's databases (#3646).
1867
+ *
1868
+ * `databases list` used to walk the chain here. The route pages, so the page
1869
+ * boundary belongs to the caller: the verb declares `--limit`/`--cursor` and
1870
+ * prints what came back.
1871
+ */
1872
+ async listDatabases(appId, options) {
1873
+ const params = new URLSearchParams();
1874
+ if (options?.owner)
1875
+ params.set("owner", options.owner);
1876
+ if (options?.limit !== undefined)
1877
+ params.set("limit", String(options.limit));
1878
+ if (options?.cursor)
1879
+ params.set("cursor", options.cursor);
1880
+ const qs = params.toString() ? `?${params.toString()}` : "";
1881
+ const resp = await this.get(`/app/${appId}/api/databases${qs}`);
1882
+ // Tolerant read: a server older than #1958 answers with a bare array.
1883
+ // Without this a published CLI pointed at a not-yet-upgraded server would
1884
+ // print nothing at all. Such a server also predates the `owner` filter
1885
+ // (#1965) and ignores the unknown query parameter, so apply the filter
1886
+ // here — the legacy rows carry `createdBy`, so this reproduces the
1887
+ // server's own post-join filter (#2245). It has no pages either.
1888
+ if (Array.isArray(resp)) {
1889
+ return {
1890
+ items: options?.owner
1891
+ ? resp.filter((db) => db?.createdBy === options.owner)
1892
+ : resp,
1893
+ hasMore: false,
1894
+ };
1895
+ }
1896
+ return {
1897
+ items: resp?.items ?? [],
1898
+ hasMore: resp?.hasMore,
1899
+ nextCursor: resp?.nextCursor ?? null,
1900
+ };
693
1901
  }
694
1902
  async createDatabase(appId, data) {
695
1903
  return this.post(`/app/${appId}/api/databases`, data);
@@ -703,29 +1911,65 @@ export class ApiClient {
703
1911
  async deleteDatabase(appId, databaseId) {
704
1912
  return this.delete(`/app/${appId}/api/databases/${databaseId}`);
705
1913
  }
1914
+ /**
1915
+ * Read a database's CEL context dict.
1916
+ *
1917
+ * The HTTP path stays `/metadata` because the wire field name is still
1918
+ * `metadata`; only the client/CLI-facing helper names were reframed.
1919
+ *
1920
+ * @deprecated Use resource metadata categories instead (`primitive metadata
1921
+ * get database <id> <category>`, #1420, #1815).
1922
+ */
1923
+ async getDatabaseCelContext(appId, databaseId) {
1924
+ return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1925
+ }
1926
+ /**
1927
+ * Update a database's CEL context dict (merge with existing).
1928
+ *
1929
+ * @deprecated Use resource metadata categories instead (`primitive metadata
1930
+ * set database <id> <category>`, #1420, #1815).
1931
+ */
1932
+ async updateDatabaseCelContext(appId, databaseId, celContext) {
1933
+ return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1934
+ }
1935
+ /**
1936
+ * @deprecated Database CEL context is deprecated; use resource metadata
1937
+ * categories instead (`primitive metadata get database <id> <category>`,
1938
+ * #1420, #1815).
1939
+ */
1940
+ async getDatabaseMetadata(appId, databaseId) {
1941
+ return this.getDatabaseCelContext(appId, databaseId);
1942
+ }
1943
+ /**
1944
+ * @deprecated Database CEL context is deprecated; use resource metadata
1945
+ * categories instead (`primitive metadata set database <id> <category>`,
1946
+ * #1420, #1815).
1947
+ */
1948
+ async updateDatabaseMetadata(appId, databaseId, metadata) {
1949
+ return this.updateDatabaseCelContext(appId, databaseId, metadata);
1950
+ }
706
1951
  // ============================================
707
1952
  // DATABASE PERMISSIONS
708
1953
  // ============================================
709
1954
  async listDatabasePermissions(appId, databaseId) {
710
1955
  return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
711
1956
  }
712
- async grantDatabasePermission(appId, databaseId, data) {
713
- return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, data);
1957
+ async addDatabaseManager(appId, databaseId, userId) {
1958
+ return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
1959
+ userId,
1960
+ permission: "manager",
1961
+ });
714
1962
  }
715
- async revokeDatabasePermission(appId, databaseId, userId) {
1963
+ async removeDatabaseManager(appId, databaseId, userId) {
716
1964
  return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
717
1965
  }
718
- // ============================================
719
- // DATABASE GROUP PERMISSIONS
720
- // ============================================
721
- async listDatabaseGroupPermissions(appId, databaseId) {
722
- return this.get(`/app/${appId}/api/databases/${databaseId}/group-permissions`);
723
- }
724
- async grantDatabaseGroupPermission(appId, databaseId, data) {
725
- return this.post(`/app/${appId}/api/databases/${databaseId}/group-permissions`, data);
1966
+ /** @deprecated Use {@link addDatabaseManager} instead. */
1967
+ async grantDatabasePermission(appId, databaseId, data) {
1968
+ return this.addDatabaseManager(appId, databaseId, data.userId);
726
1969
  }
727
- async revokeDatabaseGroupPermission(appId, databaseId, groupType, groupId) {
728
- return this.delete(`/app/${appId}/api/databases/${databaseId}/group-permissions/${groupType}/${groupId}`);
1970
+ /** @deprecated Use {@link removeDatabaseManager} instead. */
1971
+ async revokeDatabasePermission(appId, databaseId, userId) {
1972
+ return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
729
1973
  }
730
1974
  // ============================================
731
1975
  // DATABASE RECORDS & SCHEMA
@@ -736,17 +1980,140 @@ export class ApiClient {
736
1980
  async describeDatabaseModel(appId, databaseId, modelName) {
737
1981
  return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
738
1982
  }
739
- async queryDatabaseRecords(appId, databaseId, data) {
740
- return this.post(`/app/${appId}/api/databases/${databaseId}/records/query`, data);
741
- }
742
- async saveDatabaseRecord(appId, databaseId, data) {
743
- return this.post(`/app/${appId}/api/databases/${databaseId}/records/save`, data);
1983
+ /**
1984
+ * Query records in a database model. The Durable Object answers with its own
1985
+ * `{ data, hasMore, nextCursor?, prevCursor? }` envelope; that wire shape is
1986
+ * shared with the JS client, the Swift client and web-admin, so the CLI
1987
+ * normalizes it at its own boundary instead (issue #2357) and hands callers
1988
+ * the same `{ items, hasMore, nextCursor? }` envelope as
1989
+ * {@link queryDocumentRecords}.
1990
+ */
1991
+ async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
1992
+ const body = { modelName };
1993
+ if (queryOptions?.filter)
1994
+ body.filter = queryOptions.filter;
1995
+ const options = {};
1996
+ if (queryOptions?.limit)
1997
+ options.limit = queryOptions.limit;
1998
+ if (queryOptions?.cursor)
1999
+ options.uniqueStartKey = queryOptions.cursor;
2000
+ if (Object.keys(options).length > 0)
2001
+ body.options = options;
2002
+ const raw = await this.post(`/app/${appId}/api/databases/${databaseId}/records/query`, body);
2003
+ return normalizeCliListEnvelope(raw);
2004
+ }
2005
+ async countDatabaseRecords(appId, databaseId, modelName, filter) {
2006
+ const body = { modelName };
2007
+ if (filter)
2008
+ body.filter = filter;
2009
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/count`, body);
2010
+ }
2011
+ async aggregateDatabaseRecords(appId, databaseId, modelName, options) {
2012
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/aggregate`, {
2013
+ modelName,
2014
+ options,
2015
+ });
744
2016
  }
745
- async deleteDatabaseRecord(appId, databaseId, data) {
746
- return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, data);
2017
+ /**
2018
+ * Upsert one record via `records/save`: creates the record when the id is
2019
+ * new, and merges the supplied fields into it when it already exists (the DO
2020
+ * writes with `ON CONFLICT … json_patch`, so fields you leave out survive).
2021
+ * `patchDatabaseRecord` is the same merge but requires the record to exist.
2022
+ *
2023
+ * `id` is optional: when omitted the body carries no `id`, which the
2024
+ * DatabaseDO only accepts alongside an `upsertOn` field — callers that want a
2025
+ * plain create must supply an id themselves (`databases records save` mints a
2026
+ * ULID, matching the CSV import path).
2027
+ *
2028
+ * Returns the endpoint's flat `{ success, id, appliedFields }` — not the
2029
+ * saved record.
2030
+ */
2031
+ async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
2032
+ const body = { modelName, data };
2033
+ if (id !== undefined)
2034
+ body.id = id;
2035
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/save`, body);
2036
+ }
2037
+ /**
2038
+ * Merge the supplied fields into an existing record via `records/patch`.
2039
+ *
2040
+ * Fields absent from `data` are left as they are. Unlike `saveDatabaseRecord`
2041
+ * the record must already exist — the DatabaseDO answers 404 for an unknown
2042
+ * id rather than creating one. Returns the flat
2043
+ * `{ success, id, appliedFields }`.
2044
+ */
2045
+ async patchDatabaseRecord(appId, databaseId, modelName, id, data) {
2046
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/patch`, {
2047
+ modelName,
2048
+ id,
2049
+ data,
2050
+ });
747
2051
  }
748
- async countDatabaseRecords(appId, databaseId, data) {
749
- return this.post(`/app/${appId}/api/databases/${databaseId}/records/count`, data);
2052
+ // ============================================
2053
+ // DATABASE OPERATIONS (registered operations)
2054
+ // ============================================
2055
+ async listDatabaseOperations(appId, databaseId) {
2056
+ return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
2057
+ }
2058
+ /**
2059
+ * Execute a registered operation and, when the result is a query page,
2060
+ * normalize it to the CLI's one list envelope (issue #2440).
2061
+ *
2062
+ * The registered operation decides the response shape, so the normalization
2063
+ * is conditional — see {@link normalizeDatabaseOperationResult}. Everything
2064
+ * else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
2065
+ * pipeline) is returned exactly as the server sent it.
2066
+ */
2067
+ async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
2068
+ const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
2069
+ const extraHeaders = {};
2070
+ if (options?.timing) {
2071
+ extraHeaders["X-Timing"] = "true";
2072
+ }
2073
+ if (token) {
2074
+ return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
2075
+ method: "POST",
2076
+ body: JSON.stringify(data || {}),
2077
+ headers: extraHeaders,
2078
+ }));
2079
+ }
2080
+ return normalizeDatabaseOperationResult(await this.request(path, {
2081
+ method: "POST",
2082
+ body: JSON.stringify(data || {}),
2083
+ headers: extraHeaders,
2084
+ }));
2085
+ }
2086
+ /**
2087
+ * Execute a registered batch (bulk) database operation. Posts a chunk of
2088
+ * items to the canonical `operations/:name/batch` endpoint (the same one the
2089
+ * client library's `executeBatch` uses — NOT the deprecated `import-bulk`
2090
+ * alias). Used by `databases import-csv`.
2091
+ *
2092
+ * @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
2093
+ * validation/access failures abort the whole chunk with a 4xx (thrown).
2094
+ */
2095
+ async executeBatch(appId, databaseId, operationName, batch) {
2096
+ return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
2097
+ }
2098
+ /**
2099
+ * Make a request using a specific JWT token instead of the CLI's credentials.
2100
+ * Used for executing operations as a different user.
2101
+ */
2102
+ async requestWithToken(path, token, options = {}) {
2103
+ const credentials = await this.ensureAuthenticated();
2104
+ const url = `${credentials.serverUrl}${path}`;
2105
+ const headers = {
2106
+ Authorization: `Bearer ${token}`,
2107
+ "Content-Type": "application/json",
2108
+ ...(options.headers || {}),
2109
+ };
2110
+ const response = await fetchWithTLS(url, { ...options, headers });
2111
+ const text = await response.text();
2112
+ if (!response.ok) {
2113
+ const parsed = parseErrorResponse(response, text, path);
2114
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
2115
+ }
2116
+ return text ? JSON.parse(text) : null;
750
2117
  }
751
2118
  // ============================================
752
2119
  // DATABASE INDEXES
@@ -760,11 +2127,160 @@ export class ApiClient {
760
2127
  async dropDatabaseIndex(appId, databaseId, data) {
761
2128
  return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
762
2129
  }
2130
+ /**
2131
+ * Issue #974: back-provision an existing database instance's schema-declared
2132
+ * unique indexes by reading its type schema and registering any declared-but-
2133
+ * missing unique index. Idempotent.
2134
+ */
2135
+ async reindexDatabaseFromSchema(appId, databaseId) {
2136
+ return this.post(`/app/${appId}/api/databases/${databaseId}/reindex`, {});
2137
+ }
2138
+ // ============================================
2139
+ // DATABASE TYPE CONFIGS
2140
+ // ============================================
2141
+ async listDatabaseTypeConfigs(appId) {
2142
+ return this.get(`/app/${appId}/api/databases/types`);
2143
+ }
2144
+ async getDatabaseTypeConfig(appId, databaseType) {
2145
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
2146
+ }
2147
+ async createDatabaseTypeConfig(appId, data,
2148
+ // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
2149
+ // validation (inline CEL rule lint, manifest shape, schema TOML parse)
2150
+ // WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
2151
+ // malformed `defaultAccess`/manifest the same way the real POST would.
2152
+ options) {
2153
+ const query = options?.dryRun ? "?dryRun=true" : "";
2154
+ return this.post(`/app/${appId}/api/databases/types${query}`, data);
2155
+ }
2156
+ async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
2157
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2158
+ const qs = new URLSearchParams();
2159
+ if (options?.dryRun)
2160
+ qs.set("dryRun", "true");
2161
+ if (options?.acceptWarnings)
2162
+ qs.set("acceptWarnings", "true");
2163
+ const query = qs.toString();
2164
+ const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
2165
+ return this.patch(path, body);
2166
+ }
2167
+ async deleteDatabaseTypeConfig(appId, databaseType, options) {
2168
+ const query = options?.force ? "?force=true" : "";
2169
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
2170
+ }
2171
+ /**
2172
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
2173
+ * existing ops + DO field introspection. Read-only — does NOT persist.
2174
+ */
2175
+ async scaffoldDatabaseTypeSchema(appId, databaseType) {
2176
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
2177
+ }
2178
+ // ============================================
2179
+ // DATABASE TYPE OPERATIONS
2180
+ // ============================================
2181
+ async listDatabaseTypeOperations(appId, databaseType) {
2182
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
2183
+ }
2184
+ async getDatabaseTypeOperation(appId, databaseType, name) {
2185
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2186
+ }
2187
+ async createDatabaseTypeOperation(appId, databaseType, data,
2188
+ // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
2189
+ // without persisting — used by `config push --dry-run` to surface gate
2190
+ // failures the real push would hit. `schemaOverride` lets the gate run
2191
+ // against the schema the SAME push is about to land (so an op that depends
2192
+ // on a new field isn't falsely rejected against the stale stored schema).
2193
+ //
2194
+ // Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
2195
+ // does not exist on the server yet, so the server's "access required"
2196
+ // check has no `defaultAccess` to fall back on. Thread the TOML-declared
2197
+ // type-level `defaultAccess` so an op that omits `access` (intending to
2198
+ // inherit it after the type is created) isn't falsely rejected during the
2199
+ // dry-run. Server only trusts this on the fresh-type dry-run path.
2200
+ options) {
2201
+ const query = options?.dryRun ? "?dryRun=true" : "";
2202
+ let body = data;
2203
+ if (options?.dryRun && options.schemaOverride !== undefined) {
2204
+ body = { ...body, schemaOverride: options.schemaOverride };
2205
+ }
2206
+ if (options?.dryRun && options.defaultAccess !== undefined) {
2207
+ body = { ...body, defaultAccess: options.defaultAccess };
2208
+ }
2209
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2210
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2211
+ }
2212
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
2213
+ }
2214
+ async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
2215
+ // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
2216
+ // `schemaOverride` gates against the schema the same push is landing.
2217
+ // Issue #1336: `metadataManifestOverride` gates the op-access lint against
2218
+ // the manifest the same push is landing (see `createDatabaseTypeOperation`).
2219
+ options) {
2220
+ let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
2221
+ if (options?.dryRun && options.schemaOverride !== undefined) {
2222
+ body = { ...body, schemaOverride: options.schemaOverride };
2223
+ }
2224
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2225
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2226
+ }
2227
+ const query = options?.dryRun ? "?dryRun=true" : "";
2228
+ return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
2229
+ }
2230
+ async deleteDatabaseTypeOperation(appId, databaseType, name) {
2231
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2232
+ }
2233
+ // ============================================
2234
+ // DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
2235
+ // ============================================
2236
+ //
2237
+ // Transparent passthroughs against the server's type-scoped subscription
2238
+ // routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
2239
+ // The wire format is authoritative in the controller
2240
+ // (`database-type-subscriptions-controller.ts`): create requires
2241
+ // `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
2242
+ // (CEL); optional `description`, `select` (string[]), `emit`
2243
+ // (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
2244
+ //
2245
+ // The list endpoint wraps results in `{ items: [...] }` (unlike operations,
2246
+ // which returns a bare array). We unwrap here so callers get an array,
2247
+ // matching `listDatabaseTypeOperations`.
2248
+ async listDatabaseTypeSubscriptions(appId, databaseType) {
2249
+ const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
2250
+ if (Array.isArray(result))
2251
+ return result;
2252
+ return Array.isArray(result?.items) ? result.items : [];
2253
+ }
2254
+ async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2255
+ return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2256
+ }
2257
+ async createDatabaseTypeSubscription(appId, databaseType, data) {
2258
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
2259
+ }
2260
+ async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
2261
+ // #2731 B10 — optimistic concurrency, as the sibling operation update has
2262
+ // had. Omitted (by `--force`, and by every caller predating it) means the
2263
+ // write stays unconditional.
2264
+ expectedModifiedAt) {
2265
+ return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
2266
+ }
2267
+ async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2268
+ return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2269
+ }
763
2270
  // ============================================
764
2271
  // GROUPS
765
2272
  // ============================================
766
2273
  async listGroups(appId, params) {
767
- return this.get(`/app/${appId}/api/groups`, params);
2274
+ const result = await this.get(`/app/${appId}/api/groups`, params);
2275
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
2276
+ // so the CLI keeps paginating against pre-#1316 servers.
2277
+ const nextCursor = result?.nextCursor ?? result?.cursor;
2278
+ return {
2279
+ items: result?.items ?? [],
2280
+ nextCursor,
2281
+ hasMore: result?.hasMore ?? nextCursor != null,
2282
+ cursor: nextCursor,
2283
+ };
768
2284
  }
769
2285
  async createGroup(appId, data) {
770
2286
  return this.post(`/app/${appId}/api/groups`, data);
@@ -781,8 +2297,29 @@ export class ApiClient {
781
2297
  // ============================================
782
2298
  // GROUP MEMBERS
783
2299
  // ============================================
784
- async listGroupMembers(appId, groupType, groupId) {
785
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members`);
2300
+ /**
2301
+ * One page of a group's members.
2302
+ *
2303
+ * The route paginates cursor-style (no default limit, cap 200) and reports
2304
+ * the boundary, so `groups members list` carries `--limit`/`--cursor` and
2305
+ * forwards them (#3646) rather than printing a first page that claims to be
2306
+ * the whole membership.
2307
+ */
2308
+ async listGroupMembers(appId, groupType, groupId, options) {
2309
+ const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, {
2310
+ ...(options?.include ? { include: options.include } : {}),
2311
+ ...(options?.limit !== undefined ? { limit: options.limit } : {}),
2312
+ ...(options?.cursor ? { cursor: options.cursor } : {}),
2313
+ });
2314
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
2315
+ // so the CLI keeps paginating against pre-#1316 servers.
2316
+ const nextCursor = result?.nextCursor ?? result?.cursor;
2317
+ return {
2318
+ items: result?.items ?? [],
2319
+ nextCursor,
2320
+ hasMore: result?.hasMore ?? nextCursor != null,
2321
+ cursor: nextCursor,
2322
+ };
786
2323
  }
787
2324
  async addGroupMember(appId, groupType, groupId, data) {
788
2325
  return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
@@ -790,18 +2327,12 @@ export class ApiClient {
790
2327
  async removeGroupMember(appId, groupType, groupId, userId) {
791
2328
  return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
792
2329
  }
793
- async updateGroupMemberRole(appId, groupType, groupId, userId, data) {
794
- return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`, data);
795
- }
796
2330
  // ============================================
797
2331
  // GROUP RESOURCE LISTINGS
798
2332
  // ============================================
799
2333
  async listGroupDocuments(appId, groupType, groupId) {
800
2334
  return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
801
2335
  }
802
- async listGroupDatabases(appId, groupType, groupId) {
803
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/databases`);
804
- }
805
2336
  // ============================================
806
2337
  // USER MEMBERSHIPS
807
2338
  // ============================================
@@ -821,6 +2352,159 @@ export class ApiClient {
821
2352
  return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
822
2353
  }
823
2354
  // ============================================
2355
+ // COLLECTIONS
2356
+ // ============================================
2357
+ async listCollections(appId, params) {
2358
+ const qs = new URLSearchParams();
2359
+ if (params?.limit)
2360
+ qs.set("limit", String(params.limit));
2361
+ if (params?.cursor)
2362
+ qs.set("cursor", params.cursor);
2363
+ const q = qs.toString();
2364
+ const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
2365
+ return {
2366
+ items: result?.items ?? [],
2367
+ hasMore: result?.hasMore,
2368
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
2369
+ };
2370
+ }
2371
+ async listAllCollections(appId, params) {
2372
+ const qs = new URLSearchParams();
2373
+ if (params?.limit)
2374
+ qs.set("limit", String(params.limit));
2375
+ if (params?.cursor)
2376
+ qs.set("cursor", params.cursor);
2377
+ const q = qs.toString();
2378
+ const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
2379
+ return {
2380
+ items: result?.items ?? [],
2381
+ hasMore: result?.hasMore,
2382
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
2383
+ };
2384
+ }
2385
+ async createCollection(appId, data) {
2386
+ return this.post(`/app/${appId}/api/collections`, data);
2387
+ }
2388
+ async getCollection(appId, collectionId) {
2389
+ return this.get(`/app/${appId}/api/collections/${collectionId}`);
2390
+ }
2391
+ async updateCollection(appId, collectionId, data) {
2392
+ return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
2393
+ }
2394
+ async deleteCollection(appId, collectionId) {
2395
+ return this.delete(`/app/${appId}/api/collections/${collectionId}`);
2396
+ }
2397
+ // ============================================
2398
+ // COLLECTION DOCUMENTS
2399
+ // ============================================
2400
+ async listCollectionDocuments(appId, collectionId, params) {
2401
+ const qs = new URLSearchParams();
2402
+ if (params?.limit)
2403
+ qs.set("limit", String(params.limit));
2404
+ if (params?.cursor)
2405
+ qs.set("cursor", params.cursor);
2406
+ const q = qs.toString();
2407
+ const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
2408
+ return {
2409
+ items: result?.items ?? [],
2410
+ hasMore: result?.hasMore,
2411
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
2412
+ };
2413
+ }
2414
+ async addCollectionDocument(appId, collectionId, data) {
2415
+ return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
2416
+ }
2417
+ async removeCollectionDocument(appId, collectionId, documentId) {
2418
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
2419
+ }
2420
+ async listCollectionsForDocument(appId, documentId, params) {
2421
+ const qs = new URLSearchParams();
2422
+ if (params?.limit)
2423
+ qs.set("limit", String(params.limit));
2424
+ if (params?.cursor)
2425
+ qs.set("cursor", params.cursor);
2426
+ const q = qs.toString();
2427
+ const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
2428
+ return {
2429
+ items: result?.items ?? [],
2430
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
2431
+ };
2432
+ }
2433
+ // ============================================
2434
+ // COLLECTION ACCESS (GROUPS + MEMBERS)
2435
+ // ============================================
2436
+ async getCollectionAccess(appId, collectionId) {
2437
+ return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
2438
+ }
2439
+ async grantCollectionGroupPermission(appId, collectionId, data) {
2440
+ return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
2441
+ }
2442
+ async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
2443
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
2444
+ }
2445
+ async addCollectionMember(appId, collectionId, data) {
2446
+ return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
2447
+ }
2448
+ async removeCollectionMember(appId, collectionId, userId) {
2449
+ return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
2450
+ }
2451
+ // ============================================
2452
+ // COLLECTION TYPE CONFIGS
2453
+ // ============================================
2454
+ async listCollectionTypeConfigs(appId) {
2455
+ return this.get(`/app/${appId}/api/collection-type-configs`);
2456
+ }
2457
+ async getCollectionTypeConfig(appId, collectionType) {
2458
+ return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2459
+ }
2460
+ async createCollectionTypeConfig(appId, data) {
2461
+ return this.post(`/app/${appId}/api/collection-type-configs`, data);
2462
+ }
2463
+ async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
2464
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2465
+ return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
2466
+ }
2467
+ async deleteCollectionTypeConfig(appId, collectionType) {
2468
+ return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2469
+ }
2470
+ // ============================================
2471
+ // METADATA CATEGORY CONFIGS (issue #1304, P-B)
2472
+ // ============================================
2473
+ /**
2474
+ * List all metadata category configs for an app. The route returns
2475
+ * `{ configs: [...] }`; unwrap to a bare array to match the other
2476
+ * `list*Configs` helpers.
2477
+ */
2478
+ async listMetadataCategoryConfigs(appId) {
2479
+ const res = await this.get(`/app/${appId}/api/metadata-categories`);
2480
+ return Array.isArray(res?.configs) ? res.configs : [];
2481
+ }
2482
+ async getMetadataCategoryConfig(appId, resourceType, category) {
2483
+ return this.get(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
2484
+ }
2485
+ /**
2486
+ * Create or replace a metadata category config (idempotent upsert via the
2487
+ * path-addressed PUT route). `resourceType` / `category` identify the config;
2488
+ * the body carries `schema` / `readRule` / `writeRule` / `description`.
2489
+ */
2490
+ async upsertMetadataCategoryConfig(appId, resourceType, category, data,
2491
+ /** Optimistic-concurrency token; see `upsertEmailTemplate` (#2731 B10). */
2492
+ expectedModifiedAt) {
2493
+ return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
2494
+ }
2495
+ /**
2496
+ * Delete a metadata category config (issue #1426, admin-gated route shipped
2497
+ * in #1364). Orphan semantics: the config row is hard-deleted; any stored
2498
+ * `ResourceMetadata` value rows for the category are left behind and become
2499
+ * unreachable (no query path from a category to its values). Delete the
2500
+ * values first if you need them gone — an orphaned value row can't be
2501
+ * removed through any surface once its config is gone. Returns
2502
+ * `{ resourceType, category, deleted }`; a missing config is a 404.
2503
+ */
2504
+ async deleteMetadataCategoryConfig(appId, resourceType, category) {
2505
+ return this.delete(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
2506
+ }
2507
+ // ============================================
824
2508
  // ACCESS RULE SETS
825
2509
  // ============================================
826
2510
  async listRuleSets(appId, params) {
@@ -832,13 +2516,672 @@ export class ApiClient {
832
2516
  async getRuleSet(appId, ruleSetId) {
833
2517
  return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
834
2518
  }
835
- async updateRuleSet(appId, ruleSetId, data) {
836
- return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, data);
2519
+ async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
2520
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2521
+ return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
837
2522
  }
838
2523
  async deleteRuleSet(appId, ruleSetId) {
839
2524
  return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
840
2525
  }
2526
+ async getRuleSetSchema(appId) {
2527
+ return this.get(`/app/${appId}/api/rule-sets/schema`);
2528
+ }
2529
+ async getRuleSetResourceTypes(appId) {
2530
+ return this.get(`/app/${appId}/api/rule-sets/resource-types`);
2531
+ }
2532
+ async testRuleSet(appId, ruleSetId, data) {
2533
+ return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
2534
+ }
2535
+ async debugRuleSet(appId, data) {
2536
+ return this.post(`/app/${appId}/api/rule-sets/debug`, data);
2537
+ }
2538
+ // ============================================
2539
+ // GROUP TYPE CONFIGS
2540
+ // ============================================
2541
+ async listGroupTypeConfigs(appId) {
2542
+ return this.get(`/app/${appId}/api/group-type-configs`);
2543
+ }
2544
+ async getGroupTypeConfig(appId, groupType) {
2545
+ return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
2546
+ }
2547
+ async createGroupTypeConfig(appId, data) {
2548
+ return this.post(`/app/${appId}/api/group-type-configs`, data);
2549
+ }
2550
+ async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
2551
+ const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2552
+ return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
2553
+ }
2554
+ async deleteGroupTypeConfig(appId, groupType) {
2555
+ return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
2556
+ }
2557
+ // ============================================
2558
+ // DOCUMENT EXPORT / IMPORT
2559
+ // ============================================
2560
+ async exportDocumentState(appId, documentId) {
2561
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
2562
+ }
2563
+ /**
2564
+ * The export chain of a large document (#2816, behavior 20): the latest
2565
+ * completed snapshot, the sealed overlays after its epoch, and the open
2566
+ * epoch's overlay inline. Every other artifact arrives as an expiring
2567
+ * signature, read through {@link downloadDocumentArtifact}.
2568
+ */
2569
+ async exportDocumentChain(appId, documentId) {
2570
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/chain`);
2571
+ }
2572
+ /**
2573
+ * Read one granted artifact of a large document.
2574
+ *
2575
+ * The path comes from the chain and is origin-relative — the room is a
2576
+ * Durable Object and has no reliable idea which host the caller reached it
2577
+ * through — and the signature in it is the whole authorization, so no key is
2578
+ * ever composed here.
2579
+ */
2580
+ async downloadDocumentArtifact(_appId, grantedPath) {
2581
+ const credentials = await this.ensureAuthenticated();
2582
+ const url = `${credentials.serverUrl}${grantedPath}`;
2583
+ const response = await fetchWithTLS(url);
2584
+ if (!response.ok) {
2585
+ // #3403 — through the shared parser: the refusal this reads is the app
2586
+ // API's coded envelope, and `statusText` carried none of it.
2587
+ throw await blobFailure(response, "Failed to download document artifact: ");
2588
+ }
2589
+ return Buffer.from(await response.arrayBuffer());
2590
+ }
2591
+ async importDocumentState(appId, documentId, stateBase64) {
2592
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2593
+ }
2594
+ /**
2595
+ * Upload one artifact of a large document's import chain (#2816,
2596
+ * behavior 20).
2597
+ *
2598
+ * The bytes travel raw and unmodified: the manifest's digest is over the
2599
+ * STORED bytes, so re-encoding a chunk here would fail its own verification
2600
+ * on install. The query says which artifact it is — the server derives the
2601
+ * key from the document it belongs to, so nothing here composes one.
2602
+ */
2603
+ async uploadDocumentChainArtifact(appId, documentId, query, body) {
2604
+ const credentials = await this.ensureAuthenticated();
2605
+ const search = new URLSearchParams();
2606
+ for (const [key, value] of Object.entries(query)) {
2607
+ search.set(key, String(value));
2608
+ }
2609
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2610
+ `/import/chain/artifact?${search.toString()}`;
2611
+ const headers = {
2612
+ Authorization: `Bearer ${credentials.accessToken}`,
2613
+ "Content-Type": "application/octet-stream",
2614
+ };
2615
+ if (credentials.globalAdminAppId) {
2616
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2617
+ }
2618
+ const response = await fetchWithTLS(url, {
2619
+ method: "POST",
2620
+ headers,
2621
+ body: body,
2622
+ });
2623
+ if (!response.ok) {
2624
+ // #3403 — through the shared parser: reading `error` alone dropped the
2625
+ // envelope's `code` and `details`, so the refusal reached the operator
2626
+ // (and `primitive documents import`) with nothing to branch on.
2627
+ throw await blobFailure(response, "Failed to upload import artifact: ");
2628
+ }
2629
+ return await response.json().catch(() => ({}));
2630
+ }
2631
+ /** Start installing an uploaded chain (#2816, behavior 20). */
2632
+ async installDocumentChain(appId, documentId, plan) {
2633
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/chain`, plan);
2634
+ }
2635
+ /** How far a document's chain install has got (#2816, behavior 20). */
2636
+ async getDocumentChainInstall(appId, documentId) {
2637
+ return this.get(`/app/${appId}/api/documents/${documentId}/import/chain`);
2638
+ }
2639
+ async getDocument(appId, documentId) {
2640
+ return this.get(`/app/${appId}/api/documents/${documentId}`);
2641
+ }
2642
+ /**
2643
+ * Delete a document and everything hanging off it (#2756). The server route
2644
+ * runs the same cascade the client SDK's `documents.delete()` triggers —
2645
+ * Yjs state and update history, blob rows and objects, aliases, user/group
2646
+ * permissions, invitations, collection memberships — and returns
2647
+ * `{ success, message }`, which callers pass through under `--json`.
2648
+ */
2649
+ async deleteDocument(appId, documentId) {
2650
+ return this.delete(`/app/${appId}/api/documents/${documentId}`);
2651
+ }
2652
+ /**
2653
+ * Introspect a document's Yjs schema (model names, per-model fields and
2654
+ * indexes). Backs `documents records models` and `documents records
2655
+ * describe`. Reuses the existing `GET documents/:id/schema` endpoint.
2656
+ */
2657
+ async getDocumentSchema(appId, documentId) {
2658
+ return this.get(`/app/${appId}/api/documents/${documentId}/schema`);
2659
+ }
2660
+ /**
2661
+ * The `documentFormat` a records call states, as a query string to APPEND to
2662
+ * a path (#3764).
2663
+ *
2664
+ * `post`, `patch` and `delete` take no params object — only `get` does — so
2665
+ * for every verb but the reads the parameter rides the path. Empty when the
2666
+ * caller states nothing, so an unparameterised call's URL is byte-identical
2667
+ * to what it was.
2668
+ */
2669
+ documentFormatQuery(documentFormat) {
2670
+ return documentFormat === undefined ? "" : `?documentFormat=${documentFormat}`;
2671
+ }
2672
+ /**
2673
+ * Query records in a document model (issue #1964 Phase 3). Reuses the
2674
+ * server-side `GET documents/:id/records/:model` endpoint, which runs
2675
+ * through the caller's document permission (reader+) or the app-admin arm.
2676
+ * Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
2677
+ * is an opaque token — feed `nextCursor` back as `cursor` for the next page.
2678
+ * The server also dual-emits a deprecated `cursor` alias (#1316); the
2679
+ * normalizer drops it so CLI output is already on `nextCursor` when #1982
2680
+ * removes it.
2681
+ */
2682
+ async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
2683
+ const query = {};
2684
+ if (queryOptions?.filter)
2685
+ query.filter = JSON.stringify(queryOptions.filter);
2686
+ if (queryOptions?.documentFormat !== undefined) {
2687
+ query.documentFormat = queryOptions.documentFormat;
2688
+ }
2689
+ // Forward `limit` whenever the caller supplied one (including an invalid
2690
+ // 0/negative) so the server validates and returns a clean 400 rather than
2691
+ // silently applying the default.
2692
+ if (queryOptions?.limit !== undefined)
2693
+ query.limit = queryOptions.limit;
2694
+ if (queryOptions?.cursor)
2695
+ query.cursor = queryOptions.cursor;
2696
+ const raw = await this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, query);
2697
+ return normalizeCliListEnvelope(raw);
2698
+ }
2699
+ /**
2700
+ * Count records in a document model (issue #1964 Phase 3). Returns `{ count }`.
2701
+ */
2702
+ async countDocumentRecords(appId, documentId, modelName, queryOptions) {
2703
+ const query = {};
2704
+ if (queryOptions?.filter)
2705
+ query.filter = JSON.stringify(queryOptions.filter);
2706
+ if (queryOptions?.documentFormat !== undefined) {
2707
+ query.documentFormat = queryOptions.documentFormat;
2708
+ }
2709
+ return this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/count`, query);
2710
+ }
2711
+ /**
2712
+ * Aggregate records in a document model (issue #2437) — the documents twin
2713
+ * of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
2714
+ * with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
2715
+ * in the same shape as the database endpoint.
2716
+ *
2717
+ * `groupBy` takes plain field names only: StringSet facet grouping and
2718
+ * `{ field, contains }` membership grouping are database-only, which is why
2719
+ * this signature is narrower than the databases one.
2720
+ */
2721
+ async aggregateDocumentRecords(appId, documentId, modelName, options, extra) {
2722
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate` +
2723
+ this.documentFormatQuery(extra?.documentFormat), { options });
2724
+ }
2725
+ /**
2726
+ * Fetch platform-vocabulary document statistics (issue #1964 Phase 3):
2727
+ * record/model/blob counts, approximate size, and last-modified timestamp.
2728
+ */
2729
+ async getDocumentStats(appId, documentId) {
2730
+ return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
2731
+ }
2732
+ /**
2733
+ * A large document's snapshot builds, newest first, one page (#3432).
2734
+ * `GET documents/:id/snapshots` → `{ items, hasMore, nextCursor? }`.
2735
+ */
2736
+ async listDocumentSnapshots(appId, documentId, options = {}) {
2737
+ const params = new URLSearchParams();
2738
+ if (options.limit !== undefined)
2739
+ params.set("limit", String(options.limit));
2740
+ if (options.cursor)
2741
+ params.set("cursor", options.cursor);
2742
+ const query = params.toString();
2743
+ return this.get(`/app/${appId}/api/documents/${documentId}/snapshots${query ? `?${query}` : ""}`);
2744
+ }
2745
+ /**
2746
+ * A large document's bulk-load sessions, newest first (#3434, criterion 9).
2747
+ *
2748
+ * Read-only, through the document read helper's app-admin arm — the same
2749
+ * door the snapshot readers use. The action verb `documents ingest` is
2750
+ * #3435's; these two are the operator's window onto what a session did.
2751
+ */
2752
+ async listDocumentIngests(appId, documentId, options = {}) {
2753
+ const params = new URLSearchParams();
2754
+ if (options.limit !== undefined)
2755
+ params.set("limit", String(options.limit));
2756
+ if (options.cursor)
2757
+ params.set("cursor", options.cursor);
2758
+ const query = params.toString();
2759
+ return this.get(`/app/${appId}/api/documents/${documentId}/ingest${query ? `?${query}` : ""}`);
2760
+ }
2761
+ /** One bulk-load session's status (#3434). */
2762
+ async getDocumentIngest(appId, documentId, sessionId) {
2763
+ return this.get(`/app/${appId}/api/documents/${documentId}/ingest/${encodeURIComponent(sessionId)}`);
2764
+ }
2765
+ /** Open a bulk-load session on a large document (#3435, criterion 9). */
2766
+ async createDocumentIngest(appId, documentId) {
2767
+ return this.post(`/app/${appId}/api/documents/${documentId}/ingest`, {});
2768
+ }
2769
+ /**
2770
+ * Upload one chunk of a bulk-load session's artifact (#3435).
2771
+ *
2772
+ * `multipart/form-data`: the eight descriptor fields as text parts and the
2773
+ * gzipped bytes as one file part named `chunk`. No `Content-Type` is set on
2774
+ * the request — fetch supplies the boundary, and a header written by hand
2775
+ * would name one the body does not use.
2776
+ */
2777
+ async uploadDocumentIngestChunk(appId, documentId, sessionId, chunk) {
2778
+ const credentials = await this.ensureAuthenticated();
2779
+ const form = new FormData();
2780
+ form.set("model", chunk.model);
2781
+ form.set("index", String(chunk.index));
2782
+ form.set("rows", String(chunk.rows));
2783
+ form.set("bytes", String(chunk.bytes));
2784
+ form.set("rawBytes", String(chunk.rawBytes));
2785
+ form.set("sha256", chunk.sha256);
2786
+ form.set("firstId", chunk.firstId);
2787
+ form.set("lastId", chunk.lastId);
2788
+ form.set("chunk", new Blob([chunk.body]), `${chunk.index}.ndjson.gz`);
2789
+ const headers = {
2790
+ Authorization: `Bearer ${credentials.accessToken}`,
2791
+ };
2792
+ if (credentials.globalAdminAppId) {
2793
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2794
+ }
2795
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2796
+ `/ingest/${encodeURIComponent(sessionId)}/chunks`;
2797
+ const response = await fetchWithTLS(url, {
2798
+ method: "POST",
2799
+ headers,
2800
+ body: form,
2801
+ });
2802
+ const text = await response.text();
2803
+ let result = {};
2804
+ try {
2805
+ result = text ? JSON.parse(text) : {};
2806
+ }
2807
+ catch {
2808
+ result = { error: text };
2809
+ }
2810
+ if (!response.ok) {
2811
+ throw new ApiError(result?.error || `Failed to upload chunk: ${response.statusText}`, response.status, result?.code, result?.details);
2812
+ }
2813
+ return result;
2814
+ }
2815
+ /** Freeze a session's manifest and hand it to the pipeline (#3435). */
2816
+ async commitDocumentIngest(appId, documentId, sessionId) {
2817
+ return this.post(`/app/${appId}/api/documents/${documentId}/ingest/${encodeURIComponent(sessionId)}/commit`, {});
2818
+ }
2819
+ /** Give a session up; allowed until the swap (#3435). */
2820
+ async abortDocumentIngest(appId, documentId, sessionId) {
2821
+ return this.post(`/app/${appId}/api/documents/${documentId}/ingest/${encodeURIComponent(sessionId)}/abort`, {});
2822
+ }
2823
+ /** One snapshot build with its verification result (#3432). */
2824
+ async getDocumentSnapshot(appId, documentId, buildId) {
2825
+ return this.get(`/app/${appId}/api/documents/${documentId}/snapshots/${encodeURIComponent(buildId)}`);
2826
+ }
2827
+ /**
2828
+ * Snapshot a large document now (#3666): seal the open epoch and start the
2829
+ * base build that seal arms.
2830
+ *
2831
+ * Answers either the epoch that was sealed with the build it armed, or
2832
+ * `{ sealed: false, reason: "empty" }` for an epoch that carried nothing.
2833
+ */
2834
+ async requestDocumentSnapshot(appId, documentId) {
2835
+ return this.post(`/app/${appId}/api/documents/${documentId}/snapshots`, {});
2836
+ }
2837
+ /**
2838
+ * Create or replace a record in a document model (issue #1964 Phase 4).
2839
+ * `POST documents/:id/records/:model` with `{ id, data, options? }` —
2840
+ * server-side write through the document facade (Yjs-first, CRDT-safe,
2841
+ * broadcasts to connected clients). Requires read-write+ on the document
2842
+ * or the app-admin arm. Returns `{ record }`.
2843
+ */
2844
+ async saveDocumentRecord(appId, documentId, modelName, body, extra) {
2845
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}` +
2846
+ this.documentFormatQuery(extra?.documentFormat), body);
2847
+ }
2848
+ /**
2849
+ * Merge fields into an existing record (issue #1964 Phase 4).
2850
+ * `PATCH documents/:id/records/:model/:recordId` with `{ data, options? }`.
2851
+ * Returns `{ record }`.
2852
+ */
2853
+ async patchDocumentRecord(appId, documentId, modelName, recordId, body, extra) {
2854
+ return this.patch(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}` +
2855
+ this.documentFormatQuery(extra?.documentFormat), body);
2856
+ }
2857
+ /**
2858
+ * Delete a record from a document model (issue #1964 Phase 4).
2859
+ * `DELETE documents/:id/records/:model/:recordId`. A missing record id is
2860
+ * a silent no-op per the facade contract. Returns `{ deleted: true }`.
2861
+ */
2862
+ async deleteDocumentRecord(appId, documentId, modelName, recordId, extra) {
2863
+ return this.delete(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}` +
2864
+ this.documentFormatQuery(extra?.documentFormat));
2865
+ }
2866
+ /**
2867
+ * Apply an ordered multi-model create/patch/delete blob atomically (issue
2868
+ * #1964 Phase 4, wrapping the #1517 `bulkUpdate` facade). All-or-nothing:
2869
+ * a validation failure commits nothing. Returns the facade's
2870
+ * `{ applied, added, updated, deleted }`.
2871
+ */
2872
+ async bulkDocumentRecords(appId, documentId, operations, extra) {
2873
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk` +
2874
+ this.documentFormatQuery(extra?.documentFormat), { operations });
2875
+ }
2876
+ async createDocument(appId, data) {
2877
+ return this.post(`/app/${appId}/api/documents`, data);
2878
+ }
2879
+ async listDocumentPermissions(appId, documentId) {
2880
+ return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
2881
+ }
2882
+ async grantDocumentPermission(appId, documentId, permissions) {
2883
+ return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
2884
+ }
2885
+ /**
2886
+ * Revoke a user's permission on a document. Mirrors the published client's
2887
+ * `documents.removePermission` wire behavior (`src/client/api/documentsApi.ts`):
2888
+ * a userId targets the path route `DELETE .../permissions/:userId`, while an
2889
+ * email targets the query-param route `DELETE .../permissions?email=...`
2890
+ * (unified email removal, #452). This duplication of the client's wire shape
2891
+ * is deliberate and parity-tested (issue #1964 amendment 8).
2892
+ */
2893
+ async revokeDocumentPermission(appId, documentId, target) {
2894
+ if (target.email) {
2895
+ const email = encodeURIComponent(target.email);
2896
+ return this.delete(`/app/${appId}/api/documents/${documentId}/permissions?email=${email}`);
2897
+ }
2898
+ if (!target.userId) {
2899
+ throw new Error("revokeDocumentPermission: userId or email is required");
2900
+ }
2901
+ return this.delete(`/app/${appId}/api/documents/${documentId}/permissions/${target.userId}`);
2902
+ }
2903
+ /**
2904
+ * Resolve a NAMED user's effective access to a document (#3658).
2905
+ *
2906
+ * The admin token is admitted to name a subject (it can already read every
2907
+ * grant, group permission and membership the answer derives from), which is
2908
+ * what makes this an operator read rather than a per-user one.
2909
+ */
2910
+ async validateDocumentAccessForUser(appId, documentId, userId) {
2911
+ return this.post(`/app/${appId}/api/documents/${documentId}/validate-access`, { userId });
2912
+ }
2913
+ async getDocumentLinkAccess(appId, documentId) {
2914
+ return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
2915
+ }
2916
+ async setDocumentLinkAccess(appId, documentId, level) {
2917
+ return this.put(`/app/${appId}/api/documents/${documentId}/link-access`, { level });
2918
+ }
2919
+ async clearDocumentLinkAccess(appId, documentId) {
2920
+ return this.delete(`/app/${appId}/api/documents/${documentId}/link-access`);
2921
+ }
2922
+ /**
2923
+ * Pending deferred grants for a document (#2951 — replaces the removed
2924
+ * legacy `GET /documents/:id/invitations`). Returns the `{ items }`
2925
+ * envelope; rows are already filtered server-side to unresolved, unexpired
2926
+ * grants whose app invitation is still outstanding.
2927
+ */
2928
+ async listDocumentPendingInvitations(appId, documentId) {
2929
+ return this.get(`/app/${appId}/api/documents/${documentId}/pending-invitations`);
2930
+ }
2931
+ async listDocumentBlobs(appId, documentId) {
2932
+ return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
2933
+ }
2934
+ async downloadBlob(appId, documentId, blobId) {
2935
+ const credentials = await this.ensureAuthenticated();
2936
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
2937
+ const headers = {
2938
+ Authorization: `Bearer ${credentials.accessToken}`,
2939
+ };
2940
+ if (credentials.globalAdminAppId) {
2941
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2942
+ }
2943
+ const response = await fetchWithTLS(url, { headers });
2944
+ if (!response.ok) {
2945
+ throw await blobFailure(response, "Failed to download blob: ");
2946
+ }
2947
+ const arrayBuffer = await response.arrayBuffer();
2948
+ return Buffer.from(arrayBuffer);
2949
+ }
2950
+ async uploadBlob(appId, documentId, blobId, data, meta) {
2951
+ const credentials = await this.ensureAuthenticated();
2952
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
2953
+ const headers = {
2954
+ Authorization: `Bearer ${credentials.accessToken}`,
2955
+ "Content-Type": meta.contentType,
2956
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
2957
+ "X-Blob-Size": String(data.length),
2958
+ "X-Blob-Sha256": meta.sha256,
2959
+ };
2960
+ if (credentials.globalAdminAppId) {
2961
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2962
+ }
2963
+ const response = await fetchWithTLS(url, {
2964
+ method: "PUT",
2965
+ headers,
2966
+ body: data,
2967
+ });
2968
+ if (!response.ok) {
2969
+ throw await blobFailure(response, "Failed to upload blob: ");
2970
+ }
2971
+ return response.json();
2972
+ }
2973
+ async listDocumentAliases(appId, documentId) {
2974
+ return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
2975
+ }
2976
+ async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
2977
+ return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
2978
+ documentId,
2979
+ userId: ownerUserId,
2980
+ mustNotExist,
2981
+ });
2982
+ }
2983
+ /**
2984
+ * Resolve an app user by exact email — the single email → userId lookup in
2985
+ * the CLI (issue #2763). It reads the member-accessible `users/lookup`
2986
+ * endpoint rather than the admin-only `GET /users` list, and returns the
2987
+ * server's `{ exists, user? }` envelope as-is: a 403 or a transport failure
2988
+ * rejects instead of being flattened into "no such user", which is exactly
2989
+ * how the deleted admin-list helper misreported permission problems.
2990
+ */
2991
+ async lookupUserByEmail(appId, email) {
2992
+ return this.get(`/app/${appId}/api/users/lookup`, { email });
2993
+ }
2994
+ /**
2995
+ * The admin inventory of one user's documents, followed to the end (#3399).
2996
+ *
2997
+ * The endpoint speaks the standard list contract (`{ items, hasMore,
2998
+ * nextCursor }`) and caps a page, so one request reads one page: reading it
2999
+ * would truncate a heavy user's inventory at the page size, which is the
3000
+ * bug this issue is about with a smaller number on it. `documents list
3001
+ * --user-id` and `documents export-all` both read this, and both mean "all
3002
+ * of them". `paginateAll` carries the repeat-cursor and max-page guards, so
3003
+ * a server handing out a non-advancing cursor fails loudly instead of
3004
+ * looping forever.
3005
+ *
3006
+ * Tolerant of a pre-contract server, which answers with a bare
3007
+ * `{ documents }` array and no cursor: that reads as a single page.
3008
+ */
3009
+ async listAdminDocuments(appId, userId) {
3010
+ return paginateAll(async (cursor) => {
3011
+ const page = await this.listAdminDocumentsPage(appId, userId, {
3012
+ limit: ADMIN_INVENTORY_PAGE_SIZE,
3013
+ cursor,
3014
+ });
3015
+ return { items: page.items, nextCursor: page.nextCursor };
3016
+ });
3017
+ }
3018
+ /**
3019
+ * One page of the admin inventory of a user's documents (#3646).
3020
+ *
3021
+ * `documents list --user-id` prints exactly this — the page the caller's
3022
+ * `--limit`/`--cursor` asked for, with the server's own page boundary
3023
+ * beside it. `documents export-all`, which really does mean every document,
3024
+ * keeps the walking `listAdminDocuments()` above.
3025
+ *
3026
+ * Tolerant of a pre-contract server, which answers with a bare
3027
+ * `{ documents }` array and no cursor: that reads as a single page.
3028
+ */
3029
+ async listAdminDocumentsPage(appId, userId, options = {}) {
3030
+ const params = new URLSearchParams({ userId });
3031
+ if (options.limit !== undefined)
3032
+ params.set("limit", String(options.limit));
3033
+ if (options.cursor)
3034
+ params.set("cursor", options.cursor);
3035
+ const result = await this.get(`/admin/api/apps/${appId}/documents?${params.toString()}`);
3036
+ return {
3037
+ items: result?.items ?? result?.documents ?? [],
3038
+ hasMore: result?.hasMore,
3039
+ // #1316: prefer `nextCursor`; the deprecated `cursor` alias keeps a
3040
+ // shipped CLI paginating against a server that still dual-emits.
3041
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
3042
+ };
3043
+ }
3044
+ /**
3045
+ * The document `AppUser.rootDocId` points at, or null when the user has none
3046
+ * yet (#3135). A user who is not a member of the app is a 404, which
3047
+ * `documents import` reads as "this export has no target here" — distinct
3048
+ * from "this user has no root document yet".
3049
+ */
3050
+ async getUserRootDocument(appId, userId) {
3051
+ return this.get(`/admin/api/apps/${appId}/users/${userId}/root-document`);
3052
+ }
3053
+ /**
3054
+ * Get or create the user's root document (#3135).
3055
+ *
3056
+ * The server runs the same idempotent get-or-create sign-in uses, so this
3057
+ * never mints a second root. `created` is true only when this call's claim
3058
+ * won — an import must not report "created", or merge without `--overwrite`,
3059
+ * for a root a concurrent sign-in minted first.
3060
+ */
3061
+ async ensureUserRootDocument(appId, userId) {
3062
+ return this.post(`/admin/api/apps/${appId}/users/${userId}/root-document`, {});
3063
+ }
3064
+ // ============================================
3065
+ // DATABASE EXPORT / IMPORT
3066
+ // ============================================
3067
+ // `saveDatabaseRecord` lives with the other record verbs under
3068
+ // "DATABASE RECORDS & SCHEMA" above.
3069
+ /**
3070
+ * Apply an ordered batch of record operations via `records/batch`.
3071
+ *
3072
+ * `atomic` (default off, matching the DatabaseDO) makes the whole batch
3073
+ * all-or-nothing: the DO rolls every write back at the first failed
3074
+ * operation and answers with that operation's status. `databases records
3075
+ * bulk` sends `atomic: true` so it matches the documents twin's
3076
+ * all-or-nothing contract (#2437); the CSV/import paths keep the default
3077
+ * partial-success behavior.
3078
+ */
3079
+ async batchDatabaseRecords(appId, databaseId, operations, options) {
3080
+ const body = { operations };
3081
+ if (options?.atomic)
3082
+ body.atomic = true;
3083
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, body);
3084
+ }
3085
+ async deleteDatabaseRecord(appId, databaseId, modelName, id) {
3086
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, { modelName, id });
3087
+ }
3088
+ async batchDeleteDatabaseRecords(appId, databaseId, operations) {
3089
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
3090
+ }
3091
+ async listDatabaseUniqueConstraints(appId, databaseId) {
3092
+ const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
3093
+ return result?.constraints || result || [];
3094
+ }
3095
+ async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
3096
+ return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
3097
+ }
3098
+ // ============================================
3099
+ // BLOB BUCKETS
3100
+ // ============================================
3101
+ async listBlobBuckets(appId) {
3102
+ return this.get(`/app/${appId}/api/blob-buckets`);
3103
+ }
3104
+ async createBlobBucket(appId, payload) {
3105
+ return this.post(`/app/${appId}/api/blob-buckets`, payload);
3106
+ }
3107
+ async getBlobBucket(appId, bucketIdOrKey) {
3108
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
3109
+ }
3110
+ async updateBlobBucket(appId, bucketIdOrKey, payload) {
3111
+ return this.patch(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`, payload);
3112
+ }
3113
+ async deleteBlobBucket(appId, bucketIdOrKey) {
3114
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
3115
+ }
3116
+ async listBucketBlobs(appId, bucketIdOrKey, params) {
3117
+ const result = await this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
3118
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
3119
+ // so the CLI keeps paginating against pre-#1316 servers.
3120
+ const nextCursor = result?.nextCursor ?? result?.cursor;
3121
+ return {
3122
+ items: result?.items ?? [],
3123
+ nextCursor,
3124
+ hasMore: result?.hasMore ?? nextCursor != null,
3125
+ cursor: nextCursor,
3126
+ };
3127
+ }
3128
+ async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
3129
+ const credentials = await this.ensureAuthenticated();
3130
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
3131
+ const headers = {
3132
+ Authorization: `Bearer ${credentials.accessToken}`,
3133
+ "Content-Type": meta.contentType,
3134
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
3135
+ };
3136
+ if (meta.tags && meta.tags.length > 0) {
3137
+ headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
3138
+ }
3139
+ if (credentials.globalAdminAppId) {
3140
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
3141
+ }
3142
+ const response = await fetchWithTLS(url, {
3143
+ method: "POST",
3144
+ headers,
3145
+ body: data,
3146
+ });
3147
+ if (!response.ok) {
3148
+ throw await blobFailure(response, "Failed to upload blob: ");
3149
+ }
3150
+ return response.json();
3151
+ }
3152
+ async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
3153
+ const credentials = await this.ensureAuthenticated();
3154
+ const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
3155
+ const headers = {
3156
+ Authorization: `Bearer ${credentials.accessToken}`,
3157
+ };
3158
+ if (credentials.globalAdminAppId) {
3159
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
3160
+ }
3161
+ const response = await fetchWithTLS(url, { headers });
3162
+ if (!response.ok) {
3163
+ throw await blobFailure(response, "Failed to download blob: ");
3164
+ }
3165
+ const arrayBuffer = await response.arrayBuffer();
3166
+ return Buffer.from(arrayBuffer);
3167
+ }
3168
+ async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
3169
+ return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
3170
+ }
3171
+ // Batch delete (#1455): one round-trip for N ids via POST .../blobs/delete.
3172
+ async deleteBucketBlobs(appId, bucketIdOrKey, blobIds) {
3173
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/delete`, { blobIds });
3174
+ }
3175
+ async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
3176
+ return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
3177
+ expiresInSeconds: expiresInSeconds || 300,
3178
+ });
3179
+ }
3180
+ // Blob metadata (#1966): head a single blob without downloading its bytes.
3181
+ // Mirrors listBucketBlobs/downloadBucketBlob addressing; the server heads the
3182
+ // R2 object and returns the serialized BlobInfo shape.
3183
+ async getBucketBlobMetadata(appId, bucketIdOrKey, blobId) {
3184
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
3185
+ }
841
3186
  }
842
- // Export a singleton instance
843
- export const apiClient = new ApiClient();
844
3187
  //# sourceMappingURL=api-client.js.map