primitive-admin 1.0.63 → 1.0.64

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 (328) hide show
  1. package/README.md +132 -42
  2. package/assets/skill/skills/primitive-platform/SKILL.md +142 -661
  3. package/dist/bin/primitive.js +121 -41
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +37 -50
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +178 -9
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +12 -5
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/auth-sessions.d.ts +7 -0
  12. package/dist/src/commands/auth-sessions.js +144 -0
  13. package/dist/src/commands/auth-sessions.js.map +1 -0
  14. package/dist/src/commands/auth.js +76 -116
  15. package/dist/src/commands/auth.js.map +1 -1
  16. package/dist/src/commands/blob-buckets.js +2 -1
  17. package/dist/src/commands/blob-buckets.js.map +1 -1
  18. package/dist/src/commands/catalog.js +26 -10
  19. package/dist/src/commands/catalog.js.map +1 -1
  20. package/dist/src/commands/collection-type-configs.js +3 -10
  21. package/dist/src/commands/collection-type-configs.js.map +1 -1
  22. package/dist/src/commands/collections.js +736 -19
  23. package/dist/src/commands/collections.js.map +1 -1
  24. package/dist/src/commands/config.js +13 -27
  25. package/dist/src/commands/config.js.map +1 -1
  26. package/dist/src/commands/connections.js +8 -9
  27. package/dist/src/commands/connections.js.map +1 -1
  28. package/dist/src/commands/cron-triggers.js +2 -1
  29. package/dist/src/commands/cron-triggers.js.map +1 -1
  30. package/dist/src/commands/database-type-configs.js +5 -12
  31. package/dist/src/commands/database-type-configs.js.map +1 -1
  32. package/dist/src/commands/databases.js +59 -72
  33. package/dist/src/commands/databases.js.map +1 -1
  34. package/dist/src/commands/documents.d.ts +58 -0
  35. package/dist/src/commands/documents.js +1623 -95
  36. package/dist/src/commands/documents.js.map +1 -1
  37. package/dist/src/commands/email-templates.js +2 -1
  38. package/dist/src/commands/email-templates.js.map +1 -1
  39. package/dist/src/commands/env.d.ts +1 -1
  40. package/dist/src/commands/env.js +89 -43
  41. package/dist/src/commands/env.js.map +1 -1
  42. package/dist/src/commands/feature-flags.js +2 -1
  43. package/dist/src/commands/feature-flags.js.map +1 -1
  44. package/dist/src/commands/functions.d.ts +20 -0
  45. package/dist/src/commands/functions.js +1630 -0
  46. package/dist/src/commands/functions.js.map +1 -0
  47. package/dist/src/commands/group-type-configs.js +2 -1
  48. package/dist/src/commands/group-type-configs.js.map +1 -1
  49. package/dist/src/commands/groups.js +30 -17
  50. package/dist/src/commands/groups.js.map +1 -1
  51. package/dist/src/commands/guides.js +17 -11
  52. package/dist/src/commands/guides.js.map +1 -1
  53. package/dist/src/commands/init.d.ts +12 -15
  54. package/dist/src/commands/init.js +44 -92
  55. package/dist/src/commands/init.js.map +1 -1
  56. package/dist/src/commands/integrations.js +35 -12
  57. package/dist/src/commands/integrations.js.map +1 -1
  58. package/dist/src/commands/locks.js +17 -2
  59. package/dist/src/commands/locks.js.map +1 -1
  60. package/dist/src/commands/metadata-category-configs.js +2 -1
  61. package/dist/src/commands/metadata-category-configs.js.map +1 -1
  62. package/dist/src/commands/metadata.js +8 -1
  63. package/dist/src/commands/metadata.js.map +1 -1
  64. package/dist/src/commands/prompts.js +106 -59
  65. package/dist/src/commands/prompts.js.map +1 -1
  66. package/dist/src/commands/rule-sets.js +3 -10
  67. package/dist/src/commands/rule-sets.js.map +1 -1
  68. package/dist/src/commands/scripts.js +33 -28
  69. package/dist/src/commands/scripts.js.map +1 -1
  70. package/dist/src/commands/secrets.js +2 -1
  71. package/dist/src/commands/secrets.js.map +1 -1
  72. package/dist/src/commands/sessions.js +10 -9
  73. package/dist/src/commands/sessions.js.map +1 -1
  74. package/dist/src/commands/sync-app-settings.js +3 -5
  75. package/dist/src/commands/sync-app-settings.js.map +1 -1
  76. package/dist/src/commands/sync.d.ts +390 -13
  77. package/dist/src/commands/sync.js +2613 -451
  78. package/dist/src/commands/sync.js.map +1 -1
  79. package/dist/src/commands/tokens.js +2 -1
  80. package/dist/src/commands/tokens.js.map +1 -1
  81. package/dist/src/commands/users.js +19 -10
  82. package/dist/src/commands/users.js.map +1 -1
  83. package/dist/src/commands/vars.js +2 -1
  84. package/dist/src/commands/vars.js.map +1 -1
  85. package/dist/src/commands/waitlist.js +2 -1
  86. package/dist/src/commands/waitlist.js.map +1 -1
  87. package/dist/src/commands/webhooks.d.ts +29 -0
  88. package/dist/src/commands/webhooks.js +135 -64
  89. package/dist/src/commands/webhooks.js.map +1 -1
  90. package/dist/src/commands/workflows.d.ts +1 -29
  91. package/dist/src/commands/workflows.js +113 -179
  92. package/dist/src/commands/workflows.js.map +1 -1
  93. package/dist/src/lib/api-client.d.ts +628 -14
  94. package/dist/src/lib/api-client.js +776 -70
  95. package/dist/src/lib/api-client.js.map +1 -1
  96. package/dist/src/lib/app-settings-descriptor.d.ts +1 -1
  97. package/dist/src/lib/app-settings-descriptor.js +9 -1
  98. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  99. package/dist/src/lib/block-selector.d.ts +58 -0
  100. package/dist/src/lib/block-selector.js +92 -0
  101. package/dist/src/lib/block-selector.js.map +1 -0
  102. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +24 -54
  103. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +31 -153
  104. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  105. package/dist/src/lib/collection-export.d.ts +184 -0
  106. package/dist/src/lib/collection-export.js +252 -0
  107. package/dist/src/lib/collection-export.js.map +1 -0
  108. package/dist/src/lib/config-json-field.d.ts +28 -0
  109. package/dist/src/lib/config-json-field.js +56 -0
  110. package/dist/src/lib/config-json-field.js.map +1 -0
  111. package/dist/src/lib/config-object-descriptor.js +88 -6
  112. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  113. package/dist/src/lib/config-payload.d.ts +8 -1
  114. package/dist/src/lib/config-payload.js +49 -4
  115. package/dist/src/lib/config-payload.js.map +1 -1
  116. package/dist/src/lib/config-surface.d.ts +12 -1
  117. package/dist/src/lib/config-surface.js +71 -3
  118. package/dist/src/lib/config-surface.js.map +1 -1
  119. package/dist/src/lib/config.d.ts +31 -13
  120. package/dist/src/lib/config.js +34 -70
  121. package/dist/src/lib/config.js.map +1 -1
  122. package/dist/src/lib/credentials-store.d.ts +37 -22
  123. package/dist/src/lib/credentials-store.js +103 -97
  124. package/dist/src/lib/credentials-store.js.map +1 -1
  125. package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
  126. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
  127. package/dist/src/lib/deprecation.d.ts +22 -0
  128. package/dist/src/lib/deprecation.js +43 -0
  129. package/dist/src/lib/deprecation.js.map +1 -0
  130. package/dist/src/lib/document-export-permissions.d.ts +30 -0
  131. package/dist/src/lib/document-export-permissions.js +54 -0
  132. package/dist/src/lib/document-export-permissions.js.map +1 -0
  133. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  134. package/dist/src/lib/document-ingest-artifact.js +505 -0
  135. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  136. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  137. package/dist/src/lib/document-ingest-input.js +132 -0
  138. package/dist/src/lib/document-ingest-input.js.map +1 -0
  139. package/dist/src/lib/document-ingest-rows.d.ts +137 -0
  140. package/dist/src/lib/document-ingest-rows.js +181 -0
  141. package/dist/src/lib/document-ingest-rows.js.map +1 -0
  142. package/dist/src/lib/document-ingest.d.ts +101 -0
  143. package/dist/src/lib/document-ingest.js +384 -0
  144. package/dist/src/lib/document-ingest.js.map +1 -0
  145. package/dist/src/lib/env-resolver-core.d.ts +89 -18
  146. package/dist/src/lib/env-resolver-core.js +145 -23
  147. package/dist/src/lib/env-resolver-core.js.map +1 -1
  148. package/dist/src/lib/env-resolver.d.ts +18 -3
  149. package/dist/src/lib/env-resolver.js +23 -3
  150. package/dist/src/lib/env-resolver.js.map +1 -1
  151. package/dist/src/lib/function-bundle.d.ts +147 -0
  152. package/dist/src/lib/function-bundle.js +341 -0
  153. package/dist/src/lib/function-bundle.js.map +1 -0
  154. package/dist/src/lib/function-collect.d.ts +123 -0
  155. package/dist/src/lib/function-collect.js +610 -0
  156. package/dist/src/lib/function-collect.js.map +1 -0
  157. package/dist/src/lib/function-db-types.d.ts +202 -0
  158. package/dist/src/lib/function-db-types.js +870 -0
  159. package/dist/src/lib/function-db-types.js.map +1 -0
  160. package/dist/src/lib/function-document-types.d.ts +144 -0
  161. package/dist/src/lib/function-document-types.js +370 -0
  162. package/dist/src/lib/function-document-types.js.map +1 -0
  163. package/dist/src/lib/function-grants-preflight.d.ts +64 -0
  164. package/dist/src/lib/function-grants-preflight.js +105 -0
  165. package/dist/src/lib/function-grants-preflight.js.map +1 -0
  166. package/dist/src/lib/function-log-lines.d.ts +76 -0
  167. package/dist/src/lib/function-log-lines.js +160 -0
  168. package/dist/src/lib/function-log-lines.js.map +1 -0
  169. package/dist/src/lib/function-log-row.d.ts +29 -0
  170. package/dist/src/lib/function-log-row.js +73 -0
  171. package/dist/src/lib/function-log-row.js.map +1 -0
  172. package/dist/src/lib/function-log-tail.d.ts +132 -0
  173. package/dist/src/lib/function-log-tail.js +262 -0
  174. package/dist/src/lib/function-log-tail.js.map +1 -0
  175. package/dist/src/lib/function-run.d.ts +289 -0
  176. package/dist/src/lib/function-run.js +389 -0
  177. package/dist/src/lib/function-run.js.map +1 -0
  178. package/dist/src/lib/function-schema-codegen.d.ts +143 -0
  179. package/dist/src/lib/function-schema-codegen.js +420 -0
  180. package/dist/src/lib/function-schema-codegen.js.map +1 -0
  181. package/dist/src/lib/function-sync.d.ts +459 -0
  182. package/dist/src/lib/function-sync.js +1258 -0
  183. package/dist/src/lib/function-sync.js.map +1 -0
  184. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  185. package/dist/src/lib/function-trigger-listing.js +70 -0
  186. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  187. package/dist/src/lib/function-typecheck.d.ts +86 -0
  188. package/dist/src/lib/function-typecheck.js +370 -0
  189. package/dist/src/lib/function-typecheck.js.map +1 -0
  190. package/dist/src/lib/function-versions.d.ts +122 -0
  191. package/dist/src/lib/function-versions.js +182 -0
  192. package/dist/src/lib/function-versions.js.map +1 -0
  193. package/dist/src/lib/generated-allowlist.js +4 -0
  194. package/dist/src/lib/generated-allowlist.js.map +1 -1
  195. package/dist/src/lib/generated-config-surfaces.d.ts +2332 -78
  196. package/dist/src/lib/generated-config-surfaces.js +6979 -468
  197. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  198. package/dist/src/lib/generated-sdk-types.d.ts +12 -0
  199. package/dist/src/lib/generated-sdk-types.js +13 -0
  200. package/dist/src/lib/generated-sdk-types.js.map +1 -0
  201. package/dist/src/lib/init-adopt.js +2 -2
  202. package/dist/src/lib/init-adopt.js.map +1 -1
  203. package/dist/src/lib/init-assets.js +3 -3
  204. package/dist/src/lib/init-config.d.ts +1 -1
  205. package/dist/src/lib/init-ios-links.d.ts +6 -5
  206. package/dist/src/lib/init-ios-links.js +9 -8
  207. package/dist/src/lib/init-ios-links.js.map +1 -1
  208. package/dist/src/lib/init-plan.d.ts +1 -1
  209. package/dist/src/lib/init-plan.js +1 -1
  210. package/dist/src/lib/init-xcode.d.ts +6 -5
  211. package/dist/src/lib/init-xcode.js +34 -10
  212. package/dist/src/lib/init-xcode.js.map +1 -1
  213. package/dist/src/lib/ios-app-id.d.ts +34 -0
  214. package/dist/src/lib/ios-app-id.js +69 -0
  215. package/dist/src/lib/ios-app-id.js.map +1 -0
  216. package/dist/src/lib/list-options.d.ts +68 -0
  217. package/dist/src/lib/list-options.js +89 -0
  218. package/dist/src/lib/list-options.js.map +1 -0
  219. package/dist/src/lib/local-state.d.ts +1 -1
  220. package/dist/src/lib/local-state.js +1 -1
  221. package/dist/src/lib/local-test-cases.d.ts +1 -1
  222. package/dist/src/lib/local-test-cases.js +2 -1
  223. package/dist/src/lib/local-test-cases.js.map +1 -1
  224. package/dist/src/lib/log-inspection.d.ts +152 -5
  225. package/dist/src/lib/log-inspection.js +179 -2
  226. package/dist/src/lib/log-inspection.js.map +1 -1
  227. package/dist/src/lib/logout-admin-session.d.ts +33 -0
  228. package/dist/src/lib/logout-admin-session.js +70 -0
  229. package/dist/src/lib/logout-admin-session.js.map +1 -0
  230. package/dist/src/lib/output.d.ts +15 -0
  231. package/dist/src/lib/output.js +28 -0
  232. package/dist/src/lib/output.js.map +1 -1
  233. package/dist/src/lib/paginate.d.ts +15 -0
  234. package/dist/src/lib/paginate.js +17 -0
  235. package/dist/src/lib/paginate.js.map +1 -1
  236. package/dist/src/lib/project-config.d.ts +38 -21
  237. package/dist/src/lib/project-config.js +57 -47
  238. package/dist/src/lib/project-config.js.map +1 -1
  239. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  240. package/dist/src/lib/prompt-cost-format.js +41 -0
  241. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  242. package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
  243. package/dist/src/lib/prompt-schema-codegen.js +462 -0
  244. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  245. package/dist/src/lib/refresh-admin-credentials.d.ts +9 -1
  246. package/dist/src/lib/refresh-admin-credentials.js +22 -2
  247. package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
  248. package/dist/src/lib/resolve-owner.d.ts +19 -0
  249. package/dist/src/lib/resolve-owner.js +20 -0
  250. package/dist/src/lib/resolve-owner.js.map +1 -0
  251. package/dist/src/lib/skill-installer.d.ts +49 -3
  252. package/dist/src/lib/skill-installer.js +275 -100
  253. package/dist/src/lib/skill-installer.js.map +1 -1
  254. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  255. package/dist/src/lib/snapshot-audit-source.js +58 -0
  256. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  257. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  258. package/dist/src/lib/snapshot-audit-store.js +196 -0
  259. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  260. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  261. package/dist/src/lib/snapshot-audit.js +431 -0
  262. package/dist/src/lib/snapshot-audit.js.map +1 -0
  263. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  264. package/dist/src/lib/snapshot-build-rows.js +87 -0
  265. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  266. package/dist/src/lib/snapshot-build.d.ts +50 -0
  267. package/dist/src/lib/snapshot-build.js +111 -0
  268. package/dist/src/lib/snapshot-build.js.map +1 -0
  269. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  270. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  271. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  272. package/dist/src/lib/snapshots.d.ts +8 -9
  273. package/dist/src/lib/snapshots.js +20 -83
  274. package/dist/src/lib/snapshots.js.map +1 -1
  275. package/dist/src/lib/step-run-table.d.ts +43 -0
  276. package/dist/src/lib/step-run-table.js +129 -0
  277. package/dist/src/lib/step-run-table.js.map +1 -0
  278. package/dist/src/lib/storage-pending-retry.d.ts +26 -0
  279. package/dist/src/lib/storage-pending-retry.js +42 -0
  280. package/dist/src/lib/storage-pending-retry.js.map +1 -0
  281. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  282. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  283. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  284. package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
  285. package/dist/src/lib/swift-codegen/banners.js +25 -0
  286. package/dist/src/lib/swift-codegen/banners.js.map +1 -0
  287. package/dist/src/lib/swift-codegen/dbGenerator.js +19 -7
  288. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  289. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
  290. package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
  291. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
  292. package/dist/src/lib/swift-codegen/generator.d.ts +7 -1
  293. package/dist/src/lib/swift-codegen/generator.js +28 -11
  294. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  295. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +15 -0
  296. package/dist/src/lib/swift-codegen/schemaToSwift.js +21 -4
  297. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
  298. package/dist/src/lib/sync-paths.d.ts +72 -55
  299. package/dist/src/lib/sync-paths.js +97 -100
  300. package/dist/src/lib/sync-paths.js.map +1 -1
  301. package/dist/src/lib/sync-resource-types.d.ts +23 -4
  302. package/dist/src/lib/sync-resource-types.js +106 -8
  303. package/dist/src/lib/sync-resource-types.js.map +1 -1
  304. package/dist/src/lib/sync-selectors.d.ts +45 -2
  305. package/dist/src/lib/sync-selectors.js +66 -5
  306. package/dist/src/lib/sync-selectors.js.map +1 -1
  307. package/dist/src/lib/template.d.ts +2 -2
  308. package/dist/src/lib/template.js +2 -2
  309. package/dist/src/lib/test-case-keys.d.ts +1 -1
  310. package/dist/src/lib/test-case-keys.js +1 -1
  311. package/dist/src/lib/test-case-variables.d.ts +14 -0
  312. package/dist/src/lib/test-case-variables.js +42 -0
  313. package/dist/src/lib/test-case-variables.js.map +1 -1
  314. package/dist/src/lib/toml-database-config.js +19 -2
  315. package/dist/src/lib/toml-database-config.js.map +1 -1
  316. package/dist/src/lib/webhook-deliver.d.ts +209 -0
  317. package/dist/src/lib/webhook-deliver.js +519 -0
  318. package/dist/src/lib/webhook-deliver.js.map +1 -0
  319. package/dist/src/lib/workflow-codegen/generator.d.ts +1 -1
  320. package/dist/src/lib/workflow-codegen/generator.js +1 -1
  321. package/dist/src/lib/workflow-toml-validator.d.ts +17 -8
  322. package/dist/src/lib/workflow-toml-validator.js +25 -12
  323. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  324. package/dist/src/lib/workflow-usage.d.ts +196 -0
  325. package/dist/src/lib/workflow-usage.js +310 -0
  326. package/dist/src/lib/workflow-usage.js.map +1 -0
  327. package/dist/src/types/index.d.ts +12 -2
  328. package/package.json +5 -4
