primitive-admin 1.2.0-alpha.0 → 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 (412) hide show
  1. package/README.md +108 -136
  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 +12 -17
  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 +228 -566
  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/sessions.js +3 -8
  67. package/dist/src/commands/sessions.js.map +1 -1
  68. package/dist/src/commands/sync-app-settings.d.ts +36 -8
  69. package/dist/src/commands/sync-app-settings.js +109 -23
  70. package/dist/src/commands/sync-app-settings.js.map +1 -1
  71. package/dist/src/commands/sync.d.ts +436 -613
  72. package/dist/src/commands/sync.js +2604 -5977
  73. package/dist/src/commands/sync.js.map +1 -1
  74. package/dist/src/commands/tokens.js +4 -9
  75. package/dist/src/commands/tokens.js.map +1 -1
  76. package/dist/src/commands/users.js +77 -2
  77. package/dist/src/commands/users.js.map +1 -1
  78. package/dist/src/commands/workflows.d.ts +5 -83
  79. package/dist/src/commands/workflows.js +57 -2282
  80. package/dist/src/commands/workflows.js.map +1 -1
  81. package/dist/src/lib/aggregate-output.d.ts +25 -0
  82. package/dist/src/lib/aggregate-output.js +53 -0
  83. package/dist/src/lib/aggregate-output.js.map +1 -0
  84. package/dist/src/lib/api-client.d.ts +681 -581
  85. package/dist/src/lib/api-client.js +880 -851
  86. package/dist/src/lib/api-client.js.map +1 -1
  87. package/dist/src/lib/app-settings-descriptor.d.ts +47 -1
  88. package/dist/src/lib/app-settings-descriptor.js +114 -10
  89. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  90. package/dist/src/lib/auth-fixtures.d.ts +44 -0
  91. package/dist/src/lib/auth-fixtures.js +61 -0
  92. package/dist/src/lib/auth-fixtures.js.map +1 -0
  93. package/dist/src/lib/auth-flow.d.ts +41 -1
  94. package/dist/src/lib/auth-flow.js +70 -16
  95. package/dist/src/lib/auth-flow.js.map +1 -1
  96. package/dist/src/lib/block-layout.d.ts +12 -23
  97. package/dist/src/lib/block-layout.js +26 -92
  98. package/dist/src/lib/block-layout.js.map +1 -1
  99. package/dist/src/lib/block-selector.d.ts +2 -2
  100. package/dist/src/lib/block-selector.js +2 -2
  101. package/dist/src/lib/child-apps-local.d.ts +164 -0
  102. package/dist/src/lib/child-apps-local.js +258 -0
  103. package/dist/src/lib/child-apps-local.js.map +1 -0
  104. package/dist/src/lib/ci-session-request.d.ts +22 -0
  105. package/dist/src/lib/ci-session-request.js +51 -0
  106. package/dist/src/lib/ci-session-request.js.map +1 -0
  107. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +10 -8
  108. package/dist/src/lib/codegen-shared/generatedFiles.js +21 -15
  109. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  110. package/dist/src/lib/codegen-shared/prettierStable.d.ts +1 -1
  111. package/dist/src/lib/codegen-shared/prettierStable.js +1 -1
  112. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +2 -2
  113. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +2 -2
  114. package/dist/src/lib/collection-export.d.ts +76 -15
  115. package/dist/src/lib/collection-export.js +116 -23
  116. package/dist/src/lib/collection-export.js.map +1 -1
  117. package/dist/src/lib/collection-principal.d.ts +22 -0
  118. package/dist/src/lib/collection-principal.js +35 -0
  119. package/dist/src/lib/collection-principal.js.map +1 -0
  120. package/dist/src/lib/config-object-descriptor.d.ts +3 -22
  121. package/dist/src/lib/config-object-descriptor.js +56 -291
  122. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  123. package/dist/src/lib/config-payload.js +2 -3
  124. package/dist/src/lib/config-payload.js.map +1 -1
  125. package/dist/src/lib/config-surface.d.ts +5 -7
  126. package/dist/src/lib/config-surface.js +18 -40
  127. package/dist/src/lib/config-surface.js.map +1 -1
  128. package/dist/src/lib/connections-list.d.ts +10 -0
  129. package/dist/src/lib/connections-list.js +18 -0
  130. package/dist/src/lib/connections-list.js.map +1 -0
  131. package/dist/src/lib/credentials-home.d.ts +58 -0
  132. package/dist/src/lib/credentials-home.js +204 -0
  133. package/dist/src/lib/credentials-home.js.map +1 -0
  134. package/dist/src/lib/credentials-lock.d.ts +88 -0
  135. package/dist/src/lib/credentials-lock.js +302 -0
  136. package/dist/src/lib/credentials-lock.js.map +1 -0
  137. package/dist/src/lib/credentials-store.d.ts +179 -24
  138. package/dist/src/lib/credentials-store.js +712 -77
  139. package/dist/src/lib/credentials-store.js.map +1 -1
  140. package/dist/src/lib/csv.d.ts +8 -9
  141. package/dist/src/lib/csv.js +3 -4
  142. package/dist/src/lib/csv.js.map +1 -1
  143. package/dist/src/lib/custodian.d.ts +8 -0
  144. package/dist/src/lib/custodian.js +18 -0
  145. package/dist/src/lib/custodian.js.map +1 -0
  146. package/dist/src/lib/db-codegen/dbGenerator.js +5 -16
  147. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
  148. package/dist/src/lib/db-codegen/dbNaming.d.ts +5 -13
  149. package/dist/src/lib/db-codegen/dbNaming.js +5 -17
  150. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
  151. package/dist/src/lib/db-codegen/dbTemplates.d.ts +7 -56
  152. package/dist/src/lib/db-codegen/dbTemplates.js +25 -125
  153. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  154. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +2 -2
  155. package/dist/src/lib/db-codegen/dbTsTypes.js +2 -2
  156. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +67 -2
  157. package/dist/src/lib/db-codegen/dbTypeIR.js +13 -2
  158. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
  159. package/dist/src/lib/db3-export.d.ts +231 -0
  160. package/dist/src/lib/db3-export.js +510 -0
  161. package/dist/src/lib/db3-export.js.map +1 -0
  162. package/dist/src/lib/db3-index-output.d.ts +27 -0
  163. package/dist/src/lib/db3-index-output.js +49 -0
  164. package/dist/src/lib/db3-index-output.js.map +1 -0
  165. package/dist/src/lib/document-export-lookups.d.ts +31 -0
  166. package/dist/src/lib/document-export-lookups.js +53 -0
  167. package/dist/src/lib/document-export-lookups.js.map +1 -0
  168. package/dist/src/lib/document-export-permissions.d.ts +7 -5
  169. package/dist/src/lib/document-export-permissions.js +7 -5
  170. package/dist/src/lib/document-export-permissions.js.map +1 -1
  171. package/dist/src/lib/document-export-record-log.d.ts +57 -0
  172. package/dist/src/lib/document-export-record-log.js +215 -0
  173. package/dist/src/lib/document-export-record-log.js.map +1 -0
  174. package/dist/src/lib/document-ingest-artifact.d.ts +26 -2
  175. package/dist/src/lib/document-ingest-artifact.js +42 -8
  176. package/dist/src/lib/document-ingest-artifact.js.map +1 -1
  177. package/dist/src/lib/document-ingest-format2.d.ts +36 -0
  178. package/dist/src/lib/document-ingest-format2.js +78 -0
  179. package/dist/src/lib/document-ingest-format2.js.map +1 -0
  180. package/dist/src/lib/document-ingest-input.d.ts +9 -0
  181. package/dist/src/lib/document-ingest-input.js +28 -1
  182. package/dist/src/lib/document-ingest-input.js.map +1 -1
  183. package/dist/src/lib/document-ingest-rows.d.ts +16 -0
  184. package/dist/src/lib/document-ingest-rows.js +17 -0
  185. package/dist/src/lib/document-ingest-rows.js.map +1 -1
  186. package/dist/src/lib/document-ingest.d.ts +33 -1
  187. package/dist/src/lib/document-ingest.js +49 -0
  188. package/dist/src/lib/document-ingest.js.map +1 -1
  189. package/dist/src/lib/document-schema-rows.d.ts +42 -0
  190. package/dist/src/lib/document-schema-rows.js +53 -0
  191. package/dist/src/lib/document-schema-rows.js.map +1 -0
  192. package/dist/src/lib/env-resolver-core.d.ts +70 -2
  193. package/dist/src/lib/env-resolver-core.js +137 -20
  194. package/dist/src/lib/env-resolver-core.js.map +1 -1
  195. package/dist/src/lib/env-resolver.d.ts +14 -2
  196. package/dist/src/lib/env-resolver.js +32 -10
  197. package/dist/src/lib/env-resolver.js.map +1 -1
  198. package/dist/src/lib/function-authorize-check.d.ts +142 -0
  199. package/dist/src/lib/function-authorize-check.js +310 -0
  200. package/dist/src/lib/function-authorize-check.js.map +1 -0
  201. package/dist/src/lib/function-authorize-view.d.ts +15 -0
  202. package/dist/src/lib/function-authorize-view.js +50 -0
  203. package/dist/src/lib/function-authorize-view.js.map +1 -0
  204. package/dist/src/lib/function-bundle.d.ts +38 -18
  205. package/dist/src/lib/function-bundle.js +96 -35
  206. package/dist/src/lib/function-bundle.js.map +1 -1
  207. package/dist/src/lib/function-collect.d.ts +75 -12
  208. package/dist/src/lib/function-collect.js +427 -12
  209. package/dist/src/lib/function-collect.js.map +1 -1
  210. package/dist/src/lib/function-db-types.d.ts +29 -0
  211. package/dist/src/lib/function-db-types.js +117 -7
  212. package/dist/src/lib/function-db-types.js.map +1 -1
  213. package/dist/src/lib/function-document-types.d.ts +5 -3
  214. package/dist/src/lib/function-document-types.js +10 -14
  215. package/dist/src/lib/function-document-types.js.map +1 -1
  216. package/dist/src/lib/function-lint.d.ts +34 -0
  217. package/dist/src/lib/function-lint.js +112 -0
  218. package/dist/src/lib/function-lint.js.map +1 -0
  219. package/dist/src/lib/function-log-row.js +21 -2
  220. package/dist/src/lib/function-log-row.js.map +1 -1
  221. package/dist/src/lib/function-run.d.ts +16 -21
  222. package/dist/src/lib/function-run.js +37 -30
  223. package/dist/src/lib/function-run.js.map +1 -1
  224. package/dist/src/lib/function-schema-codegen.d.ts +1 -1
  225. package/dist/src/lib/function-schema-codegen.js +5 -5
  226. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  227. package/dist/src/lib/function-source-roots.d.ts +67 -0
  228. package/dist/src/lib/function-source-roots.js +148 -0
  229. package/dist/src/lib/function-source-roots.js.map +1 -0
  230. package/dist/src/lib/function-sync.d.ts +108 -22
  231. package/dist/src/lib/function-sync.js +127 -39
  232. package/dist/src/lib/function-sync.js.map +1 -1
  233. package/dist/src/lib/function-trigger-listing.d.ts +11 -6
  234. package/dist/src/lib/function-trigger-listing.js +32 -11
  235. package/dist/src/lib/function-trigger-listing.js.map +1 -1
  236. package/dist/src/lib/function-typecheck.d.ts +37 -2
  237. package/dist/src/lib/function-typecheck.js +210 -28
  238. package/dist/src/lib/function-typecheck.js.map +1 -1
  239. package/dist/src/lib/function-versions.d.ts +2 -0
  240. package/dist/src/lib/function-versions.js +1 -0
  241. package/dist/src/lib/function-versions.js.map +1 -1
  242. package/dist/src/lib/generated-allowlist.js +0 -20
  243. package/dist/src/lib/generated-allowlist.js.map +1 -1
  244. package/dist/src/lib/generated-config-surfaces.d.ts +703 -201
  245. package/dist/src/lib/generated-config-surfaces.js +2335 -2033
  246. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  247. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  248. package/dist/src/lib/generated-sdk-types.js +1 -1
  249. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  250. package/dist/src/lib/init-adopt.js +12 -3
  251. package/dist/src/lib/init-adopt.js.map +1 -1
  252. package/dist/src/lib/init-login-note.d.ts +13 -0
  253. package/dist/src/lib/init-login-note.js +22 -0
  254. package/dist/src/lib/init-login-note.js.map +1 -0
  255. package/dist/src/lib/json-value.d.ts +6 -0
  256. package/dist/src/lib/json-value.js +13 -0
  257. package/dist/src/lib/json-value.js.map +1 -0
  258. package/dist/src/lib/load-routes.d.ts +33 -0
  259. package/dist/src/lib/load-routes.js +35 -0
  260. package/dist/src/lib/load-routes.js.map +1 -0
  261. package/dist/src/lib/local-state.d.ts +38 -7
  262. package/dist/src/lib/local-state.js +172 -49
  263. package/dist/src/lib/local-state.js.map +1 -1
  264. package/dist/src/lib/local-test-cases.d.ts +5 -2
  265. package/dist/src/lib/local-test-cases.js +7 -4
  266. package/dist/src/lib/local-test-cases.js.map +1 -1
  267. package/dist/src/lib/log-inspection.d.ts +42 -4
  268. package/dist/src/lib/log-inspection.js +37 -5
  269. package/dist/src/lib/log-inspection.js.map +1 -1
  270. package/dist/src/lib/login-exchange-check.d.ts +18 -0
  271. package/dist/src/lib/login-exchange-check.js +27 -0
  272. package/dist/src/lib/login-exchange-check.js.map +1 -0
  273. package/dist/src/lib/login-scope-lines.d.ts +25 -0
  274. package/dist/src/lib/login-scope-lines.js +45 -0
  275. package/dist/src/lib/login-scope-lines.js.map +1 -0
  276. package/dist/src/lib/logout-admin-session.d.ts +2 -1
  277. package/dist/src/lib/logout-admin-session.js +7 -1
  278. package/dist/src/lib/logout-admin-session.js.map +1 -1
  279. package/dist/src/lib/object-status-filter.d.ts +9 -1
  280. package/dist/src/lib/object-status-filter.js +9 -1
  281. package/dist/src/lib/object-status-filter.js.map +1 -1
  282. package/dist/src/lib/output.js +2 -0
  283. package/dist/src/lib/output.js.map +1 -1
  284. package/dist/src/lib/paginate.d.ts +0 -5
  285. package/dist/src/lib/paginate.js +0 -5
  286. package/dist/src/lib/paginate.js.map +1 -1
  287. package/dist/src/lib/pin-refusal-hint.d.ts +12 -0
  288. package/dist/src/lib/pin-refusal-hint.js +35 -0
  289. package/dist/src/lib/pin-refusal-hint.js.map +1 -0
  290. package/dist/src/lib/primitive-env-plugin.d.ts +99 -0
  291. package/dist/src/lib/primitive-env-plugin.js +408 -0
  292. package/dist/src/lib/primitive-env-plugin.js.map +1 -0
  293. package/dist/src/lib/project-config.d.ts +7 -0
  294. package/dist/src/lib/project-config.js +22 -1
  295. package/dist/src/lib/project-config.js.map +1 -1
  296. package/dist/src/lib/project-login-apps.d.ts +8 -0
  297. package/dist/src/lib/project-login-apps.js +27 -0
  298. package/dist/src/lib/project-login-apps.js.map +1 -0
  299. package/dist/src/lib/prompt-schema-codegen.d.ts +10 -3
  300. package/dist/src/lib/prompt-schema-codegen.js +52 -11
  301. package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
  302. package/dist/src/lib/pull-write.d.ts +31 -0
  303. package/dist/src/lib/pull-write.js +49 -0
  304. package/dist/src/lib/pull-write.js.map +1 -0
  305. package/dist/src/lib/push-failure-report.d.ts +45 -0
  306. package/dist/src/lib/push-failure-report.js +57 -0
  307. package/dist/src/lib/push-failure-report.js.map +1 -0
  308. package/dist/src/lib/records-verbs.d.ts +69 -0
  309. package/dist/src/lib/records-verbs.js +40 -0
  310. package/dist/src/lib/records-verbs.js.map +1 -0
  311. package/dist/src/lib/refresh-admin-credentials.d.ts +7 -0
  312. package/dist/src/lib/refresh-admin-credentials.js +16 -4
  313. package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
  314. package/dist/src/lib/root-import-decision.d.ts +65 -0
  315. package/dist/src/lib/root-import-decision.js +109 -0
  316. package/dist/src/lib/root-import-decision.js.map +1 -0
  317. package/dist/src/lib/run-status.d.ts +1 -1
  318. package/dist/src/lib/run-status.generated.d.ts +2 -2
  319. package/dist/src/lib/run-status.generated.js +3 -1
  320. package/dist/src/lib/run-status.generated.js.map +1 -1
  321. package/dist/src/lib/run-status.js +1 -1
  322. package/dist/src/lib/schema-codegen/generated-schema-descriptor.d.ts +129 -0
  323. package/dist/src/lib/schema-codegen/generated-schema-descriptor.js +269 -0
  324. package/dist/src/lib/schema-codegen/generated-schema-descriptor.js.map +1 -0
  325. package/dist/src/lib/schema-codegen/naming.d.ts +35 -0
  326. package/dist/src/lib/schema-codegen/naming.js +83 -0
  327. package/dist/src/lib/schema-codegen/naming.js.map +1 -0
  328. package/dist/src/lib/schema-codegen/schemaToTs.d.ts +82 -0
  329. package/dist/src/lib/schema-codegen/schemaToTs.js +305 -0
  330. package/dist/src/lib/schema-codegen/schemaToTs.js.map +1 -0
  331. package/dist/src/lib/scope-request.d.ts +84 -0
  332. package/dist/src/lib/scope-request.js +235 -0
  333. package/dist/src/lib/scope-request.js.map +1 -0
  334. package/dist/src/lib/session-id.d.ts +9 -0
  335. package/dist/src/lib/session-id.js +28 -0
  336. package/dist/src/lib/session-id.js.map +1 -0
  337. package/dist/src/lib/snapshot-build-rows.d.ts +21 -0
  338. package/dist/src/lib/snapshot-build-rows.js +22 -0
  339. package/dist/src/lib/snapshot-build-rows.js.map +1 -1
  340. package/dist/src/lib/snapshot-build.js +8 -3
  341. package/dist/src/lib/snapshot-build.js.map +1 -1
  342. package/dist/src/lib/snapshots.d.ts +17 -1
  343. package/dist/src/lib/snapshots.js +50 -2
  344. package/dist/src/lib/snapshots.js.map +1 -1
  345. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +6 -2
  346. package/dist/src/lib/swift-codegen/agentGenerator.js +57 -5
  347. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -1
  348. package/dist/src/lib/swift-codegen/banners.d.ts +34 -7
  349. package/dist/src/lib/swift-codegen/banners.js +37 -7
  350. package/dist/src/lib/swift-codegen/banners.js.map +1 -1
  351. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +4 -3
  352. package/dist/src/lib/swift-codegen/dbGenerator.js +14 -117
  353. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  354. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +3 -3
  355. package/dist/src/lib/swift-codegen/functionGenerator.js +23 -18
  356. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  357. package/dist/src/lib/swift-codegen/generator.d.ts +5 -5
  358. package/dist/src/lib/swift-codegen/generator.js +35 -40
  359. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  360. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +3 -3
  361. package/dist/src/lib/swift-codegen/schemaToSwift.js +4 -4
  362. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
  363. package/dist/src/lib/swift-codegen/swiftKeys.d.ts +19 -0
  364. package/dist/src/lib/swift-codegen/swiftKeys.js +31 -0
  365. package/dist/src/lib/swift-codegen/swiftKeys.js.map +1 -0
  366. package/dist/src/lib/sync-paths.d.ts +35 -0
  367. package/dist/src/lib/sync-paths.js +57 -5
  368. package/dist/src/lib/sync-paths.js.map +1 -1
  369. package/dist/src/lib/sync-resource-types.d.ts +26 -26
  370. package/dist/src/lib/sync-resource-types.js +61 -189
  371. package/dist/src/lib/sync-resource-types.js.map +1 -1
  372. package/dist/src/lib/sync-selectors.d.ts +5 -1
  373. package/dist/src/lib/sync-selectors.js +23 -13
  374. package/dist/src/lib/sync-selectors.js.map +1 -1
  375. package/dist/src/lib/test-case-file-names.d.ts +3 -5
  376. package/dist/src/lib/test-case-file-names.js +3 -5
  377. package/dist/src/lib/test-case-file-names.js.map +1 -1
  378. package/dist/src/lib/toml-database-config.d.ts +15 -75
  379. package/dist/src/lib/toml-database-config.js +17 -306
  380. package/dist/src/lib/toml-database-config.js.map +1 -1
  381. package/dist/src/lib/transient-retry.d.ts +43 -0
  382. package/dist/src/lib/transient-retry.js +94 -0
  383. package/dist/src/lib/transient-retry.js.map +1 -0
  384. package/dist/src/lib/vars-override.d.ts +105 -0
  385. package/dist/src/lib/vars-override.js +143 -0
  386. package/dist/src/lib/vars-override.js.map +1 -0
  387. package/dist/src/lib/webhook-capture.d.ts +43 -0
  388. package/dist/src/lib/webhook-capture.js +91 -0
  389. package/dist/src/lib/webhook-capture.js.map +1 -0
  390. package/dist/src/lib/webhook-deliver.d.ts +18 -20
  391. package/dist/src/lib/webhook-deliver.js +34 -89
  392. package/dist/src/lib/webhook-deliver.js.map +1 -1
  393. package/dist/src/lib/webhook-test-run.d.ts +32 -0
  394. package/dist/src/lib/webhook-test-run.js +108 -0
  395. package/dist/src/lib/webhook-test-run.js.map +1 -0
  396. package/dist/src/lib/workflow-codegen/generator.d.ts +7 -7
  397. package/dist/src/lib/workflow-codegen/generator.js +16 -23
  398. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  399. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +6 -12
  400. package/dist/src/lib/workflow-codegen/invokerIR.js +4 -5
  401. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  402. package/dist/src/lib/workflow-toml-validator.d.ts +2 -0
  403. package/dist/src/lib/workflow-toml-validator.js +28 -1
  404. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  405. package/dist/src/types/index.d.ts +30 -37
  406. package/dist/src/validators.d.ts +7 -55
  407. package/dist/src/validators.js +7 -53
  408. package/dist/src/validators.js.map +1 -1
  409. package/dist/src/vite.d.ts +17 -0
  410. package/dist/src/vite.js +18 -0
  411. package/dist/src/vite.js.map +1 -0
  412. 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
  }
