primitive-admin 1.2.0-alpha.1 → 1.2.0-alpha.10

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 (410) hide show
  1. package/README.md +105 -134
  2. package/assets/skill/skills/primitive-platform/SKILL.md +37 -4
  3. package/dist/bin/primitive.js +30 -35
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/agent-sessions.d.ts +133 -0
  6. package/dist/src/commands/agent-sessions.js +792 -0
  7. package/dist/src/commands/agent-sessions.js.map +1 -0
  8. package/dist/src/commands/analytics.js +0 -148
  9. package/dist/src/commands/analytics.js.map +1 -1
  10. package/dist/src/commands/apps-children.d.ts +54 -0
  11. package/dist/src/commands/apps-children.js +631 -0
  12. package/dist/src/commands/apps-children.js.map +1 -0
  13. package/dist/src/commands/apps.d.ts +6 -0
  14. package/dist/src/commands/apps.js +131 -9
  15. package/dist/src/commands/apps.js.map +1 -1
  16. package/dist/src/commands/auth-sessions.d.ts +3 -3
  17. package/dist/src/commands/auth-sessions.js +117 -23
  18. package/dist/src/commands/auth-sessions.js.map +1 -1
  19. package/dist/src/commands/auth.js +460 -36
  20. package/dist/src/commands/auth.js.map +1 -1
  21. package/dist/src/commands/collections.js +316 -256
  22. package/dist/src/commands/collections.js.map +1 -1
  23. package/dist/src/commands/config.d.ts +1 -1
  24. package/dist/src/commands/config.js +60 -28
  25. package/dist/src/commands/config.js.map +1 -1
  26. package/dist/src/commands/connections.js +11 -12
  27. package/dist/src/commands/connections.js.map +1 -1
  28. package/dist/src/commands/database-type-configs.js +2 -76
  29. package/dist/src/commands/database-type-configs.js.map +1 -1
  30. package/dist/src/commands/databases.d.ts +9 -0
  31. package/dist/src/commands/databases.js +221 -457
  32. package/dist/src/commands/databases.js.map +1 -1
  33. package/dist/src/commands/db3-types.d.ts +15 -0
  34. package/dist/src/commands/db3-types.js +157 -0
  35. package/dist/src/commands/db3-types.js.map +1 -0
  36. package/dist/src/commands/db3.d.ts +12 -0
  37. package/dist/src/commands/db3.js +1052 -0
  38. package/dist/src/commands/db3.js.map +1 -0
  39. package/dist/src/commands/documents.d.ts +121 -2
  40. package/dist/src/commands/documents.js +1601 -1251
  41. package/dist/src/commands/documents.js.map +1 -1
  42. package/dist/src/commands/env.d.ts +9 -1
  43. package/dist/src/commands/env.js +164 -49
  44. package/dist/src/commands/env.js.map +1 -1
  45. package/dist/src/commands/function-trigger-commands.d.ts +28 -0
  46. package/dist/src/commands/function-trigger-commands.js +341 -0
  47. package/dist/src/commands/function-trigger-commands.js.map +1 -0
  48. package/dist/src/commands/functions.js +297 -24
  49. package/dist/src/commands/functions.js.map +1 -1
  50. package/dist/src/commands/groups.js +150 -49
  51. package/dist/src/commands/groups.js.map +1 -1
  52. package/dist/src/commands/guides.js +5 -5
  53. package/dist/src/commands/init.d.ts +7 -0
  54. package/dist/src/commands/init.js +23 -5
  55. package/dist/src/commands/init.js.map +1 -1
  56. package/dist/src/commands/integrations.js +14 -8
  57. package/dist/src/commands/integrations.js.map +1 -1
  58. package/dist/src/commands/metadata-category-configs.d.ts +2 -2
  59. package/dist/src/commands/metadata-category-configs.js +2 -2
  60. package/dist/src/commands/prompts.d.ts +7 -0
  61. package/dist/src/commands/prompts.js +48 -5
  62. package/dist/src/commands/prompts.js.map +1 -1
  63. package/dist/src/commands/scripts.d.ts +9 -17
  64. package/dist/src/commands/scripts.js +24 -479
  65. package/dist/src/commands/scripts.js.map +1 -1
  66. package/dist/src/commands/sync-app-settings.d.ts +36 -8
  67. package/dist/src/commands/sync-app-settings.js +109 -23
  68. package/dist/src/commands/sync-app-settings.js.map +1 -1
  69. package/dist/src/commands/sync.d.ts +360 -610
  70. package/dist/src/commands/sync.js +3151 -6697
  71. package/dist/src/commands/sync.js.map +1 -1
  72. package/dist/src/commands/tokens.js +3 -3
  73. package/dist/src/commands/tokens.js.map +1 -1
  74. package/dist/src/commands/users.js +77 -2
  75. package/dist/src/commands/users.js.map +1 -1
  76. package/dist/src/commands/workflows.d.ts +5 -83
  77. package/dist/src/commands/workflows.js +57 -2282
  78. package/dist/src/commands/workflows.js.map +1 -1
  79. package/dist/src/lib/aggregate-output.d.ts +25 -0
  80. package/dist/src/lib/aggregate-output.js +53 -0
  81. package/dist/src/lib/aggregate-output.js.map +1 -0
  82. package/dist/src/lib/api-client.d.ts +681 -551
  83. package/dist/src/lib/api-client.js +880 -814
  84. package/dist/src/lib/api-client.js.map +1 -1
  85. package/dist/src/lib/app-settings-descriptor.d.ts +47 -1
  86. package/dist/src/lib/app-settings-descriptor.js +114 -10
  87. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  88. package/dist/src/lib/auth-fixtures.d.ts +44 -0
  89. package/dist/src/lib/auth-fixtures.js +61 -0
  90. package/dist/src/lib/auth-fixtures.js.map +1 -0
  91. package/dist/src/lib/auth-flow.d.ts +41 -1
  92. package/dist/src/lib/auth-flow.js +70 -16
  93. package/dist/src/lib/auth-flow.js.map +1 -1
  94. package/dist/src/lib/block-layout.d.ts +12 -23
  95. package/dist/src/lib/block-layout.js +26 -92
  96. package/dist/src/lib/block-layout.js.map +1 -1
  97. package/dist/src/lib/block-selector.d.ts +2 -2
  98. package/dist/src/lib/block-selector.js +2 -2
  99. package/dist/src/lib/child-apps-local.d.ts +164 -0
  100. package/dist/src/lib/child-apps-local.js +258 -0
  101. package/dist/src/lib/child-apps-local.js.map +1 -0
  102. package/dist/src/lib/ci-session-request.d.ts +22 -0
  103. package/dist/src/lib/ci-session-request.js +51 -0
  104. package/dist/src/lib/ci-session-request.js.map +1 -0
  105. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +10 -8
  106. package/dist/src/lib/codegen-shared/generatedFiles.js +21 -15
  107. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  108. package/dist/src/lib/codegen-shared/prettierStable.d.ts +1 -1
  109. package/dist/src/lib/codegen-shared/prettierStable.js +1 -1
  110. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +2 -2
  111. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +2 -2
  112. package/dist/src/lib/collection-export.d.ts +76 -15
  113. package/dist/src/lib/collection-export.js +116 -23
  114. package/dist/src/lib/collection-export.js.map +1 -1
  115. package/dist/src/lib/collection-principal.d.ts +22 -0
  116. package/dist/src/lib/collection-principal.js +35 -0
  117. package/dist/src/lib/collection-principal.js.map +1 -0
  118. package/dist/src/lib/config-object-descriptor.d.ts +3 -22
  119. package/dist/src/lib/config-object-descriptor.js +56 -290
  120. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  121. package/dist/src/lib/config-payload.js +2 -3
  122. package/dist/src/lib/config-payload.js.map +1 -1
  123. package/dist/src/lib/config-surface.d.ts +5 -7
  124. package/dist/src/lib/config-surface.js +18 -40
  125. package/dist/src/lib/config-surface.js.map +1 -1
  126. package/dist/src/lib/connections-list.d.ts +10 -0
  127. package/dist/src/lib/connections-list.js +18 -0
  128. package/dist/src/lib/connections-list.js.map +1 -0
  129. package/dist/src/lib/credentials-home.d.ts +58 -0
  130. package/dist/src/lib/credentials-home.js +204 -0
  131. package/dist/src/lib/credentials-home.js.map +1 -0
  132. package/dist/src/lib/credentials-lock.d.ts +88 -0
  133. package/dist/src/lib/credentials-lock.js +302 -0
  134. package/dist/src/lib/credentials-lock.js.map +1 -0
  135. package/dist/src/lib/credentials-store.d.ts +179 -24
  136. package/dist/src/lib/credentials-store.js +712 -77
  137. package/dist/src/lib/credentials-store.js.map +1 -1
  138. package/dist/src/lib/csv.d.ts +8 -9
  139. package/dist/src/lib/csv.js +3 -4
  140. package/dist/src/lib/csv.js.map +1 -1
  141. package/dist/src/lib/custodian.d.ts +8 -0
  142. package/dist/src/lib/custodian.js +18 -0
  143. package/dist/src/lib/custodian.js.map +1 -0
  144. package/dist/src/lib/db-codegen/dbGenerator.js +5 -16
  145. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
  146. package/dist/src/lib/db-codegen/dbNaming.d.ts +5 -13
  147. package/dist/src/lib/db-codegen/dbNaming.js +5 -17
  148. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
  149. package/dist/src/lib/db-codegen/dbTemplates.d.ts +7 -56
  150. package/dist/src/lib/db-codegen/dbTemplates.js +25 -125
  151. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  152. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +2 -2
  153. package/dist/src/lib/db-codegen/dbTsTypes.js +2 -2
  154. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +67 -2
  155. package/dist/src/lib/db-codegen/dbTypeIR.js +13 -2
  156. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
  157. package/dist/src/lib/db3-export.d.ts +231 -0
  158. package/dist/src/lib/db3-export.js +510 -0
  159. package/dist/src/lib/db3-export.js.map +1 -0
  160. package/dist/src/lib/db3-index-output.d.ts +27 -0
  161. package/dist/src/lib/db3-index-output.js +49 -0
  162. package/dist/src/lib/db3-index-output.js.map +1 -0
  163. package/dist/src/lib/document-export-lookups.d.ts +31 -0
  164. package/dist/src/lib/document-export-lookups.js +53 -0
  165. package/dist/src/lib/document-export-lookups.js.map +1 -0
  166. package/dist/src/lib/document-export-permissions.d.ts +7 -5
  167. package/dist/src/lib/document-export-permissions.js +7 -5
  168. package/dist/src/lib/document-export-permissions.js.map +1 -1
  169. package/dist/src/lib/document-export-record-log.d.ts +57 -0
  170. package/dist/src/lib/document-export-record-log.js +215 -0
  171. package/dist/src/lib/document-export-record-log.js.map +1 -0
  172. package/dist/src/lib/document-ingest-artifact.d.ts +26 -2
  173. package/dist/src/lib/document-ingest-artifact.js +42 -8
  174. package/dist/src/lib/document-ingest-artifact.js.map +1 -1
  175. package/dist/src/lib/document-ingest-format2.d.ts +36 -0
  176. package/dist/src/lib/document-ingest-format2.js +78 -0
  177. package/dist/src/lib/document-ingest-format2.js.map +1 -0
  178. package/dist/src/lib/document-ingest-input.d.ts +9 -0
  179. package/dist/src/lib/document-ingest-input.js +28 -1
  180. package/dist/src/lib/document-ingest-input.js.map +1 -1
  181. package/dist/src/lib/document-ingest-rows.d.ts +16 -0
  182. package/dist/src/lib/document-ingest-rows.js +17 -0
  183. package/dist/src/lib/document-ingest-rows.js.map +1 -1
  184. package/dist/src/lib/document-ingest.d.ts +33 -1
  185. package/dist/src/lib/document-ingest.js +49 -0
  186. package/dist/src/lib/document-ingest.js.map +1 -1
  187. package/dist/src/lib/document-schema-rows.d.ts +42 -0
  188. package/dist/src/lib/document-schema-rows.js +53 -0
  189. package/dist/src/lib/document-schema-rows.js.map +1 -0
  190. package/dist/src/lib/env-resolver-core.d.ts +70 -2
  191. package/dist/src/lib/env-resolver-core.js +137 -20
  192. package/dist/src/lib/env-resolver-core.js.map +1 -1
  193. package/dist/src/lib/env-resolver.d.ts +14 -2
  194. package/dist/src/lib/env-resolver.js +32 -10
  195. package/dist/src/lib/env-resolver.js.map +1 -1
  196. package/dist/src/lib/function-authorize-check.d.ts +142 -0
  197. package/dist/src/lib/function-authorize-check.js +310 -0
  198. package/dist/src/lib/function-authorize-check.js.map +1 -0
  199. package/dist/src/lib/function-authorize-view.d.ts +15 -0
  200. package/dist/src/lib/function-authorize-view.js +50 -0
  201. package/dist/src/lib/function-authorize-view.js.map +1 -0
  202. package/dist/src/lib/function-bundle.d.ts +38 -18
  203. package/dist/src/lib/function-bundle.js +96 -35
  204. package/dist/src/lib/function-bundle.js.map +1 -1
  205. package/dist/src/lib/function-collect.d.ts +75 -12
  206. package/dist/src/lib/function-collect.js +427 -12
  207. package/dist/src/lib/function-collect.js.map +1 -1
  208. package/dist/src/lib/function-db-types.d.ts +29 -0
  209. package/dist/src/lib/function-db-types.js +117 -7
  210. package/dist/src/lib/function-db-types.js.map +1 -1
  211. package/dist/src/lib/function-document-types.d.ts +5 -3
  212. package/dist/src/lib/function-document-types.js +10 -14
  213. package/dist/src/lib/function-document-types.js.map +1 -1
  214. package/dist/src/lib/function-lint.d.ts +34 -0
  215. package/dist/src/lib/function-lint.js +112 -0
  216. package/dist/src/lib/function-lint.js.map +1 -0
  217. package/dist/src/lib/function-log-row.js +21 -2
  218. package/dist/src/lib/function-log-row.js.map +1 -1
  219. package/dist/src/lib/function-run.d.ts +16 -21
  220. package/dist/src/lib/function-run.js +37 -30
  221. package/dist/src/lib/function-run.js.map +1 -1
  222. package/dist/src/lib/function-schema-codegen.d.ts +1 -1
  223. package/dist/src/lib/function-schema-codegen.js +5 -5
  224. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  225. package/dist/src/lib/function-source-roots.d.ts +67 -0
  226. package/dist/src/lib/function-source-roots.js +148 -0
  227. package/dist/src/lib/function-source-roots.js.map +1 -0
  228. package/dist/src/lib/function-sync.d.ts +108 -22
  229. package/dist/src/lib/function-sync.js +127 -39
  230. package/dist/src/lib/function-sync.js.map +1 -1
  231. package/dist/src/lib/function-trigger-listing.d.ts +11 -6
  232. package/dist/src/lib/function-trigger-listing.js +32 -11
  233. package/dist/src/lib/function-trigger-listing.js.map +1 -1
  234. package/dist/src/lib/function-typecheck.d.ts +37 -2
  235. package/dist/src/lib/function-typecheck.js +210 -28
  236. package/dist/src/lib/function-typecheck.js.map +1 -1
  237. package/dist/src/lib/function-versions.d.ts +2 -0
  238. package/dist/src/lib/function-versions.js +1 -0
  239. package/dist/src/lib/function-versions.js.map +1 -1
  240. package/dist/src/lib/generated-allowlist.js +0 -20
  241. package/dist/src/lib/generated-allowlist.js.map +1 -1
  242. package/dist/src/lib/generated-config-surfaces.d.ts +703 -203
  243. package/dist/src/lib/generated-config-surfaces.js +2270 -2014
  244. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  245. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  246. package/dist/src/lib/generated-sdk-types.js +1 -1
  247. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  248. package/dist/src/lib/init-adopt.js +12 -3
  249. package/dist/src/lib/init-adopt.js.map +1 -1
  250. package/dist/src/lib/init-login-note.d.ts +13 -0
  251. package/dist/src/lib/init-login-note.js +22 -0
  252. package/dist/src/lib/init-login-note.js.map +1 -0
  253. package/dist/src/lib/json-value.d.ts +6 -0
  254. package/dist/src/lib/json-value.js +13 -0
  255. package/dist/src/lib/json-value.js.map +1 -0
  256. package/dist/src/lib/load-routes.d.ts +33 -0
  257. package/dist/src/lib/load-routes.js +35 -0
  258. package/dist/src/lib/load-routes.js.map +1 -0
  259. package/dist/src/lib/local-state.d.ts +38 -7
  260. package/dist/src/lib/local-state.js +172 -49
  261. package/dist/src/lib/local-state.js.map +1 -1
  262. package/dist/src/lib/local-test-cases.d.ts +5 -2
  263. package/dist/src/lib/local-test-cases.js +7 -4
  264. package/dist/src/lib/local-test-cases.js.map +1 -1
  265. package/dist/src/lib/log-inspection.d.ts +42 -4
  266. package/dist/src/lib/log-inspection.js +37 -5
  267. package/dist/src/lib/log-inspection.js.map +1 -1
  268. package/dist/src/lib/login-exchange-check.d.ts +18 -0
  269. package/dist/src/lib/login-exchange-check.js +27 -0
  270. package/dist/src/lib/login-exchange-check.js.map +1 -0
  271. package/dist/src/lib/login-scope-lines.d.ts +25 -0
  272. package/dist/src/lib/login-scope-lines.js +45 -0
  273. package/dist/src/lib/login-scope-lines.js.map +1 -0
  274. package/dist/src/lib/logout-admin-session.d.ts +2 -1
  275. package/dist/src/lib/logout-admin-session.js +7 -1
  276. package/dist/src/lib/logout-admin-session.js.map +1 -1
  277. package/dist/src/lib/object-status-filter.d.ts +9 -1
  278. package/dist/src/lib/object-status-filter.js +9 -1
  279. package/dist/src/lib/object-status-filter.js.map +1 -1
  280. package/dist/src/lib/output.js +2 -0
  281. package/dist/src/lib/output.js.map +1 -1
  282. package/dist/src/lib/paginate.d.ts +0 -5
  283. package/dist/src/lib/paginate.js +0 -5
  284. package/dist/src/lib/paginate.js.map +1 -1
  285. package/dist/src/lib/pin-refusal-hint.d.ts +12 -0
  286. package/dist/src/lib/pin-refusal-hint.js +35 -0
  287. package/dist/src/lib/pin-refusal-hint.js.map +1 -0
  288. package/dist/src/lib/primitive-env-plugin.d.ts +99 -0
  289. package/dist/src/lib/primitive-env-plugin.js +408 -0
  290. package/dist/src/lib/primitive-env-plugin.js.map +1 -0
  291. package/dist/src/lib/project-config.d.ts +7 -0
  292. package/dist/src/lib/project-config.js +22 -1
  293. package/dist/src/lib/project-config.js.map +1 -1
  294. package/dist/src/lib/project-login-apps.d.ts +8 -0
  295. package/dist/src/lib/project-login-apps.js +27 -0
  296. package/dist/src/lib/project-login-apps.js.map +1 -0
  297. package/dist/src/lib/prompt-schema-codegen.d.ts +10 -3
  298. package/dist/src/lib/prompt-schema-codegen.js +52 -11
  299. package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
  300. package/dist/src/lib/pull-write.d.ts +31 -0
  301. package/dist/src/lib/pull-write.js +49 -0
  302. package/dist/src/lib/pull-write.js.map +1 -0
  303. package/dist/src/lib/push-failure-report.d.ts +45 -0
  304. package/dist/src/lib/push-failure-report.js +57 -0
  305. package/dist/src/lib/push-failure-report.js.map +1 -0
  306. package/dist/src/lib/records-verbs.d.ts +69 -0
  307. package/dist/src/lib/records-verbs.js +40 -0
  308. package/dist/src/lib/records-verbs.js.map +1 -0
  309. package/dist/src/lib/refresh-admin-credentials.d.ts +7 -0
  310. package/dist/src/lib/refresh-admin-credentials.js +16 -4
  311. package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
  312. package/dist/src/lib/root-import-decision.d.ts +65 -0
  313. package/dist/src/lib/root-import-decision.js +109 -0
  314. package/dist/src/lib/root-import-decision.js.map +1 -0
  315. package/dist/src/lib/run-status.d.ts +1 -1
  316. package/dist/src/lib/run-status.generated.d.ts +2 -2
  317. package/dist/src/lib/run-status.generated.js +3 -1
  318. package/dist/src/lib/run-status.generated.js.map +1 -1
  319. package/dist/src/lib/run-status.js +1 -1
  320. package/dist/src/lib/schema-codegen/generated-schema-descriptor.d.ts +129 -0
  321. package/dist/src/lib/schema-codegen/generated-schema-descriptor.js +269 -0
  322. package/dist/src/lib/schema-codegen/generated-schema-descriptor.js.map +1 -0
  323. package/dist/src/lib/schema-codegen/naming.d.ts +35 -0
  324. package/dist/src/lib/schema-codegen/naming.js +83 -0
  325. package/dist/src/lib/schema-codegen/naming.js.map +1 -0
  326. package/dist/src/lib/schema-codegen/schemaToTs.d.ts +82 -0
  327. package/dist/src/lib/schema-codegen/schemaToTs.js +305 -0
  328. package/dist/src/lib/schema-codegen/schemaToTs.js.map +1 -0
  329. package/dist/src/lib/scope-request.d.ts +84 -0
  330. package/dist/src/lib/scope-request.js +235 -0
  331. package/dist/src/lib/scope-request.js.map +1 -0
  332. package/dist/src/lib/session-id.d.ts +9 -0
  333. package/dist/src/lib/session-id.js +28 -0
  334. package/dist/src/lib/session-id.js.map +1 -0
  335. package/dist/src/lib/snapshot-build-rows.d.ts +21 -0
  336. package/dist/src/lib/snapshot-build-rows.js +22 -0
  337. package/dist/src/lib/snapshot-build-rows.js.map +1 -1
  338. package/dist/src/lib/snapshot-build.js +8 -3
  339. package/dist/src/lib/snapshot-build.js.map +1 -1
  340. package/dist/src/lib/snapshots.d.ts +17 -1
  341. package/dist/src/lib/snapshots.js +50 -2
  342. package/dist/src/lib/snapshots.js.map +1 -1
  343. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +6 -2
  344. package/dist/src/lib/swift-codegen/agentGenerator.js +57 -5
  345. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -1
  346. package/dist/src/lib/swift-codegen/banners.d.ts +34 -7
  347. package/dist/src/lib/swift-codegen/banners.js +37 -7
  348. package/dist/src/lib/swift-codegen/banners.js.map +1 -1
  349. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +4 -3
  350. package/dist/src/lib/swift-codegen/dbGenerator.js +14 -117
  351. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  352. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +3 -3
  353. package/dist/src/lib/swift-codegen/functionGenerator.js +23 -18
  354. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  355. package/dist/src/lib/swift-codegen/generator.d.ts +5 -5
  356. package/dist/src/lib/swift-codegen/generator.js +35 -40
  357. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  358. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +3 -3
  359. package/dist/src/lib/swift-codegen/schemaToSwift.js +4 -4
  360. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
  361. package/dist/src/lib/swift-codegen/swiftKeys.d.ts +19 -0
  362. package/dist/src/lib/swift-codegen/swiftKeys.js +31 -0
  363. package/dist/src/lib/swift-codegen/swiftKeys.js.map +1 -0
  364. package/dist/src/lib/sync-paths.d.ts +35 -0
  365. package/dist/src/lib/sync-paths.js +57 -5
  366. package/dist/src/lib/sync-paths.js.map +1 -1
  367. package/dist/src/lib/sync-resource-types.d.ts +26 -26
  368. package/dist/src/lib/sync-resource-types.js +61 -189
  369. package/dist/src/lib/sync-resource-types.js.map +1 -1
  370. package/dist/src/lib/sync-selectors.d.ts +5 -1
  371. package/dist/src/lib/sync-selectors.js +23 -13
  372. package/dist/src/lib/sync-selectors.js.map +1 -1
  373. package/dist/src/lib/test-case-file-names.d.ts +3 -5
  374. package/dist/src/lib/test-case-file-names.js +3 -5
  375. package/dist/src/lib/test-case-file-names.js.map +1 -1
  376. package/dist/src/lib/toml-database-config.d.ts +15 -75
  377. package/dist/src/lib/toml-database-config.js +17 -306
  378. package/dist/src/lib/toml-database-config.js.map +1 -1
  379. package/dist/src/lib/transient-retry.d.ts +43 -0
  380. package/dist/src/lib/transient-retry.js +94 -0
  381. package/dist/src/lib/transient-retry.js.map +1 -0
  382. package/dist/src/lib/vars-override.d.ts +105 -0
  383. package/dist/src/lib/vars-override.js +143 -0
  384. package/dist/src/lib/vars-override.js.map +1 -0
  385. package/dist/src/lib/webhook-capture.d.ts +43 -0
  386. package/dist/src/lib/webhook-capture.js +91 -0
  387. package/dist/src/lib/webhook-capture.js.map +1 -0
  388. package/dist/src/lib/webhook-deliver.d.ts +18 -20
  389. package/dist/src/lib/webhook-deliver.js +34 -89
  390. package/dist/src/lib/webhook-deliver.js.map +1 -1
  391. package/dist/src/lib/webhook-test-run.d.ts +32 -0
  392. package/dist/src/lib/webhook-test-run.js +108 -0
  393. package/dist/src/lib/webhook-test-run.js.map +1 -0
  394. package/dist/src/lib/workflow-codegen/generator.d.ts +7 -7
  395. package/dist/src/lib/workflow-codegen/generator.js +16 -23
  396. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  397. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +6 -12
  398. package/dist/src/lib/workflow-codegen/invokerIR.js +4 -5
  399. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  400. package/dist/src/lib/workflow-toml-validator.d.ts +2 -0
  401. package/dist/src/lib/workflow-toml-validator.js +28 -1
  402. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  403. package/dist/src/types/index.d.ts +30 -37
  404. package/dist/src/validators.d.ts +7 -55
  405. package/dist/src/validators.js +7 -53
  406. package/dist/src/validators.js.map +1 -1
  407. package/dist/src/vite.d.ts +17 -0
  408. package/dist/src/vite.js +18 -0
  409. package/dist/src/vite.js.map +1 -0
  410. package/package.json +20 -12