@@ -1,7 +1,16 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
+ import { getCurrentEnvNameSafe } from "./env-resolver.js";
2
3
  import { fetchWithTLS } from "./fetch.js";
3
4
  import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
4
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;
5
14
  export class ApiError extends Error {
6
15
  statusCode;
7
16
  code;
@@ -40,6 +49,24 @@ export class ConflictError extends ApiError {
40
49
  this.name = "ConflictError";
41
50
  }
42
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
+ }
43
70
  /**
44
71
  * Extract a human-readable error message + structured fields from a non-OK
45
72
  * HTTP response body. Single source of truth used by every error-handler call
@@ -202,16 +229,38 @@ export function normalizeDatabaseOperationResult(raw) {
202
229
  return envelope;
203
230
  }
204
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
+ */
205
249
  credentials = null;
206
- constructor() {
207
- this.credentials = loadCredentials();
208
- }
209
250
  async ensureAuthenticated() {
210
251
  if (!this.credentials) {
211
252
  this.credentials = loadCredentials();
212
253
  }
213
254
  if (!this.credentials) {
214
- 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);
215
264
  }
216
265
  // Check if token needs refresh
217
266
  if (isTokenExpiringSoon(this.credentials)) {
@@ -235,7 +284,10 @@ export class ApiClient {
235
284
  // error or a server-side rejection. Callers that need a finer
236
285
  // distinction (e.g. `primitive token`) consume RefreshError directly
237
286
  // from refresh-admin-credentials.ts instead of going through here.
238
- throw new ApiError("Token refresh failed. Please login again.", 401);
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);
239
291
  }