@@ -1890,43 +1780,6 @@ export class ApiClient {
1890
1780
  async deleteDatabase(appId, databaseId) {
1891
1781
  return this.delete(`/app/${appId}/api/databases/${databaseId}`);
1892
1782
  }
1893
- /**
1894
- * Read a database's CEL context dict.
1895
- *
1896
- * The HTTP path stays `/metadata` because the wire field name is still
1897
- * `metadata`; only the client/CLI-facing helper names were reframed.
1898
- *
1899
- * @deprecated Use resource metadata categories instead (`primitive metadata
1900
- * get database <id> <category>`, #1420, #1815).
1901
- */
1902
- async getDatabaseCelContext(appId, databaseId) {
1903
- return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
1904
- }
1905
- /**
1906
- * Update a database's CEL context dict (merge with existing).
1907
- *
1908
- * @deprecated Use resource metadata categories instead (`primitive metadata
1909
- * set database <id> <category>`, #1420, #1815).
1910
- */
1911
- async updateDatabaseCelContext(appId, databaseId, celContext) {
1912
- return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
1913
- }
1914
- /**
1915
- * @deprecated Database CEL context is deprecated; use resource metadata
1916
- * categories instead (`primitive metadata get database <id> <category>`,
1917
- * #1420, #1815).
1918
- */
1919
- async getDatabaseMetadata(appId, databaseId) {
1920
- return this.getDatabaseCelContext(appId, databaseId);
1921
- }
1922
- /**
1923
- * @deprecated Database CEL context is deprecated; use resource metadata
1924
- * categories instead (`primitive metadata set database <id> <category>`,
1925
- * #1420, #1815).
1926
- */
1927
- async updateDatabaseMetadata(appId, databaseId, metadata) {
1928
- return this.updateDatabaseCelContext(appId, databaseId, metadata);
1929
- }
1930
1783
  // ============================================
1931
1784
  // DATABASE PERMISSIONS
1932
1785
  // ============================================
@@ -2021,72 +1874,6 @@ export class ApiClient {
2021
1874
  });