@@ -1,9 +1,9 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
- import { getCurrentEnvNameSafe } from "./env-resolver.js";
2
+ import { getCurrentEnvNameSafe, getProjectConfig } from "./env-resolver.js";
3
+ import { pinRefusalHint } from "./pin-refusal-hint.js";
3
4
  import { fetchWithTLS } from "./fetch.js";
4
5
  import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
5
6
  import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
6
- import { drainAppsUntilConverged, } from "./workflow-usage.js";
7
7
  /**
8
8
  * Page size the admin per-user inventories are asked for (#3399).
9
9
  *
@@ -11,6 +11,15 @@ import { drainAppsUntilConverged, } from "./workflow-usage.js";
11
11
  * fewest round trips the contract allows; a larger value is clamped there.
12
12
  */
13
13
  const ADMIN_INVENTORY_PAGE_SIZE = 100;
14
+ /** The agent block's wire keys, in the surface's order. */
15
+ const AGENT_TEST_CASE_WIRE_KEYS = [
16
+ "agentHistory",
17
+ "agentContext",
18
+ "agentTimezone",
19
+ "agentLocale",
20
+ "agentExpectedToolCall",
21
+ "agentExpectedNoToolCall",
22
+ ];
14
23
  export class ApiError extends Error {
15
24
  statusCode;
16
25
  code;
@@ -39,6 +48,15 @@ export class ApiError extends Error {
39
48
  }
40
49
  }
41
50
  }