240
292
  throw err;
241
293
  }
@@ -342,26 +394,79 @@ export class ApiClient {
342
394
  // ============================================
343
395
  // APPS
344
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
+ */
345
430
  async listApps() {
346
- // The server paginates cursor-style (default pageSize=25, cap=100). Historically
347
- // this method issued a single un-paginated GET and silently truncated to the
348
- // first page, which broke `primitive use <app-id>` and `primitive apps list`
349
- // for admins with more than 25 apps (see #436). Iterate every page.
350
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) => {
351
454
  const qs = new URLSearchParams({ limit: "100" });
352
455
  if (cursor)
353
456
  qs.set("cursor", cursor);
354
- const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
355
- // The server now emits `items` with `apps` as a deprecation-window
356
- // alias (#1316). Read `items` first so listings keep working once the
357
- // alias is removed; fall back to the legacy `apps` key meanwhile.
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 : []);
358
461
  return {
359
- items: resp.items ??
360
- (Array.isArray(resp.apps) ? resp.apps : []),
462
+ items: rows.map((row) => ({
463
+ appId: String(row.appId ?? row.id ?? ""),
464
+ name: String(row.name ?? ""),
465
+ })),
361
466
  nextCursor: resp.nextCursor ?? resp.cursor,
362
467
  };
363
468
  });