2022
1875
  }
2023
1876
  // ============================================
2024
- // DATABASE OPERATIONS (registered operations)
2025
- // ============================================
2026
- async listDatabaseOperations(appId, databaseId) {
2027
- return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
2028
- }
2029
- /**
2030
- * Execute a registered operation and, when the result is a query page,
2031
- * normalize it to the CLI's one list envelope (issue #2440).
2032
- *
2033
- * The registered operation decides the response shape, so the normalization
2034
- * is conditional — see {@link normalizeDatabaseOperationResult}. Everything
2035
- * else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
2036
- * pipeline) is returned exactly as the server sent it.
2037
- */
2038
- async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
2039
- const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
2040
- const extraHeaders = {};
2041
- if (options?.timing) {
2042
- extraHeaders["X-Timing"] = "true";
2043
- }
2044
- if (token) {
2045
- return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
2046
- method: "POST",
2047
- body: JSON.stringify(data || {}),
2048
- headers: extraHeaders,
2049
- }));
2050
- }
2051
- return normalizeDatabaseOperationResult(await this.request(path, {
2052
- method: "POST",
2053
- body: JSON.stringify(data || {}),
2054
- headers: extraHeaders,
2055
- }));
2056
- }
2057
- /**
2058
- * Execute a registered batch (bulk) database operation. Posts a chunk of
2059
- * items to the canonical `operations/:name/batch` endpoint (the same one the
2060
- * client library's `executeBatch` uses — NOT the deprecated `import-bulk`
2061
- * alias). Used by `databases import-csv`.
2062
- *
2063
- * @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
2064
- * validation/access failures abort the whole chunk with a 4xx (thrown).
2065
- */
2066
- async executeBatch(appId, databaseId, operationName, batch) {
2067
- return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
2068
- }
2069
- /**
2070
- * Make a request using a specific JWT token instead of the CLI's credentials.
2071
- * Used for executing operations as a different user.
2072
- */
2073
- async requestWithToken(path, token, options = {}) {
2074
- const credentials = await this.ensureAuthenticated();
2075
- const url = `${credentials.serverUrl}${path}`;
2076
- const headers = {
2077
- Authorization: `Bearer ${token}`,
2078
- "Content-Type": "application/json",
2079
- ...(options.headers || {}),
2080
- };
2081
- const response = await fetchWithTLS(url, { ...options, headers });
2082
- const text = await response.text();
2083
- if (!response.ok) {
2084
- const parsed = parseErrorResponse(response, text, path);
2085
- throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
2086
- }
2087
- return text ? JSON.parse(text) : null;
2088
- }
2089
- // ============================================
2090
1877
  // DATABASE INDEXES