51
+ /** The project's committed environments, or null outside a readable project. */
52
+ function projectEnvironments() {
53
+ try {
54
+ return getProjectConfig()?.environments ?? null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
42
60
  export class ConflictError extends ApiError {
43
61
  serverModifiedAt;
44
62
  expectedModifiedAt;
@@ -85,6 +103,15 @@ export function parseErrorResponse(response, text, path) {
85
103
  // Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
86
104
  // 404 page from hitting the wrong path) so we don't regress the helpful
87
105
  // "API endpoint not found" message at api-client.ts:343.
106
+ //
107
+ // An HTML page with any other status is not a wrong path: it keeps its
108
+ // status, so a gateway's 502 is retried and a 403 is not read as absence
109
+ // (#4148).
110
+ if (text.includes("<!DOCTYPE") && response.status !== 404) {
111
+ return {
112
+ message: `HTTP ${response.status}: the server answered with an HTML error page`,
113
+ };
114
+ }
88
115
  if (text.includes("<!DOCTYPE")) {
89
116
  const where = path ? `: ${path}` : "";
90
117
  return {
@@ -116,55 +143,15 @@ export function parseErrorResponse(response, text, path) {
116
143
  return { message, code, details, raw: errorData };
117
144
  }
118
145
  /**
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.).
146
+ * Typed exception for the database-schema feature (issue #666): a stored
147
+ * schema that does not parse. `details` carries the line and column.
148
+ * (#3189 — the operation gates' exceptions retired with operations.)
126
149
  */
127
- export class SchemaRequiredError extends ApiError {
128
- constructor(message, details) {
129
- super(message, 422, "SCHEMA_REQUIRED", details);
130
- this.name = "SchemaRequiredError";
131
- }
132
- }
133
150
  function detailsRecord(details) {
134
151
  return details && !Array.isArray(details) && typeof details === "object"
135
152
  ? details
136
153
  : undefined;
137
154
  }
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
155
  export class TomlParseError extends ApiError {
169
156
  constructor(message, details) {
170
157
  super(message, 400, "TOML_PARSE_ERROR", details);
@@ -179,55 +166,6 @@ export class TomlParseError extends ApiError {
179
166
  return typeof d?.column === "number" ? d.column : undefined;
180
167
  }
181
168
  }
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
- }
231
169
  export class ApiClient {
232
170
  /**
233
171
  * Loaded on first use (`ensureAuthenticated`), never in a constructor.
@@ -284,8 +222,9 @@ export class ApiClient {
284
222
  // error or a server-side rejection. Callers that need a finer
285
223
  // distinction (e.g. `primitive token`) consume RefreshError directly
286
224
  // 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"
225
+ // A refusal naming the session says why: revoked (#3885), or a login
226
+ // made before scoped sessions (#3891).
227
+ throw new ApiError(err.code === "SESSION_REVOKED" || err.code === "SESSION_UNSCOPED"
289
228
  ? err.message
290
229
  : "Token refresh failed. Please login again.", 401, err.code);
291
230
  }
@@ -295,60 +234,133 @@ export class ApiClient {
295
234
  async request(path, options = {}) {
296
235
  const credentials = await this.ensureAuthenticated();
297
236
  const url = `${credentials.serverUrl}${path}`;
298
- const headers = {
299
- Authorization: `Bearer ${credentials.accessToken}`,
300
- "Content-Type": "application/json",
301
- ...(options.headers || {}),
302
- };
303
- // Include globalAdminAppId if available (required for admin API calls)
304
- if (credentials.globalAdminAppId) {
305
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
306
- }
307
- const response = await fetchWithTLS(url, {
308
- ...options,
309
- headers,
310
- });
311
- const text = await response.text();
312
- if (!response.ok) {
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);
317
- }
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);
335
- }
336
- if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
337
- throw new SchemaRequiredError(parsed.message, detailsRecord);
237
+ const send = async (accessToken) => {
238
+ const headers = {
239
+ Authorization: `Bearer ${accessToken}`,
240
+ "Content-Type": "application/json",
241
+ ...(options.headers || {}),
242
+ };
243
+ // Include globalAdminAppId if available (required for admin API calls)
244
+ if (credentials.globalAdminAppId) {
245
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
338
246
  }
339
- if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
340
- throw new OperationRefError(parsed.message, detailsRecord);
247
+ const response = await fetchWithTLS(url, {
248
+ ...options,
249
+ headers,
250
+ });
251
+ return { response, text: await response.text() };
252
+ };
253
+ let { response, text } = await send(credentials.accessToken);
254
+ // #3889 — a 403 naming `admin` is retried once with a stored, unexpired
255
+ // step-up, and (#4564) so is a write a protected app refused for want of
256
+ // one. A retry refused 401 means the step-up is gone (revoked or expired
257
+ // early): drop it and show the original refusal. The retry's own refusal
258
+ // (say, a step-up without the scope this command needs) is shown as it is.
259
+ if (response.status === 403 && this.stepUpApplies(response, text, path)) {
260
+ const retried = await send(credentials.stepUp.accessToken);
261
+ if (retried.response.ok) {
262
+ return retried.text ? JSON.parse(retried.text) : null;
341
263
  }
342
- if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
343
- throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
264
+ if (retried.response.status === 401) {
265
+ this.dropStepUp();
344
266
  }
345
- if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
346
- throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
267
+ else {
268
+ ({ response, text } = retried);
347
269
  }
348
- throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
270
+ }
271
+ if (!response.ok) {
272
+ throw this.responseError(response, text, path);
349
273
  }
350
274
  return text ? JSON.parse(text) : null;
351
275
  }
276
+ /**
277
+ * Whether a 403 should be retried with the stored step-up: it names
278
+ * `admin` among the missing scopes, or it is a protected app's refusal of
279
+ * a token that is no step-up, and an unexpired step-up is stored.
280
+ */
281
+ stepUpApplies(response, text, path) {
282
+ const stepUp = this.credentials?.stepUp;
283
+ if (!stepUp?.accessToken)
284
+ return false;
285
+ if (new Date(stepUp.expiresAt).getTime() <= Date.now())
286
+ return false;
287
+ const parsed = parseErrorResponse(response, text, path);
288
+ if (parsed.code === "PROTECTED_APP_STEP_UP_REQUIRED")
289
+ return true;
290
+ const details = parsed.details;
291
+ const missing = details && !Array.isArray(details) ? details.missing : undefined;
292
+ return Array.isArray(missing) && missing.includes("admin");
293
+ }
294
+ /**
295
+ * #4564 — a request outside `request` (an upload, a function invocation)
296
+ * sent with the CLI's own credentials, retried once with the stored step-up
297
+ * exactly as `request` retries. `init` builds the request for a bearer. The
298
+ * answer is the retry's, unless the retry was refused 401: the step-up is
299
+ * dropped and the original refusal comes back, still readable.
300
+ */
301
+ async fetchWithStepUp(url, path, accessToken, init) {
302
+ const response = await fetchWithTLS(url, init(accessToken));
303
+ if (response.status !== 403)
304
+ return response;
305
+ // Read once to decide; hand the caller a response it can still read.
306
+ const text = await response.text();
307
+ const original = new Response(text, {
308
+ status: response.status,
309
+ statusText: response.statusText,
310
+ headers: response.headers,
311
+ });
312
+ if (!this.stepUpApplies(response, text, path))
313
+ return original;
314
+ const retried = await fetchWithTLS(url, init(this.credentials.stepUp.accessToken));
315
+ if (retried.status === 401) {
316
+ await retried.text().catch(() => "");
317
+ this.dropStepUp();
318
+ return original;
319
+ }
320
+ return retried;
321
+ }
322
+ /** Forget a step-up the server no longer accepts. */
323
+ dropStepUp() {
324
+ if (!this.credentials?.stepUp)
325
+ return;
326
+ const { stepUp: _dropped, ...rest } = this.credentials;
327
+ this.credentials = rest;
328
+ saveCredentials(rest);
329
+ }
330
+ /** The typed error a failed response throws. */
331
+ responseError(response, text, path) {
332
+ const parsed = parseErrorResponse(response, text, path);
333
+ // Preserve the `<!DOCTYPE` → 404 ApiError shape.
334
+ if (parsed.htmlNotFound) {
335
+ return new ApiError(parsed.message, 404);
336
+ }
337
+ // Narrow details to the record shape for the issue #666 typed-exception
338
+ // dispatch below. Conflict metadata may live under `details.*` (canonical
339
+ // location per A1) or on the top-level envelope (legacy).
340
+ const detailsRecord = parsed.details && !Array.isArray(parsed.details)
341
+ ? parsed.details
342
+ : undefined;
343
+ const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
344
+ const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
345
+ // Typed exceptions for the schema-feature (issue #666).
346
+ if (response.status === 409 && parsed.code === "CONFLICT") {
347
+ return new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
348
+ }
349
+ if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
350
+ return new TomlParseError(parsed.message, detailsRecord);
351
+ }
352
+ // #4512 — a pin refusal on another environment's app names its login.
353
+ const hint = pinRefusalHint(parsed, this.credentials?.serverUrl ?? "", projectEnvironments());
354
+ return new ApiError(hint ? `${parsed.message} ${hint}` : parsed.message, response.status, parsed.code, parsed.details);
355
+ }
356
+ /**
357
+ * `POST /admin/api/auth/token` — derive a token no wider than the stored
358
+ * one (`primitive token --scope`, #3889). A wider request is refused with
359
+ * `TOKEN_WIDER_THAN_PRESENTED`.
360
+ */
361
+ async exchangeToken(body) {
362
+ return this.post("/admin/api/auth/token", body);
363
+ }
352
364
  // Helper for GET requests with query params
353
365
  async get(path, params) {
354
366
  let fullPath = path;
@@ -410,6 +422,10 @@ export class ApiClient {
410
422
  qs.set("limit", String(params.limit));
411
423
  if (params?.cursor)
412
424
  qs.set("cursor", params.cursor);
425
+ // `kind` filters by lineage: `parent` is apps with no parent, `child`
426
+ // child apps. Omitted lists every app the caller is assigned to.
427
+ if (params?.kind)
428
+ qs.set("kind", params.kind);
413
429
  const q = qs.toString();
414
430
  const resp = await this.get(`/admin/api/admins/me/apps${q ? `?${q}` : ""}`);
415
431
  return {
@@ -432,40 +448,53 @@ export class ApiClient {
432
448
  return { apps };
433
449
  }
434
450
  /**
435
- * Every app on the server, not just the caller's own.
436
- *
437
- * Deliberately separate from `listApps()`, whose contract is "apps assigned
438
- * to me" (`GET /admin/api/admins/me/apps`, backed by `admin.queryApps()`) —
439
- * the contract every other command depends on. `analytics workflow-usage
440
- * --all-apps` has to reach apps nobody is assigned to, which is the global
441
- * route `GET /admin/api/apps` (`App.iterateAll()`), and that route
442
- * identifies an app as `appId` rather than `id` (D3130-001).
443
- *
444
- * One drain of that route is lossy — its cursor re-scans `App.iterateAll()`
445
- * from the start of each page, so a list that changes under it both repeats
446
- * and drops rows — so the walk is the union of repeated drains
447
- * (`drainAppsUntilConverged`, which carries the measurement).
448
- */
449
- async listAllAppsGlobal() {
450
- const drainOnce = () => paginateAll(async (cursor) => {
451
- const qs = new URLSearchParams({ limit: "100" });
452
- if (cursor)
453
- qs.set("cursor", cursor);
454
- const resp = await this.get(`/admin/api/apps?${qs.toString()}`);
455
- const rows = resp.items ?? [];
456
- return {
457
- items: rows.map((row) => ({
458
- appId: String(row.appId ?? row.id ?? ""),
459
- name: String(row.name ?? ""),
460
- })),
461
- nextCursor: resp.nextCursor,
462
- };
463
- });
464
- return drainAppsUntilConverged(drainOnce);
465
- }
451
+ * Create an app. An ordinary app takes `name` (and, for a super-admin,
452
+ * `initialAdminEmail`). A child app takes `parentAppId` and `childSlug`
453
+ * instead, plus the optional `branch`, `idleDays` (7-30, default 30) and
454
+ * `copySecrets` (default true); the server names it `<parent name> /
455
+ * <slug>` and copies the parent's settings.
456
+ */
466
457
  async createApp(data) {
467
458
  return this.post("/admin/api/apps", data);
468
459
  }
460
+ /**
461
+ * One page of an app's child apps, in creation order
462
+ * (`GET /admin/api/apps/{appId}/children`).
463
+ */
464
+ async listAppChildren(parentAppId, params) {
465
+ const qs = new URLSearchParams();
466
+ if (params?.limit !== undefined)
467
+ qs.set("limit", String(params.limit));
468
+ if (params?.cursor)
469
+ qs.set("cursor", params.cursor);
470
+ const q = qs.toString();
471
+ const resp = await this.get(`/admin/api/apps/${encodeURIComponent(parentAppId)}/children${q ? `?${q}` : ""}`);
472
+ return {
473
+ items: resp?.items ?? [],
474
+ hasMore: resp?.hasMore ?? false,
475
+ nextCursor: resp?.nextCursor ?? null,
476
+ };
477
+ }
478
+ /**
479
+ * Add one origin to a child's preview origins
480
+ * (`POST /admin/api/apps/{parentAppId}/children/{slug}/preview-origins`).
481
+ */
482
+ async addChildPreviewOrigin(parentAppId, slug, origin) {
483
+ return this.post(this.childPreviewOriginsPath(parentAppId, slug), { origin });
484
+ }
485
+ /**
486
+ * Remove one origin from a child's preview origins
487
+ * (`DELETE /admin/api/apps/{parentAppId}/children/{slug}/preview-origins`).
488
+ */
489
+ async removeChildPreviewOrigin(parentAppId, slug, origin) {
490
+ return this.request(this.childPreviewOriginsPath(parentAppId, slug), {
491
+ method: "DELETE",
492
+ body: JSON.stringify({ origin }),
493
+ });
494
+ }
495
+ childPreviewOriginsPath(parentAppId, slug) {
496
+ return `/admin/api/apps/${encodeURIComponent(parentAppId)}/children/${encodeURIComponent(slug)}/preview-origins`;
497
+ }
469
498
  async getApp(appId) {
470
499
  return this.get(`/admin/api/apps/${appId}`);
471
500
  }
@@ -487,6 +516,14 @@ export class ApiClient {
487
516
  async addUserByEmail(appId, data) {
488
517
  return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
489
518
  }
519
+ /**
520
+ * Set a member's display name and/or avatar URL
521
+ * (`PATCH /admin/api/apps/:appId/users/:userId/profile`). An omitted field
522
+ * is left alone; `avatarUrl: null` or `""` clears the avatar.
523
+ */
524
+ async setUserProfile(appId, userId, data) {
525
+ return this.patch(`/admin/api/apps/${appId}/users/${userId}/profile`, data);
526
+ }
490
527
  async mintTestJwt(appId, userId, role) {
491
528
  return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
492
529
  }
@@ -646,7 +683,7 @@ export class ApiClient {
646
683
  nextCursor: result?.nextCursor ?? null,
647
684
  };
648
685
  }
649
- /** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
686
+ /** Integration detail (`requestConfig` is on the detail, not the list summary). */
650
687
  async getIntegration(appId, integrationId) {
651
688
  return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
652
689
  }
@@ -686,10 +723,6 @@ export class ApiClient {
686
723
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
687
724
  return result?.items ?? [];
688
725
  }
689
- async listWorkflowRunIntegrationLogs(appId, runId, params) {
690
- const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
691
- return result?.items ?? [];
692
- }
693
726
  // ============================================
694
727
  // APP SECRETS
695
728
  // ============================================
@@ -749,42 +782,6 @@ export class ApiClient {
749
782
  // ============================================
750
783
  // WEBHOOKS
751
784
  // ============================================
752
- async listWebhooks(appId, params) {
753
- const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
754
- return {
755
- items: result?.items ?? [],
756
- hasMore: result?.hasMore,
757
- nextCursor: result?.nextCursor ?? null,
758
- };
759
- }
760
- async getWebhook(appId, webhookId) {
761
- return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
762
- }
763
- async createWebhook(appId, payload) {
764
- return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
765
- }
766
- async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
767
- const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
768
- return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
769
- }
770
- /**
771
- * Delete a webhook. Archives it by default; `hard` hard-deletes the row
772
- * instead (#2232), which is what frees a slot under the per-app webhook cap
773
- * and releases the webhook key for reuse. Same `?hard=true` verb the admin
774
- * integrations delete uses.
775
- */
776
- async deleteWebhook(appId, webhookId, options) {
777
- const query = options?.hard ? "?hard=true" : "";
778
- return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}${query}`);
779
- }
780
- /**
781
- * #2803 — the operational verb pair for a webhook. It posts to the
782
- * enable/disable route rather than PATCHing `status`, which is server-owned
783
- * and refused on every configuration write path.
784
- */
785
- async setWebhookAvailability(appId, webhookId, action) {
786
- return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
787
- }
788
785
  // ── Server functions (#3179) ─────────────────────────────────────────
789
786
  //
790
787
  // The `functions` family mirrors workflows and scripts: a header with the
@@ -829,6 +826,38 @@ export class ApiClient {
829
826
  const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs`, params);
830
827
  return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
831
828
  }
829
+ // ── A function's trigger operations (#3190) ──
830
+ // The operational home of a function's webhook and cron triggers, keyed by
831
+ // the function and a cron entry's name rather than a trigger row's id.
832
+ functionTriggersPath(appId, functionId) {
833
+ return `/admin/api/apps/${appId}/functions/${encodeURIComponent(functionId)}/triggers`;
834
+ }
835
+ /** The function's webhook delivery log, newest first. */
836
+ async listFunctionWebhookEvents(appId, functionId, params) {
837
+ const result = await this.get(`${this.functionTriggersPath(appId, functionId)}/webhook/events`, params);
838
+ const nextCursor = result?.nextCursor ?? null;
839
+ return {
840
+ items: result?.items ?? [],
841
+ hasMore: result?.hasMore ?? !!nextCursor,
842
+ nextCursor,
843
+ };
844
+ }
845
+ /** Sign a preview delivery for the function's webhook trigger. */
846
+ async testFunctionWebhook(appId, functionId, payload) {
847
+ return this.post(`${this.functionTriggersPath(appId, functionId)}/webhook/test`, payload || {});
848
+ }
849
+ /** Check captured headers and a captured body against the trigger's verifier. */
850
+ async verifyFunctionWebhook(appId, functionId, payload) {
851
+ return this.post(`${this.functionTriggersPath(appId, functionId)}/webhook/verify`, payload);
852
+ }
853
+ /** Point the trigger's `signingSecret` at a new `{{secrets.KEY}}` reference. */
854
+ async rotateFunctionWebhookSecret(appId, functionId, payload) {
855
+ return this.post(`${this.functionTriggersPath(appId, functionId)}/webhook/rotate-secret`, payload);
856
+ }
857
+ /** One cron entry verb: `fire`, `fire-scheduled`, `enable` or `disable`. */
858
+ async functionCronEntryAction(appId, functionId, name, action) {
859
+ return this.post(`${this.functionTriggersPath(appId, functionId)}/cron/${encodeURIComponent(name)}/${action}`, {});
860
+ }
832
861
  /**
833
862
  * A function run's step trace — #3348.
834
863
  *
@@ -842,6 +871,16 @@ export class ApiClient {
842
871
  const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs/${runId}/steps`);
843
872
  return { items: result?.items ?? [] };
844
873
  }
874
+ /**
875
+ * The integration calls one function run made — #3190.
876
+ *
877
+ * The run → integration-logs pivot, addressed by the function that owns the
878
+ * run (it was `workflows/runs/{runId}/integration-logs`).
879
+ */
880
+ async listFunctionRunIntegrationLogs(appId, functionId, runId, options) {
881
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs/${runId}/integration-logs`, options?.limit !== undefined ? { limit: options.limit } : undefined);
882
+ return { items: result?.items ?? [] };
883
+ }
845
884
  /**
846
885
  * End a function run that will not settle — #3348.
847
886
  *
@@ -914,26 +953,31 @@ export class ApiClient {
914
953
  async invokeFunction(appId, functionKey, body, options) {
915
954
  const credentials = await this.ensureAuthenticated();
916
955
  const base = `/app/${appId}/api/functions/${encodeURIComponent(functionKey)}`;
956
+ const task = options?.runtime === "task";
917
957
  const path = options?.asSystem
918
- ? `${base}/invoke-as-system`
919
- : options?.runtime === "task"
958
+ ? `${base}/${task ? "start-as-system" : "invoke-as-system"}`
959
+ : task
920
960
  ? `${base}/start`
921
961
  : base;
922
- const headers = {
923
- Authorization: `Bearer ${options?.bearer ?? credentials.accessToken}`,
924
- "Content-Type": "application/json",
962
+ const init = (bearer) => {
963
+ const headers = {
964
+ Authorization: `Bearer ${bearer}`,
965
+ "Content-Type": "application/json",
966
+ };
967
+ // The CLI's own credentials reach an app-scoped route through the
968
+ // global-admin header; a minted user token does not need it and must
969
+ // not carry it.
970
+ if (!options?.bearer && credentials.globalAdminAppId) {
971
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
972
+ }
973
+ return { method: "POST", headers, body: JSON.stringify(body) };
925
974
  };
926
- // The CLI's own credentials reach an app-scoped route through the
927
- // global-admin header; a minted user token does not need it and must not
928
- // carry it.
929
- if (!options?.bearer && credentials.globalAdminAppId) {
930
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
931
- }
932
- const response = await fetchWithTLS(`${credentials.serverUrl}${path}`, {
933
- method: "POST",
934
- headers,
935
- body: JSON.stringify(body),
936
- });
975
+ const url = `${credentials.serverUrl}${path}`;
976
+ // A minted user token is sent as it is; the CLI's own credentials get the
977
+ // step-up retry a protected app's refusal asks for (#4564).
978
+ const response = options?.bearer
979
+ ? await fetchWithTLS(url, init(options.bearer))
980
+ : await this.fetchWithStepUp(url, path, credentials.accessToken, init);
937
981
  const text = await response.text();
938
982
  let parsed = null;
939
983
  try {
@@ -952,6 +996,38 @@ export class ApiClient {
952
996
  body: parsed,
953
997
  };
954
998
  }
999
+ /**
1000
+ * Run only a function's `authorize` and hand back the ANSWER, whatever its
1001
+ * HTTP status — #4732. Like `invokeFunction`, it returns a refusal rather
1002
+ * than throwing it, so `functions authorize` renders the server's own
1003
+ * answer (and a case run records it as a failed row); it throws only when
1004
+ * the request did not complete. The CLI's own credentials, with the
1005
+ * step-up retry a protected app's refusal asks for.
1006
+ */
1007
+ async authorizeFunction(appId, functionId, body) {
1008
+ const credentials = await this.ensureAuthenticated();
1009
+ const path = `/admin/api/apps/${appId}/functions/${encodeURIComponent(functionId)}/authorize`;
1010
+ const init = (bearer) => {
1011
+ const headers = {
1012
+ Authorization: `Bearer ${bearer}`,
1013
+ "Content-Type": "application/json",
1014
+ };
1015
+ if (credentials.globalAdminAppId) {
1016
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1017
+ }
1018
+ return { method: "POST", headers, body: JSON.stringify(body) };
1019
+ };
1020
+ const response = await this.fetchWithStepUp(`${credentials.serverUrl}${path}`, path, credentials.accessToken, init);
1021
+ const text = await response.text();
1022
+ let parsed = null;
1023
+ try {
1024
+ parsed = text ? JSON.parse(text) : null;
1025
+ }
1026
+ catch {
1027
+ parsed = { error: text };
1028
+ }
1029
+ return { httpStatus: response.status, body: parsed };
1030
+ }
955
1031
  /**
956
1032
  * A function's versions, newest first (#3462).
957
1033
  *
@@ -1004,27 +1080,14 @@ export class ApiClient {
1004
1080
  async setIntegrationAvailability(appId, integrationId, action) {
1005
1081
  return this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/${action}`, {});
1006
1082
  }
1007
- async rotateWebhookSecret(appId, webhookId, payload) {
1008
- return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
1009
- }
1010
- async listWebhookEvents(appId, webhookId, params) {
1011
- const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
1012
- const nextCursor = result?.nextCursor ?? null;
1013
- return {
1014
- items: result?.items ?? [],
1015
- hasMore: result?.hasMore ?? !!nextCursor,
1016
- nextCursor,
1017
- };
1018
- }
1019
1083
  // ============================================
1020
1084
  // CONNECTIONS INSPECTION (#1968)
1021
1085
  // ============================================
1022
1086
  /**
1023
1087
  * List the live connections the server believes exist for exactly one of a
1024
- * document, user, or database. Read-only inspection over admin-api. Rows are
1088
+ * document or user. Read-only inspection over admin-api. Rows are
1025
1089
  * per-connection-×-subscription bindings, so `connectionId` is not unique
1026
- * across rows. `params` on a database-subscription row is caller-defined
1027
- * bound CEL input — typed `unknown`.
1090
+ * across rows.
1028
1091
  */
1029
1092
  async listConnections(appId, selector, params) {
1030
1093
  const result = await this.get(`/admin/api/apps/${appId}/connections`, {
@@ -1037,6 +1100,29 @@ export class ApiClient {
1037
1100
  nextCursor: result?.nextCursor ?? null,
1038
1101
  };
1039
1102
  }
1103
+ /**
1104
+ * One page of one user's agent sessions (#3809): the app API's sessions
1105
+ * list, which an app owner or admin may read for any user.
1106
+ */
1107
+ async listAgentSessions(appId, params) {
1108
+ const result = await this.get(`/app/${appId}/api/agent-sessions`, params);
1109
+ return {
1110
+ items: result?.items ?? [],
1111
+ hasMore: result?.hasMore,
1112
+ nextCursor: result?.nextCursor ?? null,
1113
+ };
1114
+ }
1115
+ /**
1116
+ * One agent session (#3809): its binding, status, members and active turn,
1117
+ * and for an app owner or admin the session object's state. Never its rows.
1118
+ */
1119
+ async getAgentSession(appId, sessionId) {
1120
+ return this.get(`/app/${appId}/api/agent-sessions/${encodeURIComponent(sessionId)}`);
1121
+ }
1122
+ /** Delete an agent session and its document (#3809); a repeat succeeds again. */
1123
+ async deleteAgentSession(appId, sessionId) {
1124
+ return this.delete(`/app/${appId}/api/agent-sessions/${encodeURIComponent(sessionId)}`);
1125
+ }
1040
1126
  async listSessions(appId, userId, params) {
1041
1127
  const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
1042
1128
  userId,
@@ -1064,20 +1150,6 @@ export class ApiClient {
1064
1150
  async revokeAdminSession(sessionId) {
1065
1151
  return this.delete(`/admin/api/auth/sessions/${encodeURIComponent(sessionId)}`);
1066
1152
  }
1067
- async testWebhook(appId, webhookId, payload) {
1068
- return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
1069
- }
1070
- /**
1071
- * Run a webhook's configured verifier over headers and a body captured from
1072
- * a real delivery, and report whether that signature checks out (#2445).
1073
- *
1074
- * `headers` is sent as ordered `[name, value]` pairs so a capture carrying a
1075
- * field name on more than one line survives the round trip. Nothing is
1076
- * delivered: no delivery record, no workflow run.
1077
- */
1078
- async verifyWebhook(appId, webhookId, payload) {
1079
- return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/verify`, payload);
1080
- }
1081
1153
  // ============================================
1082
1154
  // NAMED LOCKS (#1518)
1083
1155
  // ============================================
@@ -1108,51 +1180,6 @@ export class ApiClient {
1108
1180
  // ============================================
1109
1181
  // CRON TRIGGERS
1110
1182
  // ============================================
1111
- async listCronTriggers(appId) {
1112
- const result = await this.get(`/app/${appId}/api/cron-triggers`);
1113
- return { items: result?.items ?? [] };
1114
- }
1115
- async getCronTrigger(appId, triggerId) {
1116
- return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
1117
- }
1118
- async createCronTrigger(appId, payload) {
1119
- return this.post(`/app/${appId}/api/cron-triggers`, payload);
1120
- }
1121
- async updateCronTrigger(appId, triggerId, payload) {
1122
- return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
1123
- }
1124
- /**
1125
- * Retire a cron trigger. The default soft-deletes (writes the `archived`
1126
- * tombstone); `{ hard: true }` destroys the row, which is what
1127
- * `config push --prune` passes (#2803) so a pruned trigger stops consuming
1128
- * the per-app cap and holding its key.
1129
- */
1130
- async deleteCronTrigger(appId, triggerId, options = {}) {
1131
- const query = options.hard ? "?hard=true" : "";
1132
- return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}${query}`);
1133
- }
1134
- async disableCronTrigger(appId, triggerId) {
1135
- return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
1136
- }
1137
- async enableCronTrigger(appId, triggerId) {
1138
- return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
1139
- }
1140
- async testCronTrigger(appId, triggerId) {
1141
- return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
1142
- }
1143
- // ============================================
1144
- // ITERATIONS (iterate-users introspection / reset — #1209)
1145
- // ============================================
1146
- async listIterations(appId) {
1147
- const result = await this.get(`/app/${appId}/api/iterations`);
1148
- return { items: result?.items ?? [] };
1149
- }
1150
- async getIteration(appId, iterationName) {
1151
- return this.get(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}`);
1152
- }
1153
- async resetIteration(appId, iterationName) {
1154
- return this.post(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}/reset`, {});
1155
- }
1156
1183
  // ============================================
1157
1184
  // RESOURCE METADATA (values — issue #1352)
1158
1185
  // ============================================
@@ -1257,16 +1284,12 @@ export class ApiClient {
1257
1284
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/configs/${configId}/duplicate`, payload || {});
1258
1285
  }
1259
1286
  // ============================================
1260
- // SCRIPTS (Rhai transforms) — #1000 (prompt-model convergence)
1287
+ // SCRIPTS — retained history (#3190)
1261
1288
  // ============================================
1262
1289
  //
1263
- // The `Script` model is the authoring surface for `script` workflow
1264
- // steps. A `Script` is a HEADER (name/description/activeConfigId); the
1265
- // Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
1266
- // time. CLI `config push` reads `transforms/*.rhai` files and reconciles
1267
- // them: a new file → create script (mints a default config); a changed
1268
- // file → create a new config + activate it (zero fan-out — referencing
1269
- // workflows pick up the new body on their next run).
1290
+ // Scripts retired with the Rhai runtime. A `Script` HEADER
1291
+ // (name/description/activeConfigId) and its versioned `ScriptConfig` rows
1292
+ // stay readable until their retention ends; nothing writes them.
1270
1293
  async listScripts(appId) {
1271
1294
  const result = await this.get(`/admin/api/apps/${appId}/scripts`);
1272
1295
  return { items: result?.items ?? [] };
@@ -1279,57 +1302,18 @@ export class ApiClient {
1279
1302
  * is a distinct block version: `configId` is the stable version selector,
1280
1303
  * `contentHash` is its content identity, and `status` reports whether it is
1281
1304
  * `active`/`draft`/`archived`. The script header's `activeConfigId` names
1282
- * the live version. Used by `scripts configs list` and by the script test
1283
- * commands to run against a specific pinned version.
1305
+ * the version that was live. Used by `scripts configs list`.
1284
1306
  */
1285
1307
  async listScriptConfigs(appId, scriptId) {
1286
1308
  const result = await this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`);
1287
1309
  return { items: result?.items ?? [] };
1288
1310
  }
1289
- async createScript(appId, payload) {
1290
- // Mints the script header + a default `active` config carrying the body.
1291
- return this.post(`/admin/api/apps/${appId}/scripts`, payload);
1292
- }
1293
- /**
1294
- * Push a new body for an existing script by creating a new `ScriptConfig`
1295
- * and activating it (the prompt-model "edit → activate" flow). Referencing
1296
- * workflows pick up the new body on their next run with no fan-out. The
1297
- * config name is unique per script, so we mint a timestamped name.
1298
- */
1299
- async pushScriptBody(appId, scriptId, body,
1300
- /**
1301
- * The active state the caller believes holds right now (#2731 B10).
1302
- * Creating an inactive config races nothing, so the guard sits on the
1303
- * ACTIVATION step: if the active config has moved since the caller read it,
1304
- * the server rejects with a 409 instead of overwriting a concurrent script
1305
- * change. It takes BOTH halves of "what is active", because a config's body
1306
- * can be edited in place without the active id changing — the id alone
1307
- * would let such an edit through. Omitted (e.g. `--force`) means activate
1308
- * unconditionally.
1309
- */
1310
- expectedActive) {
1311
- const configName = `sync-${Date.now()}`;
1312
- const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
1313
- const activateBody = {};
1314
- if (expectedActive?.configId) {
1315
- activateBody.expectedActiveConfigId = expectedActive.configId;
1316
- }
1317
- if (expectedActive?.modifiedAt) {
1318
- activateBody.expectedActiveModifiedAt = expectedActive.modifiedAt;
1319
- }
1320
- await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, activateBody);
1321
- return config;
1322
- }
1323
1311
  /**
1324
- * Delete a script header (cascades its configs). `force` overrides the
1325
- * server's guard against deleting a script that an active workflow still
1326
- * references by name — without it the server answers 409.
1312
+ * One version of a script, body included. A body the server cannot load
1313
+ * answers a 404 naming the version rather than an empty body.
1327
1314
  */
1328
- async deleteScript(appId, scriptId, force) {
1329
- const path = force
1330
- ? `/admin/api/apps/${appId}/scripts/${scriptId}?force=true`
1331
- : `/admin/api/apps/${appId}/scripts/${scriptId}`;
1332
- return this.delete(path);
1315
+ async getScriptConfig(appId, scriptId, configId) {
1316
+ return this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${configId}`);
1333
1317
  }
1334
1318
  async getPromptSchema(appId, promptId) {
1335
1319
  return this.get(`/admin/api/apps/${appId}/prompts/${promptId}/schema`);
@@ -1387,6 +1371,13 @@ export class ApiClient {
1387
1371
  if (payload.expectedJsonSubset) {
1388
1372
  serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
1389
1373
  }
1374
+ // #4262 — an agent case's `[test.agent]` keys, each when the caller set it:
1375
+ // presence, not truthiness, so `agentExpectedNoToolCall: false` travels. A
1376
+ // chat case sets none, and its body is unchanged.
1377
+ for (const key of AGENT_TEST_CASE_WIRE_KEYS) {
1378
+ if (payload[key] !== undefined)
1379
+ serverPayload[key] = payload[key];
1380
+ }
1390
1381
  return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
1391
1382
  }
1392
1383
  async updateTestCase(appId, blockType, blockId, testCaseId, payload) {
@@ -1421,6 +1412,11 @@ export class ApiClient {
1421
1412
  }
1422
1413
  if (payload.key !== undefined)
1423
1414
  serverPayload.key = payload.key;
1415
+ // #4262 — as on create; an explicit `null` clears the field.
1416
+ for (const key of AGENT_TEST_CASE_WIRE_KEYS) {
1417
+ if (payload[key] !== undefined)
1418
+ serverPayload[key] = payload[key];
1419
+ }
1424
1420
  return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
1425
1421
  }
1426
1422
  async deleteTestCase(appId, blockType, blockId, testCaseId) {
@@ -1436,17 +1432,15 @@ export class ApiClient {
1436
1432
  const credentials = await this.ensureAuthenticated();
1437
1433
  const encodedFilename = encodeURIComponent(filename);
1438
1434
  const url = `${credentials.serverUrl}/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}/attachments/${encodedFilename}`;
1439
- const headers = {
1440
- Authorization: `Bearer ${credentials.accessToken}`,
1441
- "Content-Type": contentType,
1442
- };
1443
- if (credentials.globalAdminAppId) {
1444
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1445
- }
1446
- const response = await fetchWithTLS(url, {
1447
- method: "PUT",
1448
- headers,
1449
- body: data,
1435
+ const response = await this.fetchWithStepUp(url, url, credentials.accessToken, (bearer) => {
1436
+ const headers = {
1437
+ Authorization: `Bearer ${bearer}`,
1438
+ "Content-Type": contentType,
1439
+ };
1440
+ if (credentials.globalAdminAppId) {
1441
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
1442
+ }
1443
+ return { method: "PUT", headers, body: data };
1450
1444
  });
1451
1445
  const text = await response.text();
1452
1446
  if (!response.ok) {
@@ -1498,7 +1492,7 @@ export class ApiClient {
1498
1492
  return { items: result?.items ?? result ?? [] };
1499
1493
  }
1500
1494
  // ============================================
1501
- // WORKFLOWS
1495
+ // WORKFLOWS — retained history, read-only (#3190)
1502
1496
  // ============================================
1503
1497
  async listWorkflows(appId, params) {
1504
1498
  const result = await this.get(`/admin/api/apps/${appId}/workflows`, params);
@@ -1511,140 +1505,39 @@ export class ApiClient {
1511
1505
  async getWorkflow(appId, workflowId) {
1512
1506
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}`);
1513
1507
  }
1514
- /**
1515
- * One app's workflow step usage report (#3132).
1516
- *
1517
- * `windowDays` is omitted from the query when the caller did not ask for a
1518
- * window, so the server applies its own default rather than the CLI
1519
- * restating it on the wire.
1520
- */
1521
- async getWorkflowUsage(appId, params) {
1522
- return this.get(`/admin/api/apps/${appId}/workflows/usage`, params);
1508
+ // ============================================
1509
+ // WORKFLOW CONFIGURATIONS
1510
+ // ============================================
1511
+ async listWorkflowConfigs(appId, workflowId) {
1512
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/configs`);
1513
+ return { items: result?.items ?? [] };
1523
1514
  }
1524
- async createWorkflow(appId, payload) {
1525
- return this.post(`/admin/api/apps/${appId}/workflows`, payload);
1515
+ async getWorkflowConfig(appId, workflowId, configId) {
1516
+ return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}`);
1526
1517
  }
1527
- async updateWorkflow(appId, workflowId, payload, expectedModifiedAt) {
1528
- const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
1529
- return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
1518
+ // ============================================
1519
+ // ANALYTICS
1520
+ // ============================================
1521
+ async getAnalyticsTopUsers(appId, params) {
1522
+ return this.get(`/app/${appId}/api/analytics/users/top`, params);
1530
1523
  }
1531
- /**
1532
- * Retire a workflow, or destroy it (#2887).
1533
- *
1534
- * The plain call ARCHIVES: the definition row stays, so its runs, revisions
1535
- * and test cases keep resolving. `{ hard: true }` performs the full cascade
1536
- * and is what releases the `workflowKey` — the flag `config push --prune`
1537
- * sends, for the same reason it sends it for webhooks and integrations.
1538
- */
1539
- async deleteWorkflow(appId, workflowId, options) {
1540
- const path = `/admin/api/apps/${appId}/workflows/${workflowId}`;
1541
- return this.delete(options?.hard ? `${path}?hard=true` : path);
1524
+ async getAnalyticsIntegrationMetrics(appId, params) {
1525
+ return this.get(`/app/${appId}/api/analytics/integrations`, params);
1542
1526
  }
1543
- /**
1544
- * #2645 criterion 9 — the operational toggle. Deliberately NOT the config
1545
- * PATCH: `config push` owns every configuration field, and taking a workflow
1546
- * out of service must not touch one, or an operator's action would show up as
1547
- * drift against the repo. Same route shape as `cron-triggers pause|resume`.
1548
- */
1549
- async disableWorkflow(appId, workflowId) {
1550
- return this.post(`/app/${appId}/api/workflows/${workflowId}/disable`, {});
1527
+ async getAnalyticsOverviewDau(appId) {
1528
+ return this.get(`/app/${appId}/api/analytics/overview/dau`);
1551
1529
  }
1552
- async enableWorkflow(appId, workflowId) {
1553
- return this.post(`/app/${appId}/api/workflows/${workflowId}/enable`, {});
1530
+ async getAnalyticsOverviewWau(appId) {
1531
+ return this.get(`/app/${appId}/api/analytics/overview/wau`);
1554
1532
  }
1555
- async updateWorkflowDraft(appId, workflowId, payload) {
1556
- return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
1533
+ async getAnalyticsOverviewMau(appId) {
1534
+ return this.get(`/app/${appId}/api/analytics/overview/mau`);
1557
1535
  }
1558
- async previewWorkflow(appId, workflowId, payload) {
1559
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
1536
+ async getAnalyticsOverviewGrowth(appId, params) {
1537
+ return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
1560
1538
  }
1561
- async getPreviewStatus(appId, workflowId, instanceId) {
1562
- return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/preview/${instanceId}/status`);
1563
- }
1564
- async listWorkflowRuns(appId, workflowId, params) {
1565
- const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
1566
- const nextCursor = result?.nextCursor ?? null;
1567
- return {
1568
- items: result?.items ?? [],
1569
- hasMore: result?.hasMore,
1570
- nextCursor,
1571
- resumeAfter: result?.resumeAfter,
1572
- scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
1573
- };
1574
- }
1575
- /**
1576
- * Workflow runs started by one app user, across every workflow (#1967).
1577
- * Backed by `GET /admin/api/apps/{appId}/users/{userId}/workflow-runs`,
1578
- * which resolves the app user before querying the `runsByUser` index.
1579
- */
1580
- async listUserWorkflowRuns(appId, userId, params) {
1581
- const result = await this.get(`/admin/api/apps/${appId}/users/${encodeURIComponent(userId)}/workflow-runs`, params);
1582
- const nextCursor = result?.nextCursor ?? null;
1583
- return {
1584
- items: result?.items ?? [],
1585
- hasMore: result?.hasMore,
1586
- nextCursor,
1587
- scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
1588
- };
1589
- }
1590
- async getWorkflowRunStatus(appId, workflowId, runId) {
1591
- return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
1592
- }
1593
- async getWorkflowStepRuns(appId, workflowId, runId) {
1594
- const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
1595
- return { items: result?.items ?? [] };
1596
- }
1597
- // Workflow analytics have one client path, `getAnalyticsTopWorkflows`
1598
- // (issue #2766): `getTopWorkflows` was a second name for the same endpoint,
1599
- // and `getWorkflowAnalytics` called a route the server never registered.
1600
- // ============================================
1601
- // WORKFLOW CONFIGURATIONS
1602
- // ============================================
1603
- async listWorkflowConfigs(appId, workflowId) {
1604
- const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/configs`);
1605
- return { items: result?.items ?? [] };
1606
- }
1607
- async getWorkflowConfig(appId, workflowId, configId) {
1608
- return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}`);
1609
- }
1610
- async createWorkflowConfig(appId, workflowId, payload) {
1611
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/configs`, payload);
1612
- }
1613
- async updateWorkflowConfig(appId, workflowId, configId, payload) {
1614
- return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}`, payload);
1615
- }
1616
- async archiveWorkflowConfig(appId, workflowId, configId) {
1617
- return this.delete(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}`);
1618
- }
1619
- async activateWorkflowConfig(appId, workflowId, configId) {
1620
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}/activate`, {});
1621
- }
1622
- async duplicateWorkflowConfig(appId, workflowId, configId, payload) {
1623
- return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/configs/${configId}/duplicate`, payload || {});
1624
- }
1625
- // ============================================
1626
- // ANALYTICS
1627
- // ============================================
1628
- async getAnalyticsTopUsers(appId, params) {
1629
- return this.get(`/app/${appId}/api/analytics/users/top`, params);
1630
- }
1631
- async getAnalyticsIntegrationMetrics(appId, params) {
1632
- return this.get(`/app/${appId}/api/analytics/integrations`, params);
1633
- }
1634
- async getAnalyticsOverviewDau(appId) {
1635
- return this.get(`/app/${appId}/api/analytics/overview/dau`);
1636
- }
1637
- async getAnalyticsOverviewWau(appId) {
1638
- return this.get(`/app/${appId}/api/analytics/overview/wau`);
1639
- }
1640
- async getAnalyticsOverviewMau(appId) {
1641
- return this.get(`/app/${appId}/api/analytics/overview/mau`);
1642
- }
1643
- async getAnalyticsOverviewGrowth(appId, params) {
1644
- return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
1645
- }
1646
- async getAnalyticsDailyActive(appId, params) {
1647
- return this.get(`/app/${appId}/api/analytics/daily-active`, params);
1539
+ async getAnalyticsDailyActive(appId, params) {
1540
+ return this.get(`/app/${appId}/api/analytics/daily-active`, params);
1648
1541
  }
1649
1542
  async getAnalyticsRollingActive(appId, params) {
1650
1543
  return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
@@ -1670,9 +1563,6 @@ export class ApiClient {
1670
1563
  async getAnalyticsErrorsGroups(appId, params) {
1671
1564
  return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
1672
1565
  }
1673
- async getAnalyticsTopWorkflows(appId, params) {
1674
- return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1675
- }
1676
1566
  async getAnalyticsTopPrompts(appId, params) {
1677
1567
  return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
1678
1568
  }
@@ -1984,72 +1874,6 @@ export class ApiClient {
1984
1874
  });
1985
1875
  }
1986
1876
  // ============================================
1987
- // DATABASE OPERATIONS (registered operations)
1988
- // ============================================
1989
- async listDatabaseOperations(appId, databaseId) {
1990
- return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
1991
- }
1992
- /**
1993
- * Execute a registered operation and, when the result is a query page,
1994
- * normalize it to the CLI's one list envelope (issue #2440).
1995
- *
1996
- * The registered operation decides the response shape, so the normalization
1997
- * is conditional — see {@link normalizeDatabaseOperationResult}. Everything
1998
- * else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
1999
- * pipeline) is returned exactly as the server sent it.
2000
- */
2001
- async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
2002
- const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
2003
- const extraHeaders = {};
2004
- if (options?.timing) {
2005
- extraHeaders["X-Timing"] = "true";
2006
- }
2007
- if (token) {
2008
- return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
2009
- method: "POST",
2010
- body: JSON.stringify(data || {}),
2011
- headers: extraHeaders,
2012
- }));
2013
- }
2014
- return normalizeDatabaseOperationResult(await this.request(path, {
2015
- method: "POST",
2016
- body: JSON.stringify(data || {}),
2017
- headers: extraHeaders,
2018
- }));
2019
- }
2020
- /**
2021
- * Execute a registered batch (bulk) database operation. Posts a chunk of
2022
- * items to the canonical `operations/:name/batch` endpoint (the same one the
2023
- * client library's `executeBatch` uses — NOT the deprecated `import-bulk`
2024
- * alias). Used by `databases import-csv`.
2025
- *
2026
- * @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
2027
- * validation/access failures abort the whole chunk with a 4xx (thrown).
2028
- */
2029
- async executeBatch(appId, databaseId, operationName, batch) {
2030
- return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
2031
- }
2032
- /**
2033
- * Make a request using a specific JWT token instead of the CLI's credentials.
2034
- * Used for executing operations as a different user.
2035
- */
2036
- async requestWithToken(path, token, options = {}) {
2037
- const credentials = await this.ensureAuthenticated();
2038
- const url = `${credentials.serverUrl}${path}`;
2039
- const headers = {
2040
- Authorization: `Bearer ${token}`,
2041
- "Content-Type": "application/json",
2042
- ...(options.headers || {}),
2043
- };
2044
- const response = await fetchWithTLS(url, { ...options, headers });
2045
- const text = await response.text();
2046
- if (!response.ok) {
2047
- const parsed = parseErrorResponse(response, text, path);
2048
- throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
2049
- }
2050
- return text ? JSON.parse(text) : null;
2051
- }
2052
- // ============================================
2053
1877
  // DATABASE INDEXES
2054
1878
  // ============================================
2055
1879
  async listDatabaseIndexes(appId, databaseId, modelName) {
@@ -2082,7 +1906,7 @@ export class ApiClient {
2082
1906
  // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
2083
1907
  // validation (inline CEL rule lint, manifest shape, schema TOML parse)
2084
1908
  // WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
2085
- // malformed `defaultAccess`/manifest the same way the real POST would.
1909
+ // malformed rule or manifest the same way the real POST would.
2086
1910
  options) {
2087
1911
  const query = options?.dryRun ? "?dryRun=true" : "";
2088
1912
  return this.post(`/app/${appId}/api/databases/types${query}`, data);
@@ -2103,105 +1927,14 @@ export class ApiClient {
2103
1927
  return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
2104
1928
  }
2105
1929
  /**
2106
- * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
2107
- * existing ops + DO field introspection. Read-only — does NOT persist.
1930
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from the
1931
+ * type's databases (the models each holds, and their fields). Read-only —
1932
+ * does NOT persist.
2108
1933
  */
2109
1934
  async scaffoldDatabaseTypeSchema(appId, databaseType) {
2110
1935
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
2111
1936
  }
2112
1937
  // ============================================
2113
- // DATABASE TYPE OPERATIONS
2114
- // ============================================
2115
- async listDatabaseTypeOperations(appId, databaseType) {
2116
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
2117
- }
2118
- async getDatabaseTypeOperation(appId, databaseType, name) {
2119
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2120
- }
2121
- async createDatabaseTypeOperation(appId, databaseType, data,
2122
- // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
2123
- // without persisting — used by `config push --dry-run` to surface gate
2124
- // failures the real push would hit. `schemaOverride` lets the gate run
2125
- // against the schema the SAME push is about to land (so an op that depends
2126
- // on a new field isn't falsely rejected against the stale stored schema).
2127
- //
2128
- // Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
2129
- // does not exist on the server yet, so the server's "access required"
2130
- // check has no `defaultAccess` to fall back on. Thread the TOML-declared
2131
- // type-level `defaultAccess` so an op that omits `access` (intending to
2132
- // inherit it after the type is created) isn't falsely rejected during the
2133
- // dry-run. Server only trusts this on the fresh-type dry-run path.
2134
- options) {
2135
- const query = options?.dryRun ? "?dryRun=true" : "";
2136
- let body = data;
2137
- if (options?.dryRun && options.schemaOverride !== undefined) {
2138
- body = { ...body, schemaOverride: options.schemaOverride };
2139
- }
2140
- if (options?.dryRun && options.defaultAccess !== undefined) {
2141
- body = { ...body, defaultAccess: options.defaultAccess };
2142
- }
2143
- if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2144
- body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2145
- }
2146
- return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
2147
- }
2148
- async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
2149
- // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
2150
- // `schemaOverride` gates against the schema the same push is landing.
2151
- // Issue #1336: `metadataManifestOverride` gates the op-access lint against
2152
- // the manifest the same push is landing (see `createDatabaseTypeOperation`).
2153
- options) {
2154
- let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
2155
- if (options?.dryRun && options.schemaOverride !== undefined) {
2156
- body = { ...body, schemaOverride: options.schemaOverride };
2157
- }
2158
- if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2159
- body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2160
- }
2161
- const query = options?.dryRun ? "?dryRun=true" : "";
2162
- return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
2163
- }
2164
- async deleteDatabaseTypeOperation(appId, databaseType, name) {
2165
- return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2166
- }
2167
- // ============================================
2168
- // DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
2169
- // ============================================
2170
- //
2171
- // Transparent passthroughs against the server's type-scoped subscription
2172
- // routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
2173
- // The wire format is authoritative in the controller
2174
- // (`database-type-subscriptions-controller.ts`): create requires
2175
- // `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
2176
- // (CEL); optional `description`, `select` (string[]), `emit`
2177
- // (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
2178
- //
2179
- // The list endpoint wraps results in `{ items: [...] }` (unlike operations,
2180
- // which returns a bare array). We unwrap here so callers get an array,
2181
- // matching `listDatabaseTypeOperations`.
2182
- async listDatabaseTypeSubscriptions(appId, databaseType) {
2183
- const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
2184
- if (Array.isArray(result))
2185
- return result;
2186
- return Array.isArray(result?.items) ? result.items : [];
2187
- }
2188
- async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2189
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2190
- }
2191
- async createDatabaseTypeSubscription(appId, databaseType, data) {
2192
- return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
2193
- }
2194
- async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
2195
- // #2731 B10 — optimistic concurrency, as the sibling operation update has
2196
- // had. Omitted (by `--force`, and by every caller predating it) means the
2197
- // write stays unconditional.
2198
- expectedModifiedAt) {
2199
- return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
2200
- }
2201
- async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2202
- return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2203
- }
2204
- // ============================================
2205
1938
  // GROUPS
2206
1939
  // ============================================
2207
1940
  async listGroups(appId, params) {
@@ -2213,31 +1946,55 @@ export class ApiClient {
2213
1946
  hasMore: result?.hasMore ?? nextCursor != null,
2214
1947
  };
2215
1948
  }
1949
+ /** One page of one custodian's groups (`app` or `user:<userId>`), through the admin API. */
1950
+ async listGroupsByCustodian(appId, custodian, params) {
1951
+ const result = await this.get(`/admin/api/apps/${appId}/groups`, { custodian, ...(params ?? {}) });
1952
+ const nextCursor = result?.nextCursor;
1953
+ return {
1954
+ items: result?.items ?? [],
1955
+ nextCursor,
1956
+ hasMore: result?.hasMore ?? nextCursor != null,
1957
+ };
1958
+ }
1959
+ /** Transfer a group to `to` (`app` or `user:<userId>`) through the admin API. */
1960
+ async transferGroup(appId, groupId, to) {
1961
+ return this.post(`/admin/api/apps/${appId}/groups/${encodeURIComponent(groupId)}/transfer`, { to });
1962
+ }
1963
+ /**
1964
+ * Create a group through the admin API; `custodian` is `app` or
1965
+ * `user:<userId>`. The server mints the group's id and answers it as
1966
+ * `groupId`.
1967
+ */
2216
1968
  async createGroup(appId, data) {
2217
- return this.post(`/app/${appId}/api/groups`, data);
1969
+ return this.post(`/admin/api/apps/${appId}/groups`, data);
2218
1970
  }
2219
- async getGroup(appId, groupType, groupId) {
2220
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}`);
1971
+ /**
1972
+ * A group by id. `groupType` looks a group written before groups were
1973
+ * addressed by id up by its type first (the routes' deprecated
1974
+ * `?groupType=`), as an exported collection's grants still name them.
1975
+ */
1976
+ async getGroup(appId, groupId, options) {
1977
+ return this.get(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}`, options?.groupType ? { groupType: options.groupType } : undefined);
2221
1978
  }
2222
- async updateGroup(appId, groupType, groupId, data) {
2223
- return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}`, data);
1979
+ async updateGroup(appId, groupId, data) {
1980
+ return this.patch(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}`, data);
2224
1981
  }
2225
- async deleteGroup(appId, groupType, groupId) {
2226
- return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}`);
1982
+ async deleteGroup(appId, groupId) {
1983
+ return this.delete(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}`);
2227
1984
  }
2228
1985
  // ============================================
2229
1986
  // GROUP MEMBERS
2230
1987
  // ============================================
2231
1988
  /**
2232
- * One page of a group's members.
1989
+ * One page of a group's members, with their roles.
2233
1990
  *
2234
1991
  * The route paginates cursor-style (no default limit, cap 200) and reports
2235
1992
  * the boundary, so `groups members list` carries `--limit`/`--cursor` and
2236
1993
  * forwards them (#3646) rather than printing a first page that claims to be
2237
1994
  * the whole membership.
2238
1995
  */
2239
- async listGroupMembers(appId, groupType, groupId, options) {
2240
- const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, {
1996
+ async listGroupMembers(appId, groupId, options) {
1997
+ const result = await this.get(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}/members`, {
2241
1998
  ...(options?.include ? { include: options.include } : {}),
2242
1999
  ...(options?.limit !== undefined ? { limit: options.limit } : {}),
2243
2000
  ...(options?.cursor ? { cursor: options.cursor } : {}),
@@ -2249,17 +2006,23 @@ export class ApiClient {
2249
2006
  hasMore: result?.hasMore ?? nextCursor != null,
2250
2007
  };
2251
2008
  }
2252
- async addGroupMember(appId, groupType, groupId, data) {
2253
- return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
2009
+ /** Add a member through the admin API; `role: "owner"` restores the custodian's owner row. */
2010
+ async addGroupMember(appId, groupId, data) {
2011
+ return this.post(`/admin/api/apps/${appId}/groups/${encodeURIComponent(groupId)}/members`, data);
2012
+ }
2013
+ /** Change a member's role through the admin API, the owner row's included. */
2014
+ async setGroupMemberRole(appId, groupId, userId, role) {
2015
+ return this.patch(`/admin/api/apps/${appId}/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`, { role });
2254
2016
  }
2255
- async removeGroupMember(appId, groupType, groupId, userId) {
2256
- return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
2017
+ /** Remove a member through the admin API, the owner row included. */
2018
+ async removeGroupMember(appId, groupId, userId) {
2019
+ return this.delete(`/admin/api/apps/${appId}/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`);
2257
2020
  }
2258
2021
  // ============================================
2259
2022
  // GROUP RESOURCE LISTINGS
2260
2023
  // ============================================
2261
- async listGroupDocuments(appId, groupType, groupId) {
2262
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
2024
+ async listGroupDocuments(appId, groupId) {
2025
+ return this.get(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}/documents`);
2263
2026
  }
2264
2027
  // ============================================
2265
2028
  // USER MEMBERSHIPS
@@ -2313,6 +2076,33 @@ export class ApiClient {
2313
2076
  async createCollection(appId, data) {
2314
2077
  return this.post(`/app/${appId}/api/collections`, data);
2315
2078
  }
2079
+ /** One page of one custodian's collections (`app` or `user:<userId>`), through the admin API. */
2080
+ async listCollectionsByCustodian(appId, custodian, params) {
2081
+ const query = { custodian };
2082
+ if (params?.limit !== undefined)
2083
+ query.limit = params.limit;
2084
+ if (params?.cursor)
2085
+ query.cursor = params.cursor;
2086
+ const result = await this.get(`/admin/api/apps/${appId}/collections`, query);
2087
+ const nextCursor = result?.nextCursor;
2088
+ return {
2089
+ items: result?.items ?? [],
2090
+ nextCursor,
2091
+ hasMore: result?.hasMore ?? nextCursor != null,
2092
+ };
2093
+ }
2094
+ /** Transfer a collection to `to` (`app` or `user:<userId>`) through the admin API. */
2095
+ async transferCollection(appId, collectionId, to) {
2096
+ return this.post(`/admin/api/apps/${appId}/collections/${encodeURIComponent(collectionId)}/transfer`, { to });
2097
+ }
2098
+ /**
2099
+ * Create a collection through the admin API, for `custodian` (`app` or
2100
+ * `user:<userId>`; a user custodian holds the owner row). Spends no
2101
+ * create budget.
2102
+ */
2103
+ async createCollectionAsAdmin(appId, data) {
2104
+ return this.post(`/admin/api/apps/${appId}/collections`, data);
2105
+ }
2316
2106
  async getCollection(appId, collectionId) {
2317
2107
  return this.get(`/app/${appId}/api/collections/${collectionId}`);
2318
2108
  }
@@ -2359,41 +2149,30 @@ export class ApiClient {
2359
2149
  };
2360
2150
  }
2361
2151
  // ============================================
2362
- // COLLECTION ACCESS (GROUPS + MEMBERS)
2152
+ // COLLECTION MEMBERS (USERS + GROUPS)
2363
2153
  // ============================================
2364
- async getCollectionAccess(appId, collectionId) {
2365
- return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
2366
- }
2367
- async grantCollectionGroupPermission(appId, collectionId, data) {
2368
- return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
2369
- }
2370
- async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
2371
- return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
2154
+ /**
2155
+ * Every member of a collection, users and groups as principals with their
2156
+ * roles, in one answer.
2157
+ */
2158
+ async listCollectionMembers(appId, collectionId) {
2159
+ const result = await this.get(`/app/${appId}/api/collections/${encodeURIComponent(collectionId)}/members`);
2160
+ return { items: result?.items ?? [], hasMore: Boolean(result?.hasMore) };
2372
2161
  }
2162
+ /**
2163
+ * Add a user or a group to a collection through the admin API, with a role;
2164
+ * `role: "owner"` restores the custodian's owner row.
2165
+ */
2373
2166
  async addCollectionMember(appId, collectionId, data) {
2374
- return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
2375
- }
2376
- async removeCollectionMember(appId, collectionId, userId) {
2377
- return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
2378
- }
2379
- // ============================================
2380
- // COLLECTION TYPE CONFIGS
2381
- // ============================================
2382
- async listCollectionTypeConfigs(appId) {
2383
- return this.get(`/app/${appId}/api/collection-type-configs`);
2167
+ return this.post(`/admin/api/apps/${appId}/collections/${encodeURIComponent(collectionId)}/members`, data);
2384
2168
  }
2385
- async getCollectionTypeConfig(appId, collectionType) {
2386
- return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2169
+ /** Change a member's role through the admin API, the owner row's included. */
2170
+ async setCollectionMemberRole(appId, collectionId, principal, role) {
2171
+ return this.patch(`/admin/api/apps/${appId}/collections/${encodeURIComponent(collectionId)}/members/${encodeURIComponent(principal)}`, { role });
2387
2172
  }
2388
- async createCollectionTypeConfig(appId, data) {
2389
- return this.post(`/app/${appId}/api/collection-type-configs`, data);
2390
- }
2391
- async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
2392
- const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2393
- return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
2394
- }
2395
- async deleteCollectionTypeConfig(appId, collectionType) {
2396
- return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2173
+ /** Remove a user or `group:<groupId>` through the admin API, the owner row included. */
2174
+ async removeCollectionMember(appId, collectionId, principal) {
2175
+ return this.delete(`/admin/api/apps/${appId}/collections/${encodeURIComponent(collectionId)}/members/${encodeURIComponent(principal)}`);
2397
2176
  }
2398
2177
  // ============================================
2399
2178
  // METADATA CATEGORY CONFIGS (issue #1304, P-B)
@@ -2464,25 +2243,6 @@ export class ApiClient {
2464
2243
  return this.post(`/app/${appId}/api/rule-sets/debug`, data);
2465
2244
  }
2466
2245
  // ============================================
2467
- // GROUP TYPE CONFIGS
2468
- // ============================================
2469
- async listGroupTypeConfigs(appId) {
2470
- return this.get(`/app/${appId}/api/group-type-configs`);
2471
- }
2472
- async getGroupTypeConfig(appId, groupType) {
2473
- return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
2474
- }
2475
- async createGroupTypeConfig(appId, data) {
2476
- return this.post(`/app/${appId}/api/group-type-configs`, data);
2477
- }
2478
- async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
2479
- const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2480
- return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
2481
- }
2482
- async deleteGroupTypeConfig(appId, groupType) {
2483
- return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
2484
- }
2485
- // ============================================
2486
2246
  // DOCUMENT EXPORT / IMPORT
2487
2247
  // ============================================
2488
2248
  async exportDocumentState(appId, documentId) {
@@ -2519,51 +2279,6 @@ export class ApiClient {
2519
2279
  async importDocumentState(appId, documentId, stateBase64) {
2520
2280
  return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2521
2281
  }
2522
- /**
2523
- * Upload one artifact of a large document's import chain (#2816,
2524
- * behavior 20).
2525
- *
2526
- * The bytes travel raw and unmodified: the manifest's digest is over the
2527
- * STORED bytes, so re-encoding a chunk here would fail its own verification
2528
- * on install. The query says which artifact it is — the server derives the
2529
- * key from the document it belongs to, so nothing here composes one.
2530
- */
2531
- async uploadDocumentChainArtifact(appId, documentId, query, body) {
2532
- const credentials = await this.ensureAuthenticated();
2533
- const search = new URLSearchParams();
2534
- for (const [key, value] of Object.entries(query)) {
2535
- search.set(key, String(value));
2536
- }
2537
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2538
- `/import/chain/artifact?${search.toString()}`;
2539
- const headers = {
2540
- Authorization: `Bearer ${credentials.accessToken}`,
2541
- "Content-Type": "application/octet-stream",
2542
- };
2543
- if (credentials.globalAdminAppId) {
2544
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2545
- }
2546
- const response = await fetchWithTLS(url, {
2547
- method: "POST",
2548
- headers,
2549
- body: body,
2550
- });
2551
- if (!response.ok) {
2552
- // #3403 — through the shared parser: reading `error` alone dropped the
2553
- // envelope's `code` and `details`, so the refusal reached the operator
2554
- // (and `primitive documents import`) with nothing to branch on.
2555
- throw await blobFailure(response, "Failed to upload import artifact: ");
2556
- }
2557
- return await response.json().catch(() => ({}));
2558
- }
2559
- /** Start installing an uploaded chain (#2816, behavior 20). */
2560
- async installDocumentChain(appId, documentId, plan) {
2561
- return this.post(`/app/${appId}/api/documents/${documentId}/import/chain`, plan);
2562
- }
2563
- /** How far a document's chain install has got (#2816, behavior 20). */
2564
- async getDocumentChainInstall(appId, documentId) {
2565
- return this.get(`/app/${appId}/api/documents/${documentId}/import/chain`);
2566
- }
2567
2282
  async getDocument(appId, documentId) {
2568
2283
  return this.get(`/app/${appId}/api/documents/${documentId}`);
2569
2284
  }
@@ -2603,11 +2318,17 @@ export class ApiClient {
2603
2318
  * through the caller's document permission (reader+) or the app-admin arm.
2604
2319
  * Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
2605
2320
  * is an opaque token — feed `nextCursor` back as `cursor` for the next page.
2321
+ * `limit` is 100 records when omitted and at most 10000; it is forwarded
2322
+ * unparsed (a string from the command line) so the server validates it.
2606
2323
  */
2607
2324
  async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
2608
2325
  const query = {};
2609
2326
  if (queryOptions?.filter)
2610
2327
  query.filter = JSON.stringify(queryOptions.filter);
2328
+ if (queryOptions?.sort !== undefined)
2329
+ query.sort = JSON.stringify(queryOptions.sort);
2330
+ if (queryOptions?.include !== undefined)
2331
+ query.include = JSON.stringify(queryOptions.include);
2611
2332
  if (queryOptions?.documentFormat !== undefined) {
2612
2333
  query.documentFormat = queryOptions.documentFormat;
2613
2334
  }
@@ -2636,12 +2357,14 @@ export class ApiClient {
2636
2357
  /**
2637
2358
  * Aggregate records in a document model (issue #2437) — the documents twin
2638
2359
  * of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
2639
- * with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
2640
- * in the same shape as the database endpoint.
2360
+ * with `{ options: { operations, groupBy?, filter?, sort?, limit? } }`,
2361
+ * answering `{ result }` in the same shape as the database endpoint.
2641
2362
  *
2642
- * `groupBy` takes plain field names only: StringSet facet grouping and
2643
- * `{ field, contains }` membership grouping are database-only, which is why
2644
- * this signature is narrower than the databases one.
2363
+ * `groupBy` takes plain field names and period levels `{ field, period }`
2364
+ * (keyed `<field>_<period>` in a row's `group`). On a db3 (a format-3
2365
+ * document) a StringSet field name is a facet, `{ field, contains }` a
2366
+ * membership, and `sort` and `limit` order and bound the groups; a format-1
2367
+ * document refuses those four with a 400.
2645
2368
  */
2646
2369
  async aggregateDocumentRecords(appId, documentId, modelName, options, extra) {
2647
2370
  return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate` +
@@ -2654,6 +2377,23 @@ export class ApiClient {
2654
2377
  async getDocumentStats(appId, documentId) {
2655
2378
  return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
2656
2379
  }
2380
+ /**
2381
+ * A record-log document's room (#4306): `GET documents/:id/room` →
2382
+ * `{ ok, appId, documentId, documentFormat, room }`, `room` null for a
2383
+ * format that has no record room. With `refused` (#4310), the answer also
2384
+ * carries a page of that bulk load's refused rows. App admins only.
2385
+ */
2386
+ async getDocumentRoom(appId, documentId, query = {}) {
2387
+ const params = new URLSearchParams();
2388
+ if (query.refused !== undefined)
2389
+ params.set("refused", query.refused);
2390
+ if (query.limit !== undefined)
2391
+ params.set("limit", String(query.limit));
2392
+ if (query.cursor !== undefined)
2393
+ params.set("cursor", query.cursor);
2394
+ const search = params.toString();
2395
+ return this.get(`/app/${appId}/api/documents/${documentId}/room${search ? `?${search}` : ""}`);
2396
+ }
2657
2397
  /**
2658
2398
  * A large document's snapshot builds, newest first, one page (#3432).
2659
2399
  * `GET documents/:id/snapshots` → `{ items, hasMore, nextCursor? }`.
@@ -2687,9 +2427,13 @@ export class ApiClient {
2687
2427
  async getDocumentIngest(appId, documentId, sessionId) {
2688
2428
  return this.get(`/app/${appId}/api/documents/${documentId}/ingest/${encodeURIComponent(sessionId)}`);
2689
2429
  }
2690
- /** Open a bulk-load session on a large document (#3435, criterion 9). */
2691
- async createDocumentIngest(appId, documentId) {
2692
- return this.post(`/app/${appId}/api/documents/${documentId}/ingest`, {});
2430
+ /**
2431
+ * Open a bulk-load session on a large document (#3435, criterion 9). A
2432
+ * record-log (format 3) document's load takes `reserveEpoch`, `mode` and a
2433
+ * refresh's `models` (#4310); `{}` keeps every format's defaults.
2434
+ */
2435
+ async createDocumentIngest(appId, documentId, options = {}) {
2436
+ return this.post(`/app/${appId}/api/documents/${documentId}/ingest`, options);
2693
2437
  }
2694
2438
  /**
2695
2439
  * Upload one chunk of a bulk-load session's artifact (#3435).
@@ -2700,6 +2444,10 @@ export class ApiClient {
2700
2444
  * would name one the body does not use.
2701
2445
  */
2702
2446
  async uploadDocumentIngestChunk(appId, documentId, sessionId, chunk) {
2447
+ return this.uploadIngestChunkAt(`/app/${appId}/api/documents/${documentId}`, sessionId, chunk);
2448
+ }
2449
+ /** A chunk upload to `<base>/ingest/<session>/chunks`: the documents route or its db3 mount. */
2450
+ async uploadIngestChunkAt(base, sessionId, chunk) {
2703
2451
  const credentials = await this.ensureAuthenticated();
2704
2452
  const form = new FormData();
2705
2453
  form.set("model", chunk.model);
@@ -2711,18 +2459,15 @@ export class ApiClient {
2711
2459
  form.set("firstId", chunk.firstId);
2712
2460
  form.set("lastId", chunk.lastId);
2713
2461
  form.set("chunk", new Blob([chunk.body]), `${chunk.index}.ndjson.gz`);
2714
- const headers = {
2715
- Authorization: `Bearer ${credentials.accessToken}`,
2716
- };
2717
- if (credentials.globalAdminAppId) {
2718
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2719
- }
2720
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2721
- `/ingest/${encodeURIComponent(sessionId)}/chunks`;
2722
- const response = await fetchWithTLS(url, {
2723
- method: "POST",
2724
- headers,
2725
- body: form,
2462
+ const url = `${credentials.serverUrl}${base}/ingest/${encodeURIComponent(sessionId)}/chunks`;
2463
+ const response = await this.fetchWithStepUp(url, url, credentials.accessToken, (bearer) => {
2464
+ const headers = {
2465
+ Authorization: `Bearer ${bearer}`,
2466
+ };
2467
+ if (credentials.globalAdminAppId) {
2468
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2469
+ }
2470
+ return { method: "POST", headers, body: form };
2726
2471
  });
2727
2472
  const text = await response.text();
2728
2473
  let result = {};
@@ -2798,6 +2543,246 @@ export class ApiClient {
2798
2543
  return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk` +
2799
2544
  this.documentFormatQuery(extra?.documentFormat), { operations });
2800
2545
  }
2546
+ // ============================================
2547
+ // DB3 (format-3 documents)
2548
+ // ============================================
2549
+ /** `/app/<app>/api/db3/<id>`, the id encoded: every `db3.*` route's prefix. */
2550
+ db3Path(appId, db3Id) {
2551
+ return `/app/${appId}/api/db3/${encodeURIComponent(db3Id)}`;
2552
+ }
2553
+ /** `GET db3/:id/indexes` → `{ indexes }`, sorted by name. */
2554
+ async listDb3Indexes(appId, db3Id) {
2555
+ return this.get(`${this.db3Path(appId, db3Id)}/indexes`);
2556
+ }
2557
+ /**
2558
+ * `POST db3/:id/indexes` with `{ model, field | fields, unique?, name? }` →
2559
+ * `{ index }`. The route applies the registration rules.
2560
+ */
2561
+ async registerDb3Index(appId, db3Id, body) {
2562
+ return this.post(`${this.db3Path(appId, db3Id)}/indexes`, body);
2563
+ }
2564
+ /** `DELETE db3/:id/indexes/:name` → `{ ok, name }`. */
2565
+ async dropDb3Index(appId, db3Id, name) {
2566
+ return this.delete(`${this.db3Path(appId, db3Id)}/indexes/${encodeURIComponent(name)}`);
2567
+ }
2568
+ /** The db3 mount of {@link queryDocumentRecords}: `GET db3/:id/records/:model`. */
2569
+ async queryDb3Records(appId, db3Id, modelName, queryOptions) {
2570
+ const query = {};
2571
+ if (queryOptions?.filter)
2572
+ query.filter = JSON.stringify(queryOptions.filter);
2573
+ if (queryOptions?.sort !== undefined)
2574
+ query.sort = JSON.stringify(queryOptions.sort);
2575
+ if (queryOptions?.include !== undefined)
2576
+ query.include = JSON.stringify(queryOptions.include);
2577
+ if (queryOptions?.limit !== undefined)
2578
+ query.limit = queryOptions.limit;
2579
+ if (queryOptions?.cursor)
2580
+ query.cursor = queryOptions.cursor;
2581
+ const raw = await this.get(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}`, query);
2582
+ return normalizeCliListEnvelope(raw);
2583
+ }
2584
+ /** The db3 mount of {@link countDocumentRecords}: `GET db3/:id/records/:model/count`. */
2585
+ async countDb3Records(appId, db3Id, modelName, queryOptions) {
2586
+ const query = {};
2587
+ if (queryOptions?.filter)
2588
+ query.filter = JSON.stringify(queryOptions.filter);
2589
+ return this.get(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}/count`, query);
2590
+ }
2591
+ /** The db3 mount of {@link aggregateDocumentRecords}: `POST db3/:id/records/:model/aggregate`. */
2592
+ async aggregateDb3Records(appId, db3Id, modelName, options) {
2593
+ return this.post(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}/aggregate`, { options });
2594
+ }
2595
+ /** The db3 mount of {@link saveDocumentRecord}: `POST db3/:id/records/:model`. */
2596
+ async saveDb3Record(appId, db3Id, modelName, body) {
2597
+ return this.post(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}`, body);
2598
+ }
2599
+ /** The db3 mount of {@link patchDocumentRecord}: `PATCH db3/:id/records/:model/:recordId`. */
2600
+ async patchDb3Record(appId, db3Id, modelName, recordId, body) {
2601
+ return this.patch(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`, body);
2602
+ }
2603
+ /** The db3 mount of {@link deleteDocumentRecord}: `DELETE db3/:id/records/:model/:recordId`. */
2604
+ async deleteDb3Record(appId, db3Id, modelName, recordId) {
2605
+ return this.delete(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`);
2606
+ }
2607
+ /** The db3 mount of {@link bulkDocumentRecords}: `POST db3/:id/records/bulk`. */
2608
+ async bulkDb3Records(appId, db3Id, operations) {
2609
+ return this.post(`${this.db3Path(appId, db3Id)}/records/bulk`, { operations });
2610
+ }
2611
+ /** The db3 mount of {@link getDocumentSchema}: `GET db3/:id/schema`. */
2612
+ async getDb3Schema(appId, db3Id) {
2613
+ return this.get(`${this.db3Path(appId, db3Id)}/schema`);
2614
+ }
2615
+ /** `?limit=&cursor=` for a listing. */
2616
+ pageQuery(options = {}) {
2617
+ const params = new URLSearchParams();
2618
+ if (options.limit !== undefined)
2619
+ params.set("limit", String(options.limit));
2620
+ if (options.cursor)
2621
+ params.set("cursor", options.cursor);
2622
+ const query = params.toString();
2623
+ return query ? `?${query}` : "";
2624
+ }
2625
+ /** `GET db3/:id/ingest` (#4889): a db3's bulk-load sessions, newest first. */
2626
+ async listDb3Ingests(appId, db3Id, options = {}) {
2627
+ return this.get(`${this.db3Path(appId, db3Id)}/ingest${this.pageQuery(options)}`);
2628
+ }
2629
+ /** `GET db3/:id/ingest/:sessionId` (#4889). */
2630
+ async getDb3Ingest(appId, db3Id, sessionId) {
2631
+ return this.get(`${this.db3Path(appId, db3Id)}/ingest/${encodeURIComponent(sessionId)}`);
2632
+ }
2633
+ /** `POST db3/:id/ingest` (#4889): open a bulk-load session; `{}` keeps the defaults. */
2634
+ async createDb3Ingest(appId, db3Id, options = {}) {
2635
+ return this.post(`${this.db3Path(appId, db3Id)}/ingest`, options);
2636
+ }
2637
+ /** `POST db3/:id/ingest/:sessionId/chunks` (#4889): one chunk, multipart, as the documents upload sends it. */
2638
+ async uploadDb3IngestChunk(appId, db3Id, sessionId, chunk) {
2639
+ return this.uploadIngestChunkAt(this.db3Path(appId, db3Id), sessionId, chunk);
2640
+ }
2641
+ /** `POST db3/:id/ingest/:sessionId/commit` (#4889). */
2642
+ async commitDb3Ingest(appId, db3Id, sessionId) {
2643
+ return this.post(`${this.db3Path(appId, db3Id)}/ingest/${encodeURIComponent(sessionId)}/commit`, {});
2644
+ }
2645
+ /** `POST db3/:id/ingest/:sessionId/abort` (#4889). */
2646
+ async abortDb3Ingest(appId, db3Id, sessionId) {
2647
+ return this.post(`${this.db3Path(appId, db3Id)}/ingest/${encodeURIComponent(sessionId)}/abort`, {});
2648
+ }
2649
+ /** `GET db3/:id/snapshots` (#4889): a db3's segment exports, newest first. */
2650
+ async listDb3Snapshots(appId, db3Id, options = {}) {
2651
+ return this.get(`${this.db3Path(appId, db3Id)}/snapshots${this.pageQuery(options)}`);
2652
+ }
2653
+ /** `GET db3/:id/snapshots/:segmentId` (#4889). */
2654
+ async getDb3Snapshot(appId, db3Id, segmentId) {
2655
+ return this.get(`${this.db3Path(appId, db3Id)}/snapshots/${encodeURIComponent(segmentId)}`);
2656
+ }
2657
+ /** `POST db3/:id/snapshots` (#4889): start a segment export. */
2658
+ async requestDb3Snapshot(appId, db3Id) {
2659
+ return this.post(`${this.db3Path(appId, db3Id)}/snapshots`, {});
2660
+ }
2661
+ /** `GET db3/:id/export/chain` (#4889): the current segment, signed for download. */
2662
+ async exportDb3Chain(appId, db3Id) {
2663
+ return this.get(`${this.db3Path(appId, db3Id)}/export/chain`);
2664
+ }
2665
+ /** `GET db3/:id/blobs` (#4889): one page of a db3's blobs. */
2666
+ async listDb3Blobs(appId, db3Id, params) {
2667
+ return this.get(`${this.db3Path(appId, db3Id)}/blobs`, params);
2668
+ }
2669
+ /** `GET db3/:id/blobs/:blobId/download` (#4889): a blob's bytes. */
2670
+ async downloadDb3Blob(appId, db3Id, blobId) {
2671
+ return this.downloadBlobAt(this.db3Path(appId, db3Id), blobId);
2672
+ }
2673
+ /** `PUT db3/:id/blobs/:blobId` (#4889): a blob under its own id, as the documents upload sends it. */
2674
+ async uploadDb3Blob(appId, db3Id, blobId, data, meta) {
2675
+ return this.uploadBlobAt(this.db3Path(appId, db3Id), blobId, data, meta);
2676
+ }
2677
+ /** The db3 mount of {@link getDocumentRoom}: `GET db3/:id/room`. App admins only. */
2678
+ async getDb3Room(appId, db3Id, query = {}) {
2679
+ const params = new URLSearchParams();
2680
+ if (query.refused !== undefined)
2681
+ params.set("refused", query.refused);
2682
+ if (query.limit !== undefined)
2683
+ params.set("limit", String(query.limit));
2684
+ if (query.cursor !== undefined)
2685
+ params.set("cursor", query.cursor);
2686
+ const search = params.toString();
2687
+ return this.get(`${this.db3Path(appId, db3Id)}/room${search ? `?${search}` : ""}`);
2688
+ }
2689
+ /**
2690
+ * `POST db3` (#4804): create a db3 and answer it, `{ id, title, createdBy,
2691
+ * createdAt, modifiedAt, custodian, sync, type, tags }`. Without `custodian`
2692
+ * an admin token's create is held by the app. `id` keeps a caller-chosen id
2693
+ * (#4921), refused `DB3_ID_EXISTS` when the app holds it.
2694
+ */
2695
+ async createDb3(appId, body) {
2696
+ return this.post(`/app/${appId}/api/db3`, body);
2697
+ }
2698
+ /** `GET db3/:id` (#4804): the db3, with the caller's grant when it reads it through one. */
2699
+ async getDb3(appId, db3Id) {
2700
+ return this.get(this.db3Path(appId, db3Id));
2701
+ }
2702
+ /** `DELETE db3/:id` (#4804): the documents delete for a db3. */
2703
+ async deleteDb3(appId, db3Id) {
2704
+ return this.delete(this.db3Path(appId, db3Id));
2705
+ }
2706
+ /** The db3 mount of {@link getDocumentStats}: `GET db3/:id/stats`. */
2707
+ async getDb3Stats(appId, db3Id) {
2708
+ return this.get(`${this.db3Path(appId, db3Id)}/stats`);
2709
+ }
2710
+ /**
2711
+ * `POST db3/:id/records/:model/:recordId/increment` (#4797): add each of
2712
+ * `fields` to the record's field, answering `{ record, values }`.
2713
+ */
2714
+ async incrementDb3Record(appId, db3Id, modelName, recordId, body) {
2715
+ return this.post(`${this.db3Path(appId, db3Id)}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}/increment`, body);
2716
+ }
2717
+ /** `GET db3/:id/sync` (#4799): `{ id, sync }`. */
2718
+ async getDb3Sync(appId, db3Id) {
2719
+ return this.get(`${this.db3Path(appId, db3Id)}/sync`);
2720
+ }
2721
+ /** `PUT db3/:id/sync` (#4799): `{ id, sync, changed }`. */
2722
+ async setDb3Sync(appId, db3Id, sync) {
2723
+ return this.put(`${this.db3Path(appId, db3Id)}/sync`, { sync });
2724
+ }
2725
+ /** `GET db3/:id/app-access` (#4801): `{ id, level }`, the level every user of the app holds, or null. */
2726
+ async getDb3AppAccess(appId, db3Id) {
2727
+ return this.get(`${this.db3Path(appId, db3Id)}/app-access`);
2728
+ }
2729
+ /** `PUT db3/:id/app-access` (#4801): grant the db3 to every user of the app, or remove the grant with null. */
2730
+ async setDb3AppAccess(appId, db3Id, level) {
2731
+ return this.put(`${this.db3Path(appId, db3Id)}/app-access`, { level });
2732
+ }
2733
+ /** One custodian's db3s through the admin API (#4800): `custodian` is `app` or `user:<userId>`. */
2734
+ async listDb3ByCustodian(appId, custodian, params) {
2735
+ const result = await this.get(`/admin/api/apps/${appId}/db3`, { custodian, ...(params ?? {}) });
2736
+ const nextCursor = result?.nextCursor;
2737
+ return { items: result?.items ?? [], nextCursor, hasMore: result?.hasMore ?? nextCursor != null };
2738
+ }
2739
+ /** Transfer a db3 to `to` (`app` or `user:<userId>`) through the admin API (#4804). */
2740
+ async transferDb3(appId, db3Id, to) {
2741
+ return this.post(`/admin/api/apps/${appId}/db3/${encodeURIComponent(db3Id)}/transfer`, { to });
2742
+ }
2743
+ /** Remove or restore a db3's owner row through the admin API (#4804): `{ id, custodian, ownerRow, changed }`. */
2744
+ async setDb3OwnerRow(appId, db3Id, present) {
2745
+ return this.put(`/admin/api/apps/${appId}/db3/${encodeURIComponent(db3Id)}/owner-row`, { present });
2746
+ }
2747
+ /** `GET db3/:id/type` (#4802): the type the db3's row names, its revision, and what its room holds. */
2748
+ async getDb3Type(appId, db3Id) {
2749
+ return this.get(`${this.db3Path(appId, db3Id)}/type`);
2750
+ }
2751
+ /** `PUT db3/:id/type` (#4802): attach a type, installing its current revision whole or refusing it whole. */
2752
+ async attachDb3Type(appId, db3Id, type) {
2753
+ return this.put(`${this.db3Path(appId, db3Id)}/type`, { type });
2754
+ }
2755
+ /** `/app/<app>/api/db3-types/<name>`, the name encoded (#4802). */
2756
+ db3TypePath(appId, name) {
2757
+ return `/app/${appId}/api/db3-types/${encodeURIComponent(name)}`;
2758
+ }
2759
+ /** `GET db3-types` (#4802): the app's db3 types, each with its revision. */
2760
+ async listDb3Types(appId) {
2761
+ return this.get(`/app/${appId}/api/db3-types`);
2762
+ }
2763
+ /** `GET db3-types/:name` (#4802). */
2764
+ async getDb3TypeConfig(appId, name) {
2765
+ return this.get(this.db3TypePath(appId, name));
2766
+ }
2767
+ /**
2768
+ * `PUT db3-types/:name` (#4802): create or update the type, and push its new
2769
+ * revision to every db3 of it. `dryRun` judges the body and persists nothing.
2770
+ */
2771
+ async pushDb3Type(appId, name, data, options) {
2772
+ return this.put(`${this.db3TypePath(appId, name)}${options?.dryRun ? "?dryRun=true" : ""}`, data);
2773
+ }
2774
+ /** `DELETE db3-types/:name` (#4802): refused while a db3 is of the type. */
2775
+ async deleteDb3Type(appId, name) {
2776
+ return this.delete(this.db3TypePath(appId, name));
2777
+ }
2778
+ /** `GET db3-types/:name/push-status` (#4802): a push's outcome per db3; the latest without `runId`. */
2779
+ async getDb3TypePushStatus(appId, name, runId) {
2780
+ return this.get(`${this.db3TypePath(appId, name)}/push-status${runId ? `?runId=${encodeURIComponent(runId)}` : ""}`);
2781
+ }
2782
+ /** `POST db3-types/:name/push?runId=` (#4802): push the current revision again to a run's refused and failed db3s. */
2783
+ async retryDb3TypePush(appId, name, runId) {
2784
+ return this.post(`${this.db3TypePath(appId, name)}/push?runId=${encodeURIComponent(runId)}`, {});
2785
+ }
2801
2786
  async createDocument(appId, data) {
2802
2787
  return this.post(`/app/${appId}/api/documents`, data);
2803
2788
  }
@@ -2853,12 +2838,21 @@ export class ApiClient {
2853
2838
  async listDocumentPendingInvitations(appId, documentId) {
2854
2839
  return this.get(`/app/${appId}/api/documents/${documentId}/pending-invitations`);
2855
2840
  }
2856
- async listDocumentBlobs(appId, documentId) {
2857
- return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
2841
+ /**
2842
+ * One page of a document's uploaded blobs (the `{ items, hasMore,
2843
+ * nextCursor? }` envelope); pass the previous page's `nextCursor` for the
2844
+ * next.
2845
+ */
2846
+ async listDocumentBlobs(appId, documentId, params) {
2847
+ return this.get(`/app/${appId}/api/documents/${documentId}/blobs`, params);
2858
2848
  }
2859
2849
  async downloadBlob(appId, documentId, blobId) {
2850
+ return this.downloadBlobAt(`/app/${appId}/api/documents/${documentId}`, blobId);
2851
+ }
2852
+ /** A blob's bytes from `<base>/blobs/<id>/download`: the documents route or its db3 mount. */
2853
+ async downloadBlobAt(base, blobId) {
2860
2854
  const credentials = await this.ensureAuthenticated();
2861
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
2855
+ const url = `${credentials.serverUrl}${base}/blobs/${blobId}/download`;
2862
2856
  const headers = {
2863
2857
  Authorization: `Bearer ${credentials.accessToken}`,
2864
2858
  };
@@ -2873,8 +2867,37 @@ export class ApiClient {
2873
2867
  return Buffer.from(arrayBuffer);
2874
2868
  }
2875
2869
  async uploadBlob(appId, documentId, blobId, data, meta) {
2870
+ return this.uploadBlobAt(`/app/${appId}/api/documents/${documentId}`, blobId, data, meta);
2871
+ }
2872
+ /** A blob PUT to `<base>/blobs/<id>`: the documents route or its db3 mount. */
2873
+ async uploadBlobAt(base, blobId, data, meta) {
2874
+ const credentials = await this.ensureAuthenticated();
2875
+ const url = `${credentials.serverUrl}${base}/blobs/${blobId}`;
2876
+ const response = await this.fetchWithStepUp(url, url, credentials.accessToken, (bearer) => {
2877
+ const headers = {
2878
+ Authorization: `Bearer ${bearer}`,
2879
+ "Content-Type": meta.contentType,
2880
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
2881
+ "X-Blob-Size": String(data.length),
2882
+ "X-Blob-Sha256": meta.sha256,
2883
+ };
2884
+ if (credentials.globalAdminAppId) {
2885
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2886
+ }
2887
+ return { method: "PUT", headers, body: data };
2888
+ });
2889
+ if (!response.ok) {
2890
+ throw await blobFailure(response, "Failed to upload blob: ");
2891
+ }
2892
+ return response.json();
2893
+ }
2894
+ /**
2895
+ * Upload a file to an agent session by `sessionId` (#4585): the document
2896
+ * blob upload's headers and body, on the session's attachment route.
2897
+ */
2898
+ async uploadAgentSessionAttachment(appId, sessionId, blobId, data, meta) {
2876
2899
  const credentials = await this.ensureAuthenticated();
2877
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
2900
+ const url = `${credentials.serverUrl}/app/${appId}/api/agent-sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(blobId)}`;
2878
2901
  const headers = {
2879
2902
  Authorization: `Bearer ${credentials.accessToken}`,
2880
2903
  "Content-Type": meta.contentType,
@@ -2885,19 +2908,64 @@ export class ApiClient {
2885
2908
  if (credentials.globalAdminAppId) {
2886
2909
  headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2887
2910
  }
2888
- const response = await fetchWithTLS(url, {
2889
- method: "PUT",
2890
- headers,
2891
- body: data,
2892
- });
2911
+ const response = await fetchWithTLS(url, { method: "PUT", headers, body: data });
2893
2912
  if (!response.ok) {
2894
- throw await blobFailure(response, "Failed to upload blob: ");
2913
+ throw await blobFailure(response, "Failed to upload attachment: ");
2895
2914
  }
2896
2915
  return response.json();
2897
2916
  }
2917
+ /** Download an agent session's file by `sessionId` (#4585). */
2918
+ async downloadAgentSessionAttachment(appId, sessionId, blobId) {
2919
+ const response = await this.agentSessionAttachmentRead("GET", appId, sessionId, blobId);
2920
+ if (!response.ok) {
2921
+ throw await blobFailure(response, "Failed to download attachment: ");
2922
+ }
2923
+ return Buffer.from(await response.arrayBuffer());
2924
+ }
2925
+ /**
2926
+ * An agent session file's download headers (lower-cased names) by
2927
+ * `sessionId` (#4585). A refusal has no body, so it fails with the status
2928
+ * alone and no code.
2929
+ */
2930
+ async headAgentSessionAttachment(appId, sessionId, blobId) {
2931
+ const response = await this.agentSessionAttachmentRead("HEAD", appId, sessionId, blobId);
2932
+ if (!response.ok && response.status !== 304) {
2933
+ throw new ApiError(`Failed to read attachment headers: HTTP ${response.status}`, response.status);
2934
+ }
2935
+ const headers = {};
2936
+ response.headers.forEach((value, name) => {
2937
+ headers[name.toLowerCase()] = value;
2938
+ });
2939
+ return { status: response.status, headers };
2940
+ }
2941
+ async agentSessionAttachmentRead(method, appId, sessionId, blobId) {
2942
+ const credentials = await this.ensureAuthenticated();
2943
+ const url = `${credentials.serverUrl}/app/${appId}/api/agent-sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(blobId)}/download`;
2944
+ const headers = {
2945
+ Authorization: `Bearer ${credentials.accessToken}`,
2946
+ };
2947
+ if (credentials.globalAdminAppId) {
2948
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2949
+ }
2950
+ return fetchWithTLS(url, { method, headers });
2951
+ }
2898
2952
  async listDocumentAliases(appId, documentId) {
2899
2953
  return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
2900
2954
  }
2955
+ /**
2956
+ * The document a user's alias points at, or null when the user holds no
2957
+ * alias under that key (#4148).
2958
+ */
2959
+ async getDocumentAlias(appId, aliasScope, aliasKey, userId) {
2960
+ try {
2961
+ return await this.get(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, { userId });
2962
+ }
2963
+ catch (error) {
2964
+ if (error instanceof ApiError && error.statusCode === 404)
2965
+ return null;
2966
+ throw error;
2967
+ }
2968
+ }
2901
2969
  async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
2902
2970
  return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
2903
2971
  documentId,
@@ -3042,21 +3110,19 @@ export class ApiClient {
3042
3110
  async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
3043
3111
  const credentials = await this.ensureAuthenticated();
3044
3112
  const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
3045
- const headers = {
3046
- Authorization: `Bearer ${credentials.accessToken}`,
3047
- "Content-Type": meta.contentType,
3048
- "X-Blob-Filename": encodeURIComponent(meta.filename),
3049
- };
3050
- if (meta.tags && meta.tags.length > 0) {
3051
- headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
3052
- }
3053
- if (credentials.globalAdminAppId) {
3054
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
3055
- }
3056
- const response = await fetchWithTLS(url, {
3057
- method: "POST",
3058
- headers,
3059
- body: data,
3113
+ const response = await this.fetchWithStepUp(url, url, credentials.accessToken, (bearer) => {
3114
+ const headers = {
3115
+ Authorization: `Bearer ${bearer}`,
3116
+ "Content-Type": meta.contentType,
3117
+ "X-Blob-Filename": encodeURIComponent(meta.filename),
3118
+ };
3119
+ if (meta.tags && meta.tags.length > 0) {
3120
+ headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
3121
+ }
3122
+ if (credentials.globalAdminAppId) {
3123
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
3124
+ }
3125
+ return { method: "POST", headers, body: data };
3060
3126
  });
3061
3127
  if (!response.ok) {
3062
3128
  throw await blobFailure(response, "Failed to upload blob: ");