364
- return { apps };
469
+ return drainAppsUntilConverged(drainOnce);
365
470
  }
366
471
  async createApp(data) {
367
472
  return this.post("/admin/api/apps", data);
@@ -411,9 +516,16 @@ export class ApiClient {
411
516
  const result = await this.get(path);
412
517
  return {
413
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,
414
522
  nextCursor: result?.nextCursor ?? null,
415
523
  };
416
524
  }
525
+ /**
526
+ * @deprecated Use {@link setUserAvailability} with `"disable"`
527
+ * (`PUT /admin/api/apps/:appId/users/:userId/disable`) instead (#518).
528
+ */
417
529
  async removeUser(appId, userId) {
418
530
  return this.delete(`/app/${appId}/api/users/${userId}`);
419
531
  }
@@ -521,8 +633,12 @@ export class ApiClient {
521
633
  // ============================================
522
634
  async listDeferredGrants(appId, params) {
523
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 ?? [];
524
639
  return {
525
- grants: result?.grants ?? result?.items ?? [],
640
+ items,
641
+ grants: items,
526
642
  nextCursor: result?.nextCursor ?? null,
527
643
  hasMore: result?.hasMore,
528
644
  };
@@ -537,6 +653,7 @@ export class ApiClient {
537
653
  const result = await this.get(`/admin/api/apps/${appId}/integrations`, params);
538
654
  return {
539
655
  items: result?.items ?? [],
656
+ hasMore: result?.hasMore,
540
657
  nextCursor: result?.nextCursor ?? null,
541
658
  };
542
659
  }
@@ -647,6 +764,7 @@ export class ApiClient {
647
764
  const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
648
765
  return {
649
766
  items: result?.items ?? [],
767
+ hasMore: result?.hasMore,
650
768
  nextCursor: result?.nextCursor ?? null,
651
769
  };
652
770
  }
@@ -678,6 +796,217 @@ export class ApiClient {
678
796
  async setWebhookAvailability(appId, webhookId, action) {
679
797
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
680
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
+ }
681
1010
  /** #2803 — the operational verb pair for a prompt (admin API). */
682
1011
  async setPromptAvailability(appId, promptId, action) {
683
1012
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
@@ -717,6 +1046,7 @@ export class ApiClient {
717
1046
  });
718
1047
  return {
719
1048
  items: result?.items ?? [],
1049
+ hasMore: result?.hasMore,
720
1050
  nextCursor: result?.nextCursor ?? null,
721
1051
  };
722
1052
  }
@@ -727,9 +1057,26 @@ export class ApiClient {
727
1057
  });
728
1058
  return {
729
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,
730
1073
  nextCursor: result?.nextCursor ?? null,
731
1074
  };
732
1075
  }
1076
+ /** Revoke an admin session (idempotent). */
1077
+ async revokeAdminSession(sessionId) {
1078
+ return this.delete(`/admin/api/auth/sessions/${encodeURIComponent(sessionId)}`);
1079
+ }
733
1080
  async testWebhook(appId, webhookId, payload) {
734
1081
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
735
1082
  }
@@ -754,8 +1101,16 @@ export class ApiClient {
754
1101
  async getLockStatus(appId, key) {
755
1102
  return this.post(`/app/${appId}/api/locks/status`, { key });
756
1103
  }
757
- async acquireLock(appId, key, ttlMs) {
758
- return this.post(`/app/${appId}/api/locks/acquire`, { key, ttlMs });
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
+ });
759
1114
  }