2091
1878
  // ============================================
2092
1879
  async listDatabaseIndexes(appId, databaseId, modelName) {
@@ -2119,7 +1906,7 @@ export class ApiClient {
2119
1906
  // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
2120
1907
  // validation (inline CEL rule lint, manifest shape, schema TOML parse)
2121
1908
  // WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
2122
- // malformed `defaultAccess`/manifest the same way the real POST would.
1909
+ // malformed rule or manifest the same way the real POST would.
2123
1910
  options) {
2124
1911
  const query = options?.dryRun ? "?dryRun=true" : "";
2125
1912
  return this.post(`/app/${appId}/api/databases/types${query}`, data);
@@ -2140,105 +1927,14 @@ export class ApiClient {
2140
1927
  return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
2141
1928
  }
2142
1929
  /**
2143
- * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
2144
- * 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.
2145
1933
  */
2146
1934
  async scaffoldDatabaseTypeSchema(appId, databaseType) {
2147
1935
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
2148
1936
  }
2149
1937
  // ============================================
2150
- // DATABASE TYPE OPERATIONS
2151
- // ============================================
2152
- async listDatabaseTypeOperations(appId, databaseType) {
2153
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
2154
- }
2155
- async getDatabaseTypeOperation(appId, databaseType, name) {
2156
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2157
- }
2158
- async createDatabaseTypeOperation(appId, databaseType, data,
2159
- // Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
2160
- // without persisting — used by `config push --dry-run` to surface gate
2161
- // failures the real push would hit. `schemaOverride` lets the gate run
2162
- // against the schema the SAME push is about to land (so an op that depends
2163
- // on a new field isn't falsely rejected against the stale stored schema).
2164
- //
2165
- // Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
2166
- // does not exist on the server yet, so the server's "access required"
2167
- // check has no `defaultAccess` to fall back on. Thread the TOML-declared
2168
- // type-level `defaultAccess` so an op that omits `access` (intending to
2169
- // inherit it after the type is created) isn't falsely rejected during the
2170
- // dry-run. Server only trusts this on the fresh-type dry-run path.
2171
- options) {
2172
- const query = options?.dryRun ? "?dryRun=true" : "";
2173
- let body = data;
2174
- if (options?.dryRun && options.schemaOverride !== undefined) {
2175
- body = { ...body, schemaOverride: options.schemaOverride };
2176
- }
2177
- if (options?.dryRun && options.defaultAccess !== undefined) {
2178
- body = { ...body, defaultAccess: options.defaultAccess };
2179
- }
2180
- if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2181
- body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2182
- }
2183
- return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
2184
- }
2185
- async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
2186
- // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
2187
- // `schemaOverride` gates against the schema the same push is landing.
2188
- // Issue #1336: `metadataManifestOverride` gates the op-access lint against
2189
- // the manifest the same push is landing (see `createDatabaseTypeOperation`).
2190
- options) {
2191
- let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
2192
- if (options?.dryRun && options.schemaOverride !== undefined) {
2193
- body = { ...body, schemaOverride: options.schemaOverride };
2194
- }
2195
- if (options?.dryRun && options.metadataManifestOverride !== undefined) {
2196
- body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
2197
- }
2198
- const query = options?.dryRun ? "?dryRun=true" : "";
2199
- return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
2200
- }
2201
- async deleteDatabaseTypeOperation(appId, databaseType, name) {
2202
- return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
2203
- }
2204
- // ============================================
2205
- // DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
2206
- // ============================================
2207
- //
2208
- // Transparent passthroughs against the server's type-scoped subscription
2209
- // routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
2210
- // The wire format is authoritative in the controller
2211
- // (`database-type-subscriptions-controller.ts`): create requires
2212
- // `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
2213
- // (CEL); optional `description`, `select` (string[]), `emit`
2214
- // (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
2215
- //
2216
- // The list endpoint wraps results in `{ items: [...] }` (unlike operations,
2217
- // which returns a bare array). We unwrap here so callers get an array,
2218
- // matching `listDatabaseTypeOperations`.
2219
- async listDatabaseTypeSubscriptions(appId, databaseType) {
2220
- const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
2221
- if (Array.isArray(result))
2222
- return result;
2223
- return Array.isArray(result?.items) ? result.items : [];
2224
- }
2225
- async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2226
- return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2227
- }
2228
- async createDatabaseTypeSubscription(appId, databaseType, data) {
2229
- return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
2230
- }
2231
- async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
2232
- // #2731 B10 — optimistic concurrency, as the sibling operation update has
2233
- // had. Omitted (by `--force`, and by every caller predating it) means the
2234
- // write stays unconditional.
2235
- expectedModifiedAt) {
2236
- return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
2237
- }
2238
- async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
2239
- return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
2240
- }
2241
- // ============================================
2242
1938
  // GROUPS
