okengine 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (510) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +4 -1
  7. package/site/content/docs/ai/try-it.mdx +58 -0
  8. package/site/content/docs/client/auth.mdx +208 -0
  9. package/site/content/docs/client/calling.mdx +451 -0
  10. package/site/content/docs/client/index.mdx +209 -0
  11. package/site/content/docs/client/live.mdx +234 -0
  12. package/site/content/docs/client/meta.json +5 -0
  13. package/site/content/docs/client/react.mdx +249 -0
  14. package/site/content/docs/elements/ai/agents.mdx +232 -0
  15. package/site/content/docs/elements/ai/index.mdx +341 -0
  16. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  17. package/site/content/docs/elements/ai/meta.json +5 -0
  18. package/site/content/docs/elements/ai/models.mdx +283 -0
  19. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  20. package/site/content/docs/elements/channel/email.mdx +266 -0
  21. package/site/content/docs/elements/channel/index.mdx +344 -0
  22. package/site/content/docs/elements/channel/meta.json +5 -0
  23. package/site/content/docs/elements/channel/push.mdx +218 -0
  24. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  25. package/site/content/docs/elements/channel/sms.mdx +264 -0
  26. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  27. package/site/content/docs/elements/clock/index.mdx +300 -0
  28. package/site/content/docs/elements/clock/meta.json +5 -0
  29. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  30. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  31. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  32. package/site/content/docs/elements/flow/http.mdx +1080 -0
  33. package/site/content/docs/elements/flow/index.mdx +618 -0
  34. package/site/content/docs/elements/flow/meta.json +5 -0
  35. package/site/content/docs/elements/flow/routing.mdx +559 -0
  36. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  37. package/site/content/docs/elements/gate/auth.mdx +371 -0
  38. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  39. package/site/content/docs/elements/gate/index.mdx +418 -0
  40. package/site/content/docs/elements/gate/meta.json +5 -0
  41. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  42. package/site/content/docs/elements/gate/rls.mdx +303 -0
  43. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  44. package/site/content/docs/elements/index.mdx +20 -10
  45. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  46. package/site/content/docs/elements/signal/index.mdx +352 -0
  47. package/site/content/docs/elements/signal/live.mdx +590 -0
  48. package/site/content/docs/elements/signal/meta.json +5 -0
  49. package/site/content/docs/elements/signal/once.mdx +596 -0
  50. package/site/content/docs/elements/store/files.mdx +659 -0
  51. package/site/content/docs/elements/store/index.mdx +310 -0
  52. package/site/content/docs/elements/store/kv.mdx +629 -0
  53. package/site/content/docs/elements/store/meta.json +5 -0
  54. package/site/content/docs/elements/store/search.mdx +946 -0
  55. package/site/content/docs/elements/store/sql.mdx +937 -0
  56. package/site/content/docs/elements/vault/config.mdx +256 -0
  57. package/site/content/docs/elements/vault/index.mdx +356 -0
  58. package/site/content/docs/elements/vault/meta.json +5 -0
  59. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  60. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  61. package/site/content/docs/index.mdx +63 -36
  62. package/site/content/docs/meta.json +8 -7
  63. package/site/content/docs/plugins/cors.mdx +2 -1
  64. package/site/content/docs/plugins/csrf.mdx +4 -1
  65. package/site/content/docs/plugins/headers.mdx +2 -1
  66. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  67. package/site/content/docs/plugins/magic-link.mdx +6 -0
  68. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  69. package/site/content/docs/plugins/oauth.mdx +1 -1
  70. package/site/content/docs/plugins/otp.mdx +12 -1
  71. package/site/content/docs/plugins/passkey.mdx +39 -15
  72. package/site/content/docs/plugins/two-factor.mdx +91 -34
  73. package/site/content/docs/plugins/username.mdx +1 -1
  74. package/site/content/docs/recipes/caddy.mdx +3 -3
  75. package/site/content/docs/recipes/index.mdx +18 -28
  76. package/site/content/docs/recipes/mailpit.mdx +1 -1
  77. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  78. package/site/content/docs/recipes/meta.json +1 -4
  79. package/site/content/docs/recipes/nginx.mdx +2 -2
  80. package/site/content/docs/recipes/openrouter.mdx +283 -0
  81. package/site/content/docs/recipes/pgdog.mdx +3 -3
  82. package/site/content/docs/recipes/postgres.mdx +1 -2
  83. package/site/content/docs/recipes/rustfs.mdx +2 -2
  84. package/site/content/docs/recipes/traefik.mdx +3 -3
  85. package/site/content/docs/reference/cli.mdx +231 -0
  86. package/site/content/docs/reference/configuration.mdx +30 -10
  87. package/site/content/docs/reference/environment-variables.mdx +93 -20
  88. package/site/content/docs/reference/errors.mdx +132 -29
  89. package/site/content/docs/reference/fx.mdx +137 -73
  90. package/site/content/docs/reference/index.mdx +13 -21
  91. package/site/content/docs/reference/meta.json +4 -5
  92. package/site/content/docs/reference/okid.mdx +42 -17
  93. package/site/content/docs/reference/plugins.mdx +4 -3
  94. package/site/content/docs/reference/security.mdx +197 -0
  95. package/site/content/docs/understand/meta.json +5 -0
  96. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  97. package/site/content/docs/understand/the-model.mdx +32 -0
  98. package/site/content/docs/understand/the-problem.mdx +74 -0
  99. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  100. package/src/auth/api-keys.ts +2 -1
  101. package/src/auth/bindings.ts +64 -16
  102. package/src/auth/gate-auth.test.ts +6 -1
  103. package/src/auth/identity.ts +152 -5
  104. package/src/auth/index.ts +29 -0
  105. package/src/auth/invites.ts +2 -1
  106. package/src/auth/method-context.ts +41 -0
  107. package/src/auth/oauth-as/crypto.ts +2 -1
  108. package/src/auth/oauth-as/stores.ts +2 -1
  109. package/src/auth/operator.ts +2 -1
  110. package/src/auth/sessions-jwt.test.ts +77 -0
  111. package/src/auth/sessions.ts +20 -4
  112. package/src/auth/tenants.ts +3 -2
  113. package/src/auth/two-factor-challenge.test.ts +61 -0
  114. package/src/auth/two-factor-challenge.ts +284 -0
  115. package/src/auth/verification.ts +5 -0
  116. package/src/bench/REPORT.md +49 -0
  117. package/src/bench/g03-signal-once.bench.ts +1 -1
  118. package/src/bench/g10-observability-contention.bench.ts +1 -1
  119. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  120. package/src/bench/load-app.ts +2 -10
  121. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  122. package/src/cli/ai-setup/apply.ts +294 -52
  123. package/src/cli/ai-setup/catalog.ts +238 -1342
  124. package/src/cli/ai-setup/index.ts +14 -99
  125. package/src/cli/ai-setup/prompts.ts +60 -443
  126. package/src/cli/ask-seed.test.ts +96 -0
  127. package/src/cli/ask-seed.ts +82 -0
  128. package/src/cli/ask-vault-gaps.test.ts +84 -0
  129. package/src/cli/ask-vault-gaps.ts +146 -0
  130. package/src/cli/build.ts +2 -2
  131. package/src/cli/client-add.test.ts +26 -1
  132. package/src/cli/client-add.ts +106 -27
  133. package/src/cli/db-seed.ts +2 -0
  134. package/src/cli/db.ts +150 -11
  135. package/src/cli/dev.ts +119 -211
  136. package/src/cli/docker-clean.ts +2 -2
  137. package/src/cli/docker-cli.test.ts +1 -1
  138. package/src/cli/doctor-diff.ts +4 -2
  139. package/src/cli/doctor-pii.test.ts +1 -1
  140. package/src/cli/doctor.ts +14 -1
  141. package/src/cli/hero-meta.test.ts +4 -2
  142. package/src/cli/load-config.images.test.ts +8 -10
  143. package/src/cli/load-config.ts +1 -1
  144. package/src/cli/registry.ts +22 -10
  145. package/src/cli/replay.ts +3 -1
  146. package/src/cli/start.ts +1 -1
  147. package/src/cli/tui/keys.ts +2 -3
  148. package/src/client/auth/cookies.ts +74 -0
  149. package/src/client/auth/create-auth-client.ts +636 -0
  150. package/src/client/auth/denials.ts +99 -0
  151. package/src/client/auth/session.ts +283 -0
  152. package/src/client/auth.test.ts +251 -0
  153. package/src/client/auth.ts +39 -114
  154. package/src/client/create-with-session.ts +245 -0
  155. package/src/client/create.ts +51 -5
  156. package/src/client/index.ts +11 -1
  157. package/src/client/live.ts +11 -79
  158. package/src/client/notes-contract.test.ts +5 -10
  159. package/src/client/sse.ts +137 -0
  160. package/src/client/stream.ts +147 -0
  161. package/src/client/transport.test.ts +32 -0
  162. package/src/client/transport.ts +123 -26
  163. package/src/client/types.ts +118 -14
  164. package/src/client-react/index.ts +185 -24
  165. package/src/client-react/use-live-query.ts +13 -3
  166. package/src/compiler/aot.test.ts +7 -4
  167. package/src/compiler/effects-embed.test.ts +56 -0
  168. package/src/compiler/effects-fetch.test.ts +43 -0
  169. package/src/compiler/effects-infer.ts +58 -2
  170. package/src/compiler/extract.test.ts +257 -43
  171. package/src/compiler/extract.ts +566 -48
  172. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  173. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  174. package/src/compiler/fixtures/skyport.expected.json +4 -4
  175. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  176. package/src/compiler/response.ts +2 -2
  177. package/src/compiler/search-writer-isolation.test.ts +40 -0
  178. package/src/config/index.ts +11 -0
  179. package/src/console/server/app.ts +17 -23
  180. package/src/console/server/bind.ts +4 -0
  181. package/src/console/server/console.test.ts +2 -2
  182. package/src/console/server/flows-invoke.test.ts +50 -34
  183. package/src/console/server/flows.ts +436 -255
  184. package/src/console/server/runs-ingest.test.ts +6 -3
  185. package/src/console/server/serve.ts +1 -1
  186. package/src/console/server/signals.test.ts +1 -5
  187. package/src/console/server/signals.ts +8 -6
  188. package/src/console/server/state.ts +6 -1
  189. package/src/console/server/store.ts +1 -1
  190. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  191. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  192. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  193. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  194. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  195. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  196. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  197. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  198. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  199. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  200. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  201. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  202. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  203. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  204. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  205. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  206. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  207. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  208. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  209. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  210. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  211. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  212. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  213. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  214. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  215. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  216. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  217. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  218. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  219. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  220. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  221. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  222. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  223. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  224. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  225. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  226. package/src/console/ui-next/dist/index.html +6 -13
  227. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  228. package/src/console/ui-next/src/client.ts +7 -2
  229. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  230. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  231. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  232. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  235. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  236. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  237. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  238. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  239. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  240. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  241. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  242. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  243. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  244. package/src/docker/ai-model-status.test.ts +1 -12
  245. package/src/docker/ai-model-status.ts +10 -38
  246. package/src/docker/compose-up.test.ts +100 -0
  247. package/src/docker/compose-up.ts +292 -0
  248. package/src/docker/compose.ts +4 -14
  249. package/src/docker/derive.ts +9 -34
  250. package/src/docker/docker.test.ts +16 -261
  251. package/src/docker/images-config.test.ts +13 -25
  252. package/src/docker/index.ts +1 -24
  253. package/src/docker/recipes/index.ts +0 -23
  254. package/src/docker/recipes/pgdog.ts +6 -4
  255. package/src/docker/stack-id.test.ts +2 -2
  256. package/src/docker/stack-id.ts +0 -1
  257. package/src/docker/types.ts +2 -2
  258. package/src/drivers/ai-anthropic.ts +10 -0
  259. package/src/drivers/ai-openai-compatible.ts +34 -7
  260. package/src/drivers/ai-providers.test.ts +0 -107
  261. package/src/drivers/ai-stream.test.ts +0 -34
  262. package/src/drivers/ai-types.ts +17 -8
  263. package/src/drivers/channel-fcm.ts +6 -1
  264. package/src/drivers/channel-msegat.ts +6 -1
  265. package/src/drivers/channel-resend.ts +5 -1
  266. package/src/drivers/channel-smtp.ts +5 -1
  267. package/src/drivers/channel-sndr.ts +10 -1
  268. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  269. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  270. package/src/drivers/channel-taqnyat.ts +6 -1
  271. package/src/drivers/channel-types.ts +10 -0
  272. package/src/drivers/channel-unifonic.ts +6 -1
  273. package/src/drivers/channel-wa-cloud.ts +6 -1
  274. package/src/drivers/channel-webpush.ts +6 -1
  275. package/src/drivers/external.ts +33 -0
  276. package/src/drivers/index.ts +0 -10
  277. package/src/drivers/meilisearch.ts +6 -0
  278. package/src/drivers/oauth-types.ts +4 -0
  279. package/src/drivers/postgres.test.ts +74 -3
  280. package/src/drivers/postgres.ts +175 -6
  281. package/src/drivers/signal-types.ts +5 -5
  282. package/src/drivers/types.ts +7 -0
  283. package/src/drivers/vault-types.ts +5 -0
  284. package/src/elements/ai/declare.ts +17 -4
  285. package/src/elements/ai/eval.ts +1 -1
  286. package/src/elements/ai/mcp-http.ts +1 -1
  287. package/src/elements/ai/pii.ts +1 -1
  288. package/src/elements/ai/providers.test.ts +289 -0
  289. package/src/elements/ai/providers.ts +169 -0
  290. package/src/elements/ai/runtime.ts +68 -5
  291. package/src/elements/ai/schema.ts +2 -2
  292. package/src/elements/ai.test.ts +7 -7
  293. package/src/elements/ai.ts +17 -1
  294. package/src/elements/channel/runtime.ts +105 -12
  295. package/src/elements/clock/cron-fields.test.ts +144 -0
  296. package/src/elements/clock/cron-fields.ts +185 -0
  297. package/src/elements/clock/declare.ts +247 -3
  298. package/src/elements/clock.ts +21 -1
  299. package/src/elements/index.ts +15 -0
  300. package/src/elements/signal/chaos-child.ts +1 -2
  301. package/src/elements/signal/declare.ts +65 -37
  302. package/src/elements/signal/delivery-modes.test.ts +11 -31
  303. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  304. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  305. package/src/elements/signal/key-ordering.test.ts +6 -26
  306. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  307. package/src/elements/signal/optional-emit.test.ts +6 -9
  308. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  309. package/src/elements/signal/orphan-messages.test.ts +4 -15
  310. package/src/elements/signal/reconcile.test.ts +2 -2
  311. package/src/elements/signal/runtime.ts +1 -1
  312. package/src/elements/signal/schema-emit.test.ts +4 -8
  313. package/src/elements/signal.test.ts +24 -49
  314. package/src/elements/signal.ts +9 -2
  315. package/src/elements/store/domain-ddl.test.ts +1 -1
  316. package/src/elements/store/live-http.test.ts +9 -4
  317. package/src/elements/store/prepare-row.test.ts +123 -0
  318. package/src/elements/store/resource.ts +43 -12
  319. package/src/elements/store/schema-decl.ts +117 -0
  320. package/src/elements/store/search-backfill.ts +205 -0
  321. package/src/elements/store/search-bind.ts +66 -0
  322. package/src/elements/store/search-bm25.ts +78 -0
  323. package/src/elements/store/search-ddl.ts +156 -0
  324. package/src/elements/store/search-embed-flow.ts +141 -0
  325. package/src/elements/store/search-errors.ts +34 -0
  326. package/src/elements/store/search-fusion.ts +112 -0
  327. package/src/elements/store/search-lsh.ts +179 -0
  328. package/src/elements/store/search-runtime.ts +300 -0
  329. package/src/elements/store/search.test.ts +144 -0
  330. package/src/elements/store/sql-session.test.ts +1 -0
  331. package/src/elements/store/sql-session.ts +83 -3
  332. package/src/elements/store/table.ts +23 -2
  333. package/src/elements/store/upsert-app.test.ts +1 -3
  334. package/src/elements/store.ts +50 -0
  335. package/src/full.ts +4 -2
  336. package/src/http.ts +4 -1
  337. package/src/i18n/catalogs/ar.ts +14 -6
  338. package/src/i18n/catalogs/en.ts +14 -6
  339. package/src/index.ts +6 -1
  340. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  341. package/src/kernel/adopt-routes.ts +9 -1
  342. package/src/kernel/app-auth.ts +5 -0
  343. package/src/kernel/app.ts +102 -9
  344. package/src/kernel/auto-cache.test.ts +6 -12
  345. package/src/kernel/auto-registry.test.ts +3 -3
  346. package/src/kernel/boot-bind/ai.test.ts +22 -19
  347. package/src/kernel/boot-bind/ai.ts +5 -43
  348. package/src/kernel/boot-bind/clock.ts +23 -5
  349. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  350. package/src/kernel/boot.test.ts +11 -6
  351. package/src/kernel/boot.ts +66 -16
  352. package/src/kernel/boundary-contract.ts +93 -0
  353. package/src/kernel/budget.test.ts +1 -1
  354. package/src/kernel/call.ts +89 -0
  355. package/src/kernel/capability.ts +6 -0
  356. package/src/kernel/client-descriptor.ts +123 -0
  357. package/src/kernel/clock-timezone.test.ts +80 -0
  358. package/src/kernel/correlation.test.ts +1 -1
  359. package/src/kernel/dry-run.ts +11 -8
  360. package/src/kernel/effects-stamping.test.ts +42 -9
  361. package/src/kernel/effects.test.ts +4 -2
  362. package/src/kernel/effects.ts +51 -9
  363. package/src/kernel/errors-channel.ts +17 -0
  364. package/src/kernel/errors-live-resume.ts +3 -2
  365. package/src/kernel/errors-tenant.ts +7 -4
  366. package/src/kernel/errors.registry-helpers.ts +96 -0
  367. package/src/kernel/errors.registry.test.ts +139 -25
  368. package/src/kernel/errors.ts +102 -23
  369. package/src/kernel/external-effects.test.ts +196 -0
  370. package/src/kernel/flow.test.ts +13 -4
  371. package/src/kernel/flow.ts +69 -71
  372. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  373. package/src/kernel/fx-dead-letters.test.ts +3 -3
  374. package/src/kernel/fx-fetch.ts +55 -0
  375. package/src/kernel/fx-live-stream.ts +2 -2
  376. package/src/kernel/fx-live.test.ts +8 -8
  377. package/src/kernel/fx.test.ts +17 -11
  378. package/src/kernel/fx.ts +180 -57
  379. package/src/kernel/horizontal-child.ts +1 -1
  380. package/src/kernel/http-query.test.ts +3 -6
  381. package/src/kernel/index.ts +13 -3
  382. package/src/kernel/instance-id.ts +1 -1
  383. package/src/kernel/instances.test.ts +40 -0
  384. package/src/kernel/instances.ts +47 -9
  385. package/src/kernel/live-http.test.ts +6 -7
  386. package/src/kernel/live-http.ts +6 -2
  387. package/src/kernel/live-resume.test.ts +1 -1
  388. package/src/kernel/mcp-tool.test.ts +3 -3
  389. package/src/kernel/on.ts +98 -4
  390. package/src/kernel/pipeline.test.ts +12 -15
  391. package/src/kernel/plugin-elements.test.ts +1 -1
  392. package/src/kernel/stamp-http.test.ts +3 -3
  393. package/src/kernel/triggers.ts +210 -95
  394. package/src/kernel-entry.ts +0 -1
  395. package/src/manifest/diff.ts +11 -1
  396. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  397. package/src/manifest/types.ts +46 -2
  398. package/src/mcp/docs-index.ts +3 -3
  399. package/src/mcp/docs-mcp.test.ts +2 -2
  400. package/src/mcp/docs-tools.ts +1 -1
  401. package/src/okid.test.ts +56 -0
  402. package/src/okid.ts +55 -11
  403. package/src/plugins/anonymous.ts +8 -4
  404. package/src/plugins/auth/shared.ts +27 -3
  405. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  406. package/src/plugins/auth-methods.security.test.ts +561 -23
  407. package/src/plugins/config-source.ts +1 -1
  408. package/src/plugins/csrf.test.ts +65 -0
  409. package/src/plugins/index.ts +2 -0
  410. package/src/plugins/magic-link.ts +36 -16
  411. package/src/plugins/oauth/shared.ts +3 -1
  412. package/src/plugins/oauth.ts +22 -14
  413. package/src/plugins/otp.ts +37 -21
  414. package/src/plugins/passkey-webauthn.ts +9 -6
  415. package/src/plugins/passkey.ts +98 -26
  416. package/src/plugins/pre-account-hijack.test.ts +223 -0
  417. package/src/plugins/two-factor.ts +466 -37
  418. package/src/plugins/username.ts +19 -7
  419. package/src/release/build-lib.ts +1 -0
  420. package/src/release/limits.ts +2 -2
  421. package/src/release/measure.ts +1 -1
  422. package/src/runs/collect.test.ts +0 -2
  423. package/src/runtime/json-code-block.test.ts +250 -20
  424. package/src/runtime/json-code-block.ts +1261 -41
  425. package/src/term.test.ts +1 -1
  426. package/src/test/create-test-app.test.ts +12 -12
  427. package/src/test/live-signals.test.ts +6 -6
  428. package/src/test/provisions.integration.test.ts +10 -14
  429. package/src/test/reset-element-registries.ts +1 -1
  430. package/src/test/tenant-isolation.test.ts +12 -6
  431. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  432. package/site/content/docs/deployment/docker.mdx +0 -227
  433. package/site/content/docs/deployment/index.mdx +0 -83
  434. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  435. package/site/content/docs/deployment/meta.json +0 -5
  436. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  437. package/site/content/docs/elements/ai.mdx +0 -385
  438. package/site/content/docs/elements/channel.mdx +0 -346
  439. package/site/content/docs/elements/clock.mdx +0 -244
  440. package/site/content/docs/elements/flow.mdx +0 -420
  441. package/site/content/docs/elements/gate.mdx +0 -436
  442. package/site/content/docs/elements/signal.mdx +0 -380
  443. package/site/content/docs/elements/store.mdx +0 -1099
  444. package/site/content/docs/elements/vault.mdx +0 -405
  445. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  446. package/site/content/docs/get-started/index.mdx +0 -43
  447. package/site/content/docs/get-started/installation.mdx +0 -220
  448. package/site/content/docs/get-started/introduction.mdx +0 -144
  449. package/site/content/docs/get-started/meta.json +0 -13
  450. package/site/content/docs/get-started/project-structure.mdx +0 -925
  451. package/site/content/docs/get-started/testing.mdx +0 -328
  452. package/site/content/docs/get-started/why.mdx +0 -114
  453. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  454. package/site/content/docs/recipes/ollama.mdx +0 -142
  455. package/site/content/docs/recipes/sglang.mdx +0 -105
  456. package/site/content/docs/recipes/vllm.mdx +0 -106
  457. package/site/content/docs/reference/cli.md +0 -232
  458. package/site/content/docs/reference/client.mdx +0 -469
  459. package/site/content/docs/reference/security.md +0 -74
  460. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  461. package/src/cli/ai-setup/recommend.test.ts +0 -225
  462. package/src/cli/ai-setup/recommend.ts +0 -225
  463. package/src/cli/dev-controls.test.ts +0 -122
  464. package/src/cli/dev-controls.ts +0 -164
  465. package/src/cli/tui/DevLive.tsx +0 -126
  466. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  467. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  468. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  469. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  470. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  471. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  472. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  473. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  474. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  475. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  476. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  477. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  478. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  479. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  480. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  481. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  482. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  483. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  484. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  485. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  486. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  487. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  488. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  489. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  490. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  491. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  492. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  493. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  494. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  495. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  496. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  497. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  498. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  499. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  500. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  501. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  502. package/src/docker/ollama-pull.ts +0 -232
  503. package/src/docker/recipes/llama-cpp.ts +0 -298
  504. package/src/docker/recipes/ollama.ts +0 -44
  505. package/src/docker/recipes/sglang.ts +0 -55
  506. package/src/docker/recipes/vllm.ts +0 -44
  507. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  508. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  509. package/src/drivers/ai-ollama.ts +0 -389
  510. package/src/drivers/ollama.ts +0 -14