760
1115
  async releaseLock(appId, key, handleId) {
761
1116
  return this.post(`/app/${appId}/api/locks/release`, {
@@ -862,6 +1217,7 @@ export class ApiClient {
862
1217
  const result = await this.get(`/admin/api/apps/${appId}/prompts`, params);
863
1218
  return {
864
1219
  items: result?.items ?? [],
1220
+ hasMore: result?.hasMore,
865
1221
  nextCursor: result?.nextCursor ?? null,
866
1222
  };
867
1223
  }
@@ -1009,6 +1365,7 @@ export class ApiClient {
1009
1365
  const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases${suffix}`);
1010
1366
  return {
1011
1367
  items: result?.items ?? result ?? [],
1368
+ hasMore: result?.hasMore,
1012
1369
  nextCursor: result?.nextCursor ?? undefined,
1013
1370
  };
1014
1371
  }
@@ -1160,12 +1517,23 @@ export class ApiClient {
1160
1517
  const result = await this.get(`/admin/api/apps/${appId}/workflows`, params);
1161
1518
  return {
1162
1519
  items: result?.items ?? [],
1520
+ hasMore: result?.hasMore,
1163
1521
  nextCursor: result?.nextCursor ?? null,
1164
1522
  };
1165
1523
  }
1166
1524
  async getWorkflow(appId, workflowId) {
1167
1525
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}`);
1168
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
+ }
1169
1537
  async createWorkflow(appId, payload) {
1170
1538
  return this.post(`/admin/api/apps/${appId}/workflows`, payload);
1171
1539
  }
@@ -1327,12 +1695,27 @@ export class ApiClient {
1327
1695
  // ============================================
1328
1696
  // SUPER ADMIN: ADMINS
1329
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
+ }
1330
1708
  async listAllAdmins(params) {
1331
1709
  const result = await this.get("/admin/api/admins", params);
1332
1710
  // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1333
1711
  const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1712
+ const items = result?.items ?? result?.admins ?? [];
1334
1713
  return {
1335
- admins: result?.admins ?? result?.items ?? [],
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,
1336
1719
  nextCursor,
1337
1720
  cursor: nextCursor,
1338
1721
  };
@@ -1367,6 +1750,7 @@ export class ApiClient {
1367
1750
  const result = await this.get("/admin/api/catalog/prompts", params);
1368
1751
  return {
1369
1752
  items: result?.items ?? [],
1753
+ hasMore: result?.hasMore,
1370
1754
  nextCursor: result?.nextCursor ?? null,
1371
1755
  };
1372
1756
  }
@@ -1386,6 +1770,7 @@ export class ApiClient {
1386
1770
  const result = await this.get("/admin/api/catalog/integrations", params);
1387
1771
  return {
1388
1772
  items: result?.items ?? [],
1773
+ hasMore: result?.hasMore,
1389
1774
  nextCursor: result?.nextCursor ?? null,
1390
1775
  };
1391
1776
  }
@@ -1477,30 +1862,42 @@ export class ApiClient {
1477
1862
  * `options.owner` narrows the result to the databases that user created; the
1478
1863
  * server resolves it through the `databasesByCreator` index (#1965).
1479
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
+ */
1480
1872
  async listDatabases(appId, options) {
1481
- return paginateAll(async (cursor) => {
1482
- const params = new URLSearchParams();
1483
- if (options?.owner)
1484
- params.set("owner", options.owner);
1485
- if (cursor)
1486
- params.set("cursor", cursor);
1487
- const qs = params.toString() ? `?${params.toString()}` : "";
1488
- const resp = await this.get(`/app/${appId}/api/databases${qs}`);
1489
- // Tolerant read: a server older than #1958 answers with a bare array.
1490
- // Without this a published CLI pointed at a not-yet-upgraded server
1491
- // would print nothing at all. Such a server also predates the `owner`
1492
- // filter (#1965) and ignores the unknown query parameter, so apply the
1493
- // filter here — the legacy rows carry `createdBy`, so this reproduces
1494
- // the server's own post-join filter (#2245).
1495
- if (Array.isArray(resp)) {
1496
- return {
1497
- items: options?.owner
1498
- ? resp.filter((db) => db?.createdBy === options.owner)
1499
- : resp,
1500
- };
1501
- }
1502
- return { items: resp?.items ?? [], nextCursor: resp?.nextCursor };
1503
- });
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
+ };
1504
1901
  }
1505
1902
  async createDatabase(appId, data) {
1506
1903
  return this.post(`/app/${appId}/api/databases`, data);
@@ -1519,19 +1916,35 @@ export class ApiClient {
1519
1916
  *
1520
1917
  * The HTTP path stays `/metadata` because the wire field name is still
1521
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).
1522
1922
  */
1523
1923
  async getDatabaseCelContext(appId, databaseId) {
1524
1924
  return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1525
1925
  }
1526
- /** Update a database's CEL context dict (merge with existing). */
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
+ */
1527
1932
  async updateDatabaseCelContext(appId, databaseId, celContext) {
1528
1933
  return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1529
1934
  }
1530
- /** @deprecated Use {@link getDatabaseCelContext} instead. */
1935
+ /**
1936
+ * @deprecated Database CEL context is deprecated; use resource metadata
1937
+ * categories instead (`primitive metadata get database <id> <category>`,
1938
+ * #1420, #1815).
1939
+ */
1531
1940
  async getDatabaseMetadata(appId, databaseId) {
1532
1941
  return this.getDatabaseCelContext(appId, databaseId);
1533
1942
  }
1534
- /** @deprecated Use {@link updateDatabaseCelContext} instead. */
1943
+ /**
1944
+ * @deprecated Database CEL context is deprecated; use resource metadata
1945
+ * categories instead (`primitive metadata set database <id> <category>`,
1946
+ * #1420, #1815).
1947
+ */
1535
1948
  async updateDatabaseMetadata(appId, databaseId, metadata) {
1536
1949
  return this.updateDatabaseCelContext(appId, databaseId, metadata);
1537
1950
  }
@@ -1884,9 +2297,20 @@ export class ApiClient {
1884
2297
  // ============================================
1885
2298
  // GROUP MEMBERS
1886
2299
  // ============================================
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
+ */
1887
2308
  async listGroupMembers(appId, groupType, groupId, options) {
1888
- const qs = options?.include ? `?include=${options.include}` : "";
1889
- const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
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
+ });
1890
2314
  // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1891
2315
  // so the CLI keeps paginating against pre-#1316 servers.
1892
2316
  const nextCursor = result?.nextCursor ?? result?.cursor;
@@ -1940,6 +2364,7 @@ export class ApiClient {
1940
2364
  const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1941
2365
  return {
1942
2366
  items: result?.items ?? [],
2367
+ hasMore: result?.hasMore,
1943
2368
  nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1944
2369
  };
1945
2370
  }
@@ -1953,6 +2378,7 @@ export class ApiClient {
1953
2378
  const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1954
2379
  return {
1955
2380
  items: result?.items ?? [],
2381
+ hasMore: result?.hasMore,
1956
2382
  nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1957
2383
  };
1958
2384
  }
@@ -1981,6 +2407,7 @@ export class ApiClient {
1981
2407
  const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1982
2408
  return {
1983
2409
  items: result?.items ?? [],
2410
+ hasMore: result?.hasMore,
1984
2411
  nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1985
2412
  };
1986
2413
  }
@@ -2133,9 +2560,82 @@ export class ApiClient {
2133
2560
  async exportDocumentState(appId, documentId) {
2134
2561
  return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
2135
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
+ }
2136
2591
  async importDocumentState(appId, documentId, stateBase64) {
2137
2592
  return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2138
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
+ }
2139
2639
  async getDocument(appId, documentId) {
2140
2640
  return this.get(`/app/${appId}/api/documents/${documentId}`);
2141
2641
  }
@@ -2157,6 +2657,18 @@ export class ApiClient {
2157
2657
  async getDocumentSchema(appId, documentId) {
2158
2658
  return this.get(`/app/${appId}/api/documents/${documentId}/schema`);
2159
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
+ }
2160
2672
  /**
2161
2673
  * Query records in a document model (issue #1964 Phase 3). Reuses the
2162
2674
  * server-side `GET documents/:id/records/:model` endpoint, which runs
@@ -2171,6 +2683,9 @@ export class ApiClient {
2171
2683
  const query = {};
2172
2684
  if (queryOptions?.filter)
2173
2685
  query.filter = JSON.stringify(queryOptions.filter);
2686
+ if (queryOptions?.documentFormat !== undefined) {
2687
+ query.documentFormat = queryOptions.documentFormat;
2688
+ }
2174
2689
  // Forward `limit` whenever the caller supplied one (including an invalid
2175
2690
  // 0/negative) so the server validates and returns a clean 400 rather than
2176
2691
  // silently applying the default.
@@ -2188,6 +2703,9 @@ export class ApiClient {
2188
2703
  const query = {};
2189
2704
  if (queryOptions?.filter)
2190
2705
  query.filter = JSON.stringify(queryOptions.filter);
2706
+ if (queryOptions?.documentFormat !== undefined) {
2707
+ query.documentFormat = queryOptions.documentFormat;
2708
+ }
2191
2709
  return this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/count`, query);
2192
2710
  }
2193
2711
  /**
@@ -2200,8 +2718,9 @@ export class ApiClient {
2200
2718
  * `{ field, contains }` membership grouping are database-only, which is why
2201
2719
  * this signature is narrower than the databases one.
2202
2720
  */
2203
- async aggregateDocumentRecords(appId, documentId, modelName, options) {
2204
- return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate`, { options });
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 });
2205
2724
  }
2206
2725
  /**
2207
2726
  * Fetch platform-vocabulary document statistics (issue #1964 Phase 3):
@@ -2210,6 +2729,111 @@ export class ApiClient {
2210
2729
  async getDocumentStats(appId, documentId) {
2211
2730
  return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
2212
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
+ }
2213
2837
  /**
2214
2838
  * Create or replace a record in a document model (issue #1964 Phase 4).
2215
2839
  * `POST documents/:id/records/:model` with `{ id, data, options? }` —
@@ -2217,24 +2841,27 @@ export class ApiClient {
2217
2841
  * broadcasts to connected clients). Requires read-write+ on the document
2218
2842
  * or the app-admin arm. Returns `{ record }`.
2219
2843
  */
2220
- async saveDocumentRecord(appId, documentId, modelName, body) {
2221
- return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, body);
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);
2222
2847
  }
2223
2848
  /**
2224
2849
  * Merge fields into an existing record (issue #1964 Phase 4).
2225
2850
  * `PATCH documents/:id/records/:model/:recordId` with `{ data, options? }`.
2226
2851
  * Returns `{ record }`.
2227
2852
  */
2228
- async patchDocumentRecord(appId, documentId, modelName, recordId, body) {
2229
- return this.patch(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`, body);
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);
2230
2856
  }
2231
2857
  /**
2232
2858
  * Delete a record from a document model (issue #1964 Phase 4).
2233
2859
  * `DELETE documents/:id/records/:model/:recordId`. A missing record id is
2234
2860
  * a silent no-op per the facade contract. Returns `{ deleted: true }`.
2235
2861
  */
2236
- async deleteDocumentRecord(appId, documentId, modelName, recordId) {
2237
- return this.delete(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`);
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));
2238
2865
  }
2239
2866
  /**
2240
2867
  * Apply an ordered multi-model create/patch/delete blob atomically (issue
@@ -2242,8 +2869,9 @@ export class ApiClient {
2242
2869
  * a validation failure commits nothing. Returns the facade's
2243
2870
  * `{ applied, added, updated, deleted }`.
2244
2871
  */
2245
- async bulkDocumentRecords(appId, documentId, operations) {
2246
- return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk`, { operations });
2872
+ async bulkDocumentRecords(appId, documentId, operations, extra) {
2873
+ return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk` +
2874
+ this.documentFormatQuery(extra?.documentFormat), { operations });
2247
2875
  }
2248
2876
  async createDocument(appId, data) {
2249
2877
  return this.post(`/app/${appId}/api/documents`, data);
@@ -2272,6 +2900,16 @@ export class ApiClient {
2272
2900
  }
2273
2901
  return this.delete(`/app/${appId}/api/documents/${documentId}/permissions/${target.userId}`);
2274
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
+ }
2275
2913
  async getDocumentLinkAccess(appId, documentId) {
2276
2914
  return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
2277
2915
  }
@@ -2281,8 +2919,14 @@ export class ApiClient {
2281
2919
  async clearDocumentLinkAccess(appId, documentId) {
2282
2920
  return this.delete(`/app/${appId}/api/documents/${documentId}/link-access`);
2283
2921
  }
2284
- async listDocumentInvitations(appId, documentId) {
2285
- return this.get(`/app/${appId}/api/documents/${documentId}/invitations`);
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`);
2286
2930
  }
2287
2931
  async listDocumentBlobs(appId, documentId) {
2288
2932
  return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
@@ -2298,7 +2942,7 @@ export class ApiClient {
2298
2942
  }
2299
2943
  const response = await fetchWithTLS(url, { headers });
2300
2944
  if (!response.ok) {
2301
- throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
2945
+ throw await blobFailure(response, "Failed to download blob: ");
2302
2946
  }
2303
2947
  const arrayBuffer = await response.arrayBuffer();
2304
2948
  return Buffer.from(arrayBuffer);
@@ -2322,8 +2966,7 @@ export class ApiClient {
2322
2966
  body: data,
2323
2967
  });
2324
2968
  if (!response.ok) {
2325
- const text = await response.text();
2326
- throw new ApiError(`Failed to upload blob: ${text}`, response.status);
2969
+ throw await blobFailure(response, "Failed to upload blob: ");
2327
2970
  }
2328
2971
  return response.json();
2329
2972
  }
@@ -2348,9 +2991,75 @@ export class ApiClient {
2348
2991
  async lookupUserByEmail(appId, email) {
2349
2992
  return this.get(`/app/${appId}/api/users/lookup`, { email });
2350
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
+ */
2351
3009
  async listAdminDocuments(appId, userId) {
2352
- const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
2353
- return result?.documents || result || [];
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`, {});
2354
3063
  }
2355
3064
  // ============================================
2356
3065
  // DATABASE EXPORT / IMPORT
@@ -2436,8 +3145,7 @@ export class ApiClient {
2436
3145
  body: data,
2437
3146
  });
2438
3147
  if (!response.ok) {
2439
- const text = await response.text();
2440
- throw new ApiError(`Failed to upload blob: ${text}`, response.status);
3148
+ throw await blobFailure(response, "Failed to upload blob: ");
2441
3149
  }
2442
3150
  return response.json();
2443
3151
  }
@@ -2452,7 +3160,7 @@ export class ApiClient {
2452
3160
  }
2453
3161
  const response = await fetchWithTLS(url, { headers });
2454
3162
  if (!response.ok) {
2455
- throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
3163
+ throw await blobFailure(response, "Failed to download blob: ");
2456
3164
  }
2457
3165
  const arrayBuffer = await response.arrayBuffer();
2458
3166
  return Buffer.from(arrayBuffer);
@@ -2476,6 +3184,4 @@ export class ApiClient {
2476
3184
  return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
2477
3185
  }
2478
3186
  }
2479
- // Export a singleton instance
2480
- export const apiClient = new ApiClient();
2481
3187
  //# sourceMappingURL=api-client.js.map