2243
1939
  // ============================================
2244
1940
  async listGroups(appId, params) {
@@ -2250,31 +1946,55 @@ export class ApiClient {
2250
1946
  hasMore: result?.hasMore ?? nextCursor != null,
2251
1947
  };
2252
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
+ */
2253
1968
  async createGroup(appId, data) {
2254
- return this.post(`/app/${appId}/api/groups`, data);
1969
+ return this.post(`/admin/api/apps/${appId}/groups`, data);
2255
1970
  }
2256
- async getGroup(appId, groupType, groupId) {
2257
- 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);
2258
1978
  }
2259
- async updateGroup(appId, groupType, groupId, data) {
2260
- 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);
2261
1981
  }
2262
- async deleteGroup(appId, groupType, groupId) {
2263
- return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}`);
1982
+ async deleteGroup(appId, groupId) {
1983
+ return this.delete(`/app/${appId}/api/groups/${encodeURIComponent(groupId)}`);
2264
1984
  }
2265
1985
  // ============================================
2266
1986
  // GROUP MEMBERS
2267
1987
  // ============================================
2268
1988
  /**
2269
- * One page of a group's members.
1989
+ * One page of a group's members, with their roles.
2270
1990
  *
2271
1991
  * The route paginates cursor-style (no default limit, cap 200) and reports
2272
1992
  * the boundary, so `groups members list` carries `--limit`/`--cursor` and
2273
1993
  * forwards them (#3646) rather than printing a first page that claims to be
2274
1994
  * the whole membership.
2275
1995
  */
2276
- async listGroupMembers(appId, groupType, groupId, options) {
2277
- 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`, {
2278
1998
  ...(options?.include ? { include: options.include } : {}),
2279
1999
  ...(options?.limit !== undefined ? { limit: options.limit } : {}),
2280
2000
  ...(options?.cursor ? { cursor: options.cursor } : {}),
@@ -2286,17 +2006,23 @@ export class ApiClient {
2286
2006
  hasMore: result?.hasMore ?? nextCursor != null,
2287
2007
  };
2288
2008
  }