@@ -8,6 +8,7 @@
8
8
  import { beforeEach, describe, expect, test } from "bun:test";
9
9
  import { z } from "zod";
10
10
  import { oke } from "../kernel/app.ts";
11
+ import { call } from "../kernel/call.ts";
11
12
  import { flow, resetFlowSeq } from "../kernel/flow.ts";
12
13
  import { on, resetBindings } from "../kernel/on.ts";
13
14
  import { http } from "../kernel/triggers.ts";
@@ -38,20 +39,15 @@ const NewNote = z.object({
38
39
  const NotFound = z.object({});
39
40
 
40
41
  const create = on(
41
- http.post("/notes"),
42
+ http.post("/notes", { in: NewNote, out: NoteId }),
42
43
  flow("notes.create", {
43
- in: NewNote,
44
- out: NoteId,
45
44
  do: (input) => ({ id: `n_${input.title}` }),
46
45
  }),
47
46
  );
48
47
 
49
48
  const get = on(
50
- http.get("/notes/:id"),
49
+ http.get("/notes/:id", { in: NoteId, out: Note, errors: { NotFound } }),
51
50
  flow("notes.get", {
52
- in: NoteId,
53
- out: Note,
54
- errors: { NotFound },
55
51
  do: ({ id }, fx) =>
56
52
  id === "missing" ? fx.fail("NotFound", {}) : { id, title: "First", body: "Hello" },
57
53
  }),
@@ -252,7 +248,7 @@ describe("Notes — typeof app carries contracts", () => {
252
248
  });
253
249
 
254
250
  test("untriggered adopted flow is RPC-only on the client", async () => {
255
- const stats = flow("notes.stats", {
251
+ const stats = call("notes.stats", {
256
252
  in: NoteId,
257
253
  out: z.object({ clicks: z.number() }),
258
254
  do: () => ({ clicks: 7 }),
@@ -280,8 +276,7 @@ describe("Notes — typeof app carries contracts", () => {
280
276
  });
281
277
 
282
278
  test("typeof app live exposure is subscribe, not JSON RPC", () => {
283
- const orderStatus = signal("order-status", {
284
- delivery: "live",
279
+ const orderStatus = signal.live("order-status", {
285
280
  optional: true,
286
281
  schema: z.object({ orderId: z.string(), status: z.string() }),
287
282
  });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared SSE frame pump for live subscribe and JSON streams.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ /** Callback for one parsed SSE data frame. */
8
+ export type SseFrameHandler = (event: unknown, id: string | undefined) => void;
9
+
10
+ /**
11
+ * Read an SSE response body until `[DONE]`, abort, or EOF.
12
+ *
13
+ * @param res - Fetch response (`text/event-stream`)
14
+ * @param onEvent - Frame handler
15
+ * @param signal - Abort signal
16
+ * @param onOpen - Called after content-type validation
17
+ */
18
+ export async function readSse(
19
+ res: Response,
20
+ onEvent: SseFrameHandler,
21
+ signal: AbortSignal,
22
+ onOpen?: () => void,
23
+ ): Promise<void> {
24
+ if (signal.aborted) return;
25
+ const ct = res.headers.get("content-type") ?? "";
26
+ if (!res.ok || !res.body) {
27
+ const text = await res.text().catch(() => "");
28
+ throw sseError(res.status, text);
29
+ }
30
+ if (!ct.includes("text/event-stream")) {
31
+ const text = await res.text().catch(() => "");
32
+ throw sseError(res.status, text || `Expected text/event-stream, got ${ct || "none"}`);
33
+ }
34
+ onOpen?.();
35
+ const reader = res.body.getReader();
36
+ const dec = new TextDecoder();
37
+ let buf = "";
38
+ try {
39
+ for (;;) {
40
+ if (signal.aborted) return;
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ buf += dec.decode(value, { stream: true });
44
+ let sep = buf.indexOf("\n\n");
45
+ while (sep >= 0) {
46
+ const raw = buf.slice(0, sep);
47
+ buf = buf.slice(sep + 2);
48
+ const stop = dispatchFrame(raw, onEvent, signal);
49
+ if (stop || signal.aborted) return;
50
+ sep = buf.indexOf("\n\n");
51
+ }
52
+ }
53
+ } finally {
54
+ reader.releaseLock();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Yield SSE JSON frames as an async iterable until `[DONE]`.
60
+ *
61
+ * @param res - Fetch response
62
+ * @param signal - Abort signal
63
+ */
64
+ export async function* iterateSse(res: Response, signal: AbortSignal): AsyncGenerator<unknown> {
65
+ const queue: unknown[] = [];
66
+ let wake: (() => void) | undefined;
67
+ let done = false;
68
+ let err: unknown;
69
+
70
+ const pump = readSse(
71
+ res,
72
+ (event) => {
73
+ queue.push(event);
74
+ wake?.();
75
+ },
76
+ signal,
77
+ )
78
+ .then(() => {
79
+ done = true;
80
+ wake?.();
81
+ })
82
+ .catch((e) => {
83
+ err = e;
84
+ done = true;
85
+ wake?.();
86
+ });
87
+
88
+ try {
89
+ for (;;) {
90
+ while (queue.length > 0) {
91
+ yield queue.shift();
92
+ }
93
+ if (done) break;
94
+ await new Promise<void>((r) => {
95
+ wake = r;
96
+ });
97
+ wake = undefined;
98
+ }
99
+ if (err) throw err;
100
+ } finally {
101
+ await pump.catch(() => {});
102
+ }
103
+ }
104
+
105
+ function dispatchFrame(raw: string, onEvent: SseFrameHandler, signal: AbortSignal): boolean {
106
+ const dataLines: string[] = [];
107
+ let id: string | undefined;
108
+ for (const line of raw.split("\n")) {
109
+ if (line.startsWith("id:")) id = line.slice(3).replace(/^ /, "");
110
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
111
+ }
112
+ if (dataLines.length === 0) return false;
113
+ const data = dataLines.join("\n");
114
+ if (data === "[DONE]") return true;
115
+ if (signal.aborted) return true;
116
+ onEvent(JSON.parse(data) as unknown, id);
117
+ return false;
118
+ }
119
+
120
+ /** Build an Error with optional HTTP status. */
121
+ export function sseError(status: number, body: string): Error {
122
+ let message = `HTTP ${status}`;
123
+ if (body) {
124
+ try {
125
+ const json: unknown = JSON.parse(body);
126
+ if (json !== null && typeof json === "object" && "error" in json) {
127
+ const err = (json as { error?: { code?: string; data?: { message?: string } } }).error;
128
+ message = err?.data?.message ?? err?.code ?? message;
129
+ }
130
+ } catch {
131
+ message = body.slice(0, 200);
132
+ }
133
+ }
134
+ const e = new Error(message);
135
+ (e as Error & { status?: number }).status = status;
136
+ return e;
137
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Finite SSE stream consumer for `fx.json.stream` Flows.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { iterateSse, sseError } from "./sse.ts";
8
+ import type { ClientFetch, ClientHeaders, ClientOptions, ClientRouteMap } from "./types.ts";
9
+
10
+ /** Flow id → REST route for stream-only (non-live) SSE. */
11
+ export type StreamByFlow = Readonly<
12
+ Record<string, { readonly method: string; readonly path: string }>
13
+ >;
14
+
15
+ /**
16
+ * Collect `stream: true` routes that are not live exposures.
17
+ *
18
+ * @param $routes - Runtime route map
19
+ */
20
+ export function flattenStreamRoutes($routes: ClientRouteMap | undefined): StreamByFlow {
21
+ const out: Record<string, { readonly method: string; readonly path: string }> = {};
22
+ if (!$routes) return out;
23
+ for (const [unit, flows] of Object.entries($routes)) {
24
+ if (!flows || typeof flows !== "object") continue;
25
+ for (const [flow, contract] of Object.entries(flows)) {
26
+ if (!contract || typeof contract !== "object") continue;
27
+ if (!("stream" in contract) || contract.stream !== true) continue;
28
+ if ("live" in contract && typeof contract.live === "string") continue;
29
+ const method = "method" in contract ? contract.method : undefined;
30
+ const path = "path" in contract ? contract.path : undefined;
31
+ if (typeof method === "string" && typeof path === "string") {
32
+ out[`${unit}.${flow}`] = { method, path };
33
+ }
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * Open a one-shot SSE stream (no auto-resubscribe).
41
+ *
42
+ * @param base - Origin
43
+ * @param route - REST method/path
44
+ * @param input - JSON body / path params
45
+ * @param opts - Client options
46
+ */
47
+ export async function* openStream(
48
+ base: string,
49
+ route: { readonly method: string; readonly path: string },
50
+ input: unknown,
51
+ opts: ClientOptions,
52
+ ): AsyncGenerator<unknown> {
53
+ const fetchFn: ClientFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
54
+ let refreshed = false;
55
+ for (;;) {
56
+ const ctrl = new AbortController();
57
+ const parent = opts.signal;
58
+ if (parent) {
59
+ if (parent.aborted) {
60
+ ctrl.abort();
61
+ } else {
62
+ parent.addEventListener("abort", () => ctrl.abort(), { once: true });
63
+ }
64
+ }
65
+ const { url, method, body } = streamRequest(base, route.method, route.path, input);
66
+ const headers = new Headers({ accept: "text/event-stream" });
67
+ const extra = typeof opts.headers === "function" ? await opts.headers() : opts.headers;
68
+ applyHeaders(headers, extra);
69
+ if (body !== undefined && !headers.has("content-type")) {
70
+ headers.set("content-type", "application/json");
71
+ }
72
+ const token =
73
+ opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
74
+ ? await opts.auth.getToken()
75
+ : undefined;
76
+ if (token && !headers.has("authorization")) {
77
+ headers.set("authorization", `Bearer ${token}`);
78
+ }
79
+ const res = await fetchFn(url, {
80
+ method,
81
+ headers,
82
+ body,
83
+ signal: ctrl.signal,
84
+ ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
85
+ });
86
+ if (
87
+ res.status === 401 &&
88
+ opts.auth &&
89
+ "refresh" in opts.auth &&
90
+ typeof opts.auth.refresh === "function" &&
91
+ !refreshed
92
+ ) {
93
+ refreshed = true;
94
+ await opts.auth.refresh();
95
+ continue;
96
+ }
97
+ if (!res.ok) {
98
+ const text = await res.text().catch(() => "");
99
+ throw sseError(res.status, text);
100
+ }
101
+ yield* iterateSse(res, ctrl.signal);
102
+ return;
103
+ }
104
+ }
105
+
106
+ function streamRequest(
107
+ base: string,
108
+ method: string,
109
+ path: string,
110
+ input: unknown,
111
+ ): { url: string; method: string; body: string | undefined } {
112
+ const params =
113
+ input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
114
+ let pathOut = path;
115
+ const rest: Record<string, unknown> = {};
116
+ for (const [k, v] of Object.entries(params)) {
117
+ const token = `:${k}`;
118
+ if (pathOut.includes(token)) {
119
+ pathOut = pathOut.replaceAll(token, encodeURIComponent(String(v)));
120
+ } else {
121
+ rest[k] = v;
122
+ }
123
+ }
124
+ const upper = method.toUpperCase();
125
+ let body: string | undefined;
126
+ if (upper === "GET" || upper === "HEAD") {
127
+ const query: string[] = [];
128
+ for (const [k, v] of Object.entries(rest)) {
129
+ if (v !== undefined) {
130
+ query.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
131
+ }
132
+ }
133
+ const qs = query.length ? `?${query.join("&")}` : "";
134
+ return { url: `${base}${pathOut}${qs}`, method: upper, body: undefined };
135
+ }
136
+ body = JSON.stringify(Object.keys(rest).length > 0 ? rest : (input ?? {}));
137
+ return { url: `${base}${pathOut}`, method: upper, body };
138
+ }
139
+
140
+ function applyHeaders(headers: Headers, extra: ClientHeaders | undefined): void {
141
+ if (!extra) return;
142
+ if (Array.isArray(extra)) {
143
+ for (const [k, v] of extra) headers.set(k, v);
144
+ } else {
145
+ for (const [k, v] of Object.entries(extra)) headers.set(k, v);
146
+ }
147
+ }
@@ -142,3 +142,35 @@ describe("transport — auth refresh", () => {
142
142
  }
143
143
  });
144
144
  });
145
+
146
+ describe("transport — binary response", () => {
147
+ test("response blob returns Blob data", async () => {
148
+ const bytes = new Uint8Array([37, 80, 68, 70]); // %PDF
149
+ const api = createClient<PingApp>("http://app.test", {
150
+ fetch: async () =>
151
+ new Response(bytes, {
152
+ status: 200,
153
+ headers: { "content-type": "application/pdf" },
154
+ }),
155
+ });
156
+ const { data, error } = await api.sys.ping({ response: "blob" });
157
+ expect(error).toBeNull();
158
+ expect(data).toBeInstanceOf(Blob);
159
+ expect((data as unknown as Blob).type).toContain("pdf");
160
+ });
161
+
162
+ test("response arrayBuffer returns bytes", async () => {
163
+ const bytes = new Uint8Array([1, 2, 3, 4]);
164
+ const api = createClient<PingApp>("http://app.test", {
165
+ fetch: async () =>
166
+ new Response(bytes, {
167
+ status: 200,
168
+ headers: { "content-type": "application/octet-stream" },
169
+ }),
170
+ });
171
+ const { data, error } = await api.sys.ping({ response: "arrayBuffer" });
172
+ expect(error).toBeNull();
173
+ expect(data).toBeInstanceOf(ArrayBuffer);
174
+ expect(new Uint8Array(data as unknown as ArrayBuffer)).toEqual(bytes);
175
+ });
176
+ });
@@ -5,7 +5,20 @@
5
5
  * Optional `routes` map switches to REST (`method` + path template).
6
6
  */
7
7
 
8
- import type { ClientEnvelope, ClientFetch, ClientHeaders, ClientOptions } from "./types.ts";
8
+ import type {
9
+ ClientBodyInit,
10
+ ClientEnvelope,
11
+ ClientFetch,
12
+ ClientHeaders,
13
+ ClientOptions,
14
+ } from "./types.ts";
15
+
16
+ /** Per-call transport options (binary decode, abort). */
17
+ export interface TransportCallOptions {
18
+ readonly headers?: ClientHeaders;
19
+ readonly response?: "json" | "blob" | "arrayBuffer";
20
+ readonly signal?: AbortSignal;
21
+ }
9
22
 
10
23
  /** Internal transport handle. */
11
24
  export interface Transport {
@@ -14,9 +27,13 @@ export interface Transport {
14
27
  *
15
28
  * @param key - `unit/flow`
16
29
  * @param input - JSON body / path-param source
17
- * @param headers - Per-call extra headers (merged after static ones)
30
+ * @param headersOrOpts - Per-call headers or full call options
18
31
  */
19
- call(key: string, input: unknown, headers?: ClientHeaders): Promise<ClientEnvelope>;
32
+ call(
33
+ key: string,
34
+ input: unknown,
35
+ headersOrOpts?: ClientHeaders | TransportCallOptions,
36
+ ): Promise<ClientEnvelope>;
20
37
  }
21
38
 
22
39
  /**
@@ -32,15 +49,24 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
32
49
  const backoff = opts.retry?.backoff ?? 2;
33
50
 
34
51
  return {
35
- async call(key, input, callHeaders) {
52
+ async call(key, input, headersOrOpts) {
53
+ const callOpts = normalizeCallOpts(headersOrOpts);
36
54
  let refreshed = false;
37
55
  let attempt = 0;
38
56
  let delay = delay0;
57
+ const callClientOpts: ClientOptions =
58
+ callOpts.signal !== undefined ? { ...opts, signal: callOpts.signal } : opts;
39
59
 
40
60
  for (;;) {
41
61
  try {
42
- const res = await once(base, key, input, opts, fetchFn, callHeaders);
43
- if (res.status === 401 && opts.auth?.refresh && !refreshed) {
62
+ const res = await once(base, key, input, callClientOpts, fetchFn, callOpts.headers);
63
+ if (
64
+ res.status === 401 &&
65
+ opts.auth &&
66
+ "refresh" in opts.auth &&
67
+ typeof opts.auth.refresh === "function" &&
68
+ !refreshed
69
+ ) {
44
70
  refreshed = true;
45
71
  await opts.auth.refresh();
46
72
  continue;
@@ -50,6 +76,9 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
50
76
  if (structured) return structured;
51
77
  throw new Error(`HTTP ${res.status}`);
52
78
  }
79
+ if (callOpts.response === "blob" || callOpts.response === "arrayBuffer") {
80
+ return decodeBinary(res, callOpts.response);
81
+ }
53
82
  return decode(res);
54
83
  } catch (err) {
55
84
  const transient = isTransient(err);
@@ -73,6 +102,23 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
73
102
  };
74
103
  }
75
104
 
105
+ function normalizeCallOpts(
106
+ headersOrOpts: ClientHeaders | TransportCallOptions | undefined,
107
+ ): TransportCallOptions {
108
+ if (headersOrOpts === undefined) return {};
109
+ if (Array.isArray(headersOrOpts)) return { headers: headersOrOpts };
110
+ if (
111
+ typeof headersOrOpts === "object" &&
112
+ ("response" in headersOrOpts || "signal" in headersOrOpts || "headers" in headersOrOpts)
113
+ ) {
114
+ const o = headersOrOpts as TransportCallOptions;
115
+ if (o.response !== undefined || o.signal !== undefined || o.headers !== undefined) {
116
+ return o;
117
+ }
118
+ }
119
+ return { headers: headersOrOpts as ClientHeaders };
120
+ }
121
+
76
122
  /**
77
123
  * Single HTTP attempt. Throws on network / abort / 5xx (retryable).
78
124
  *
@@ -108,29 +154,34 @@ async function once(
108
154
  } else if (callHeaders) {
109
155
  for (const [k, v] of Object.entries(callHeaders)) headers.set(k, v);
110
156
  }
111
- if (body !== undefined && !headers.has("content-type")) {
157
+ if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
112
158
  headers.set("content-type", "application/json");
113
159
  }
114
160
 
115
- const token = opts.auth ? await opts.auth.getToken() : undefined;
161
+ const token =
162
+ opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
163
+ ? await opts.auth.getToken()
164
+ : undefined;
116
165
  if (token && !headers.has("authorization")) {
117
166
  headers.set("authorization", `Bearer ${token}`);
118
167
  }
119
168
 
120
- const ctrl = opts.timeout !== undefined ? new AbortController() : undefined;
121
- const timer =
122
- ctrl && opts.timeout !== undefined ? setTimeout(() => ctrl.abort(), opts.timeout) : undefined;
123
-
124
- try {
125
- return await fetchFn(url, {
126
- method,
127
- headers,
128
- body,
129
- signal: ctrl?.signal,
130
- });
131
- } finally {
132
- if (timer !== undefined) clearTimeout(timer);
169
+ const signals: AbortSignal[] = [];
170
+ if (opts.signal) signals.push(opts.signal);
171
+ if (opts.timeout !== undefined) {
172
+ const t = AbortSignal.timeout(opts.timeout);
173
+ signals.push(t);
133
174
  }
175
+ const signal =
176
+ signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
177
+
178
+ return await fetchFn(url, {
179
+ method,
180
+ headers,
181
+ body: body as RequestInit["body"],
182
+ signal,
183
+ ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
184
+ });
134
185
  }
135
186
 
136
187
  /**
@@ -158,10 +209,11 @@ function rpcRequest(
158
209
  base: string,
159
210
  key: string,
160
211
  input: unknown,
161
- ): { url: string; method: string; body: string | undefined } {
212
+ ): { url: string; method: string; body: ClientBodyInit | undefined } {
162
213
  const url = `${base}/_oke/${key}`;
163
- const body = input === undefined ? undefined : JSON.stringify(input ?? {});
164
- return { url, method: "POST", body };
214
+ if (input === undefined) return { url, method: "POST", body: undefined };
215
+ if (isRawBody(input)) return { url, method: "POST", body: input };
216
+ return { url, method: "POST", body: JSON.stringify(input ?? {}) };
165
217
  }
166
218
 
167
219
  function restRequest(
@@ -169,7 +221,10 @@ function restRequest(
169
221
  method: string,
170
222
  path: string,
171
223
  input: unknown,
172
- ): { url: string; method: string; body: string | undefined } {
224
+ ): { url: string; method: string; body: ClientBodyInit | undefined } {
225
+ if (isRawBody(input)) {
226
+ return { url: `${base}${path}`, method: method.toUpperCase(), body: input };
227
+ }
173
228
  const params =
174
229
  input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
175
230
  let pathOut = path;
@@ -186,7 +241,7 @@ function restRequest(
186
241
  }
187
242
 
188
243
  const upper = method.toUpperCase();
189
- let body: string | undefined;
244
+ let body: ClientBodyInit | undefined;
190
245
  if (upper === "GET" || upper === "HEAD") {
191
246
  for (const [k, v] of Object.entries(rest)) {
192
247
  if (v !== undefined) {
@@ -204,6 +259,16 @@ function restRequest(
204
259
  return { url: `${base}${pathOut}${qs}`, method: upper, body };
205
260
  }
206
261
 
262
+ function isRawBody(input: unknown): input is ClientBodyInit {
263
+ if (input === null || input === undefined) return false;
264
+ if (typeof Blob !== "undefined" && input instanceof Blob) return true;
265
+ if (typeof FormData !== "undefined" && input instanceof FormData) return true;
266
+ if (typeof ArrayBuffer !== "undefined" && input instanceof ArrayBuffer) return true;
267
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(input)) return true;
268
+ if (typeof ReadableStream !== "undefined" && input instanceof ReadableStream) return true;
269
+ return false;
270
+ }
271
+
207
272
  async function decode(res: Response): Promise<ClientEnvelope> {
208
273
  if (res.status === 204) {
209
274
  return { data: undefined, error: null };
@@ -254,6 +319,38 @@ async function decode(res: Response): Promise<ClientEnvelope> {
254
319
  };
255
320
  }
256
321
 
322
+ async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promise<ClientEnvelope> {
323
+ if (!res.ok) {
324
+ const structured = await decodeIfEnvelopeClone(res);
325
+ if (structured) return structured;
326
+ return {
327
+ data: null,
328
+ error: {
329
+ code: "TransportError",
330
+ data: { message: `HTTP ${res.status}`, status: res.status },
331
+ },
332
+ };
333
+ }
334
+ const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
335
+ return { data, error: null };
336
+ }
337
+
338
+ /** Try JSON envelope from an error response without assuming the body is reusable. */
339
+ async function decodeIfEnvelopeClone(res: Response): Promise<ClientEnvelope | null> {
340
+ const text = await res.text();
341
+ if (!text) return null;
342
+ let json: unknown;
343
+ try {
344
+ json = JSON.parse(text);
345
+ } catch {
346
+ return null;
347
+ }
348
+ if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
349
+ return json as ClientEnvelope;
350
+ }
351
+ return null;
352
+ }
353
+
257
354
  function isTransient(err: unknown): boolean {
258
355
  if (!(err instanceof Error)) return false;
259
356
  if (err.name === "AbortError") return false;