2289
- async addGroupMember(appId, groupType, groupId, data) {
2290
- 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);
2291
2012
  }
2292
- async removeGroupMember(appId, groupType, groupId, userId) {
2293
- return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
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 });
2016
+ }
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)}`);
2294
2020
  }
2295
2021
  // ============================================
2296
2022
  // GROUP RESOURCE LISTINGS
2297
2023
  // ============================================
2298
- async listGroupDocuments(appId, groupType, groupId) {
2299
- 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`);
2300
2026
  }
2301
2027
  // ============================================
2302
2028
  // USER MEMBERSHIPS
@@ -2350,6 +2076,33 @@ export class ApiClient {
2350
2076
  async createCollection(appId, data) {
2351
2077
  return this.post(`/app/${appId}/api/collections`, data);
2352
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
+ }
2353
2106
  async getCollection(appId, collectionId) {
2354
2107
  return this.get(`/app/${appId}/api/collections/${collectionId}`);
2355
2108
  }
@@ -2396,41 +2149,30 @@ export class ApiClient {
2396
2149
  };
2397
2150
  }
2398
2151
  // ============================================
2399
- // COLLECTION ACCESS (GROUPS + MEMBERS)
2152
+ // COLLECTION MEMBERS (USERS + GROUPS)
2400
2153
  // ============================================
2401
- async getCollectionAccess(appId, collectionId) {
2402
- return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
2403
- }
2404
- async grantCollectionGroupPermission(appId, collectionId, data) {
2405
- return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
2406
- }
2407
- async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
2408
- 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) };
2409
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
+ */
2410
2166
  async addCollectionMember(appId, collectionId, data) {
2411
- return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
2412
- }
2413
- async removeCollectionMember(appId, collectionId, userId) {
2414
- return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
2167
+ return this.post(`/admin/api/apps/${appId}/collections/${encodeURIComponent(collectionId)}/members`, data);
2415
2168
  }
2416
- // ============================================
2417
- // COLLECTION TYPE CONFIGS
2418
- // ============================================
2419
- async listCollectionTypeConfigs(appId) {
2420
- return this.get(`/app/${appId}/api/collection-type-configs`);
2421
- }
2422
- async getCollectionTypeConfig(appId, collectionType) {
2423
- return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
2424
- }
2425
- async createCollectionTypeConfig(appId, data) {
2426
- return this.post(`/app/${appId}/api/collection-type-configs`, data);
2427
- }
2428
- async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
2429
- const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2430
- return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
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 });
2431
2172
  }
2432
- async deleteCollectionTypeConfig(appId, collectionType) {
2433
- 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)}`);
2434
2176
  }
2435
2177
  // ============================================
2436
2178
  // METADATA CATEGORY CONFIGS (issue #1304, P-B)
@@ -2501,25 +2243,6 @@ export class ApiClient {
2501
2243
  return this.post(`/app/${appId}/api/rule-sets/debug`, data);
2502
2244
  }
2503
2245
  // ============================================
2504
- // GROUP TYPE CONFIGS
2505
- // ============================================
2506
- async listGroupTypeConfigs(appId) {
2507
- return this.get(`/app/${appId}/api/group-type-configs`);
2508
- }
2509
- async getGroupTypeConfig(appId, groupType) {
2510
- return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
2511
- }
2512
- async createGroupTypeConfig(appId, data) {
2513
- return this.post(`/app/${appId}/api/group-type-configs`, data);
2514
- }
2515
- async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
2516
- const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
2517
- return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
2518
- }
2519
- async deleteGroupTypeConfig(appId, groupType) {
2520
- return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
2521
- }
2522
- // ============================================
2523
2246
  // DOCUMENT EXPORT / IMPORT
2524
2247
  // ============================================
2525
2248
  async exportDocumentState(appId, documentId) {
@@ -2556,51 +2279,6 @@ export class ApiClient {
2556
2279
  async importDocumentState(appId, documentId, stateBase64) {
2557
2280
  return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2558
2281
  }
2559
- /**
2560
- * Upload one artifact of a large document's import chain (#2816,
2561
- * behavior 20).
2562
- *
2563
- * The bytes travel raw and unmodified: the manifest's digest is over the
2564
- * STORED bytes, so re-encoding a chunk here would fail its own verification
2565
- * on install. The query says which artifact it is — the server derives the
2566
- * key from the document it belongs to, so nothing here composes one.
2567
- */
2568
- async uploadDocumentChainArtifact(appId, documentId, query, body) {
2569
- const credentials = await this.ensureAuthenticated();
2570
- const search = new URLSearchParams();
2571
- for (const [key, value] of Object.entries(query)) {
2572
- search.set(key, String(value));
2573
- }
2574
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2575
- `/import/chain/artifact?${search.toString()}`;
2576
- const headers = {
2577
- Authorization: `Bearer ${credentials.accessToken}`,
2578
- "Content-Type": "application/octet-stream",
2579
- };
2580
- if (credentials.globalAdminAppId) {
2581
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2582
- }
2583
- const response = await fetchWithTLS(url, {
2584
- method: "POST",
2585
- headers,
2586
- body: body,
2587
- });
2588
- if (!response.ok) {
2589
- // #3403 — through the shared parser: reading `error` alone dropped the
2590
- // envelope's `code` and `details`, so the refusal reached the operator
2591
- // (and `primitive documents import`) with nothing to branch on.
2592
- throw await blobFailure(response, "Failed to upload import artifact: ");
2593
- }
2594
- return await response.json().catch(() => ({}));
2595
- }
2596
- /** Start installing an uploaded chain (#2816, behavior 20). */
2597
- async installDocumentChain(appId, documentId, plan) {
2598
- return this.post(`/app/${appId}/api/documents/${documentId}/import/chain`, plan);
2599
- }
2600
- /** How far a document's chain install has got (#2816, behavior 20). */
2601
- async getDocumentChainInstall(appId, documentId) {
2602
- return this.get(`/app/${appId}/api/documents/${documentId}/import/chain`);
2603
- }
2604
2282
  async getDocument(appId, documentId) {
2605
2283
  return this.get(`/app/${appId}/api/documents/${documentId}`);
2606
2284
  }
@@ -2640,11 +2318,17 @@ export class ApiClient {
2640
2318
  * through the caller's document permission (reader+) or the app-admin arm.
2641
2319
  * Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
2642
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.
2643
2323
  */
2644
2324
  async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
2645
2325
  const query = {};
2646
2326
  if (queryOptions?.filter)
2647
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);
2648
2332
  if (queryOptions?.documentFormat !== undefined) {
2649
2333
  query.documentFormat = queryOptions.documentFormat;
2650
2334
  }
@@ -2673,12 +2357,14 @@ export class ApiClient {
2673
2357
  /**
2674
2358
  * Aggregate records in a document model (issue #2437) — the documents twin
2675
2359
  * of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
2676
- * with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
2677
- * 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.
2678
2362
  *
2679
- * `groupBy` takes plain field names only: StringSet facet grouping and
2680
- * `{ field, contains }` membership grouping are database-only, which is why
2681
- * 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.
2682
2368
  */
2683
2369
  async aggregateDocumentRecords(appId, documentId, modelName, options, extra) {
2684
2370
  return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate` +
@@ -2691,6 +2377,23 @@ export class ApiClient {
2691
2377
  async getDocumentStats(appId, documentId) {
2692
2378
  return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
2693
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
+ }
2694
2397
  /**
2695
2398
  * A large document's snapshot builds, newest first, one page (#3432).
2696
2399
  * `GET documents/:id/snapshots` → `{ items, hasMore, nextCursor? }`.
@@ -2724,9 +2427,13 @@ export class ApiClient {
2724
2427
  async getDocumentIngest(appId, documentId, sessionId) {
2725
2428
  return this.get(`/app/${appId}/api/documents/${documentId}/ingest/${encodeURIComponent(sessionId)}`);
2726
2429
  }
2727
- /** Open a bulk-load session on a large document (#3435, criterion 9). */
2728
- async createDocumentIngest(appId, documentId) {
2729
- 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);
2730
2437
  }
2731
2438
  /**
2732
2439
  * Upload one chunk of a bulk-load session's artifact (#3435).
@@ -2737,6 +2444,10 @@ export class ApiClient {
2737
2444
  * would name one the body does not use.
2738
2445
  */
2739
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) {
2740
2451
  const credentials = await this.ensureAuthenticated();
2741
2452
  const form = new FormData();
2742
2453
  form.set("model", chunk.model);
@@ -2748,18 +2459,15 @@ export class ApiClient {
2748
2459
  form.set("firstId", chunk.firstId);
2749
2460
  form.set("lastId", chunk.lastId);
2750
2461
  form.set("chunk", new Blob([chunk.body]), `${chunk.index}.ndjson.gz`);
2751
- const headers = {
2752
- Authorization: `Bearer ${credentials.accessToken}`,
2753
- };
2754
- if (credentials.globalAdminAppId) {
2755
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2756
- }
2757
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2758
- `/ingest/${encodeURIComponent(sessionId)}/chunks`;
2759
- const response = await fetchWithTLS(url, {
2760
- method: "POST",
2761
- headers,
2762
- 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 };
2763
2471
  });
2764
2472
  const text = await response.text();
2765
2473
  let result = {};
@@ -2835,6 +2543,246 @@ export class ApiClient {
2835
2543
  return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk` +
2836
2544
  this.documentFormatQuery(extra?.documentFormat), { operations });
2837
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
+ }
2838
2786
  async createDocument(appId, data) {
2839
2787
  return this.post(`/app/${appId}/api/documents`, data);
2840
2788
  }
@@ -2890,12 +2838,21 @@ export class ApiClient {
2890
2838
  async listDocumentPendingInvitations(appId, documentId) {
2891
2839
  return this.get(`/app/${appId}/api/documents/${documentId}/pending-invitations`);
2892
2840
  }
2893
- async listDocumentBlobs(appId, documentId) {
2894
- 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);
2895
2848
  }
2896
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) {
2897
2854
  const credentials = await this.ensureAuthenticated();
2898
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
2855
+ const url = `${credentials.serverUrl}${base}/blobs/${blobId}/download`;
2899
2856
  const headers = {
2900
2857
  Authorization: `Bearer ${credentials.accessToken}`,
2901
2858
  };
@@ -2910,8 +2867,37 @@ export class ApiClient {
2910
2867
  return Buffer.from(arrayBuffer);
2911
2868
  }
2912
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) {
2913
2874
  const credentials = await this.ensureAuthenticated();
2914
- const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
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) {
2899
+ const credentials = await this.ensureAuthenticated();
2900
+ const url = `${credentials.serverUrl}/app/${appId}/api/agent-sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(blobId)}`;
2915
2901
  const headers = {
2916
2902
  Authorization: `Bearer ${credentials.accessToken}`,
2917
2903
  "Content-Type": meta.contentType,
@@ -2922,19 +2908,64 @@ export class ApiClient {
2922
2908
  if (credentials.globalAdminAppId) {
2923
2909
  headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2924
2910
  }
2925
- const response = await fetchWithTLS(url, {
2926
- method: "PUT",
2927
- headers,
2928
- body: data,
2929
- });
2911
+ const response = await fetchWithTLS(url, { method: "PUT", headers, body: data });
2930
2912
  if (!response.ok) {
2931
- throw await blobFailure(response, "Failed to upload blob: ");
2913
+ throw await blobFailure(response, "Failed to upload attachment: ");
2932
2914
  }
2933
2915
  return response.json();
2934
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
+ }
2935
2952
  async listDocumentAliases(appId, documentId) {
2936
2953
  return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
2937
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
+ }
2938
2969
  async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
2939
2970
  return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
2940
2971
  documentId,
@@ -3079,21 +3110,19 @@ export class ApiClient {
3079
3110
  async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
3080
3111
  const credentials = await this.ensureAuthenticated();
3081
3112
  const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
3082
- const headers = {
3083
- Authorization: `Bearer ${credentials.accessToken}`,
3084
- "Content-Type": meta.contentType,
3085
- "X-Blob-Filename": encodeURIComponent(meta.filename),
3086
- };
3087
- if (meta.tags && meta.tags.length > 0) {
3088
- headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
3089
- }
3090
- if (credentials.globalAdminAppId) {
3091
- headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
3092
- }
3093
- const response = await fetchWithTLS(url, {
3094
- method: "POST",
3095
- headers,
3096
- 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 };
3097
3126
  });
3098
3127
  if (!response.ok) {
3099
3128
  throw await blobFailure(response, "Failed to upload blob: ");