okengine 0.18.4 → 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 (512) 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/index.mdx +25 -6
  5. package/site/content/docs/ai/llms-txt.mdx +1 -1
  6. package/site/content/docs/ai/meta.json +1 -1
  7. package/site/content/docs/ai/skills.mdx +4 -1
  8. package/site/content/docs/ai/try-it.mdx +58 -0
  9. package/site/content/docs/client/auth.mdx +208 -0
  10. package/site/content/docs/client/calling.mdx +451 -0
  11. package/site/content/docs/client/index.mdx +209 -0
  12. package/site/content/docs/client/live.mdx +234 -0
  13. package/site/content/docs/client/meta.json +5 -0
  14. package/site/content/docs/client/react.mdx +249 -0
  15. package/site/content/docs/elements/ai/agents.mdx +232 -0
  16. package/site/content/docs/elements/ai/index.mdx +341 -0
  17. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  18. package/site/content/docs/elements/ai/meta.json +5 -0
  19. package/site/content/docs/elements/ai/models.mdx +283 -0
  20. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  21. package/site/content/docs/elements/channel/email.mdx +266 -0
  22. package/site/content/docs/elements/channel/index.mdx +344 -0
  23. package/site/content/docs/elements/channel/meta.json +5 -0
  24. package/site/content/docs/elements/channel/push.mdx +218 -0
  25. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  26. package/site/content/docs/elements/channel/sms.mdx +264 -0
  27. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  28. package/site/content/docs/elements/clock/index.mdx +300 -0
  29. package/site/content/docs/elements/clock/meta.json +5 -0
  30. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  31. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  32. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  33. package/site/content/docs/elements/flow/http.mdx +1080 -0
  34. package/site/content/docs/elements/flow/index.mdx +618 -0
  35. package/site/content/docs/elements/flow/meta.json +5 -0
  36. package/site/content/docs/elements/flow/routing.mdx +559 -0
  37. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  38. package/site/content/docs/elements/gate/auth.mdx +371 -0
  39. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  40. package/site/content/docs/elements/gate/index.mdx +418 -0
  41. package/site/content/docs/elements/gate/meta.json +5 -0
  42. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  43. package/site/content/docs/elements/gate/rls.mdx +303 -0
  44. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  45. package/site/content/docs/elements/index.mdx +52 -11
  46. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  47. package/site/content/docs/elements/signal/index.mdx +352 -0
  48. package/site/content/docs/elements/signal/live.mdx +590 -0
  49. package/site/content/docs/elements/signal/meta.json +5 -0
  50. package/site/content/docs/elements/signal/once.mdx +596 -0
  51. package/site/content/docs/elements/store/files.mdx +659 -0
  52. package/site/content/docs/elements/store/index.mdx +310 -0
  53. package/site/content/docs/elements/store/kv.mdx +629 -0
  54. package/site/content/docs/elements/store/meta.json +5 -0
  55. package/site/content/docs/elements/store/search.mdx +946 -0
  56. package/site/content/docs/elements/store/sql.mdx +937 -0
  57. package/site/content/docs/elements/vault/config.mdx +256 -0
  58. package/site/content/docs/elements/vault/index.mdx +356 -0
  59. package/site/content/docs/elements/vault/meta.json +5 -0
  60. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  61. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  62. package/site/content/docs/index.mdx +78 -30
  63. package/site/content/docs/meta.json +11 -8
  64. package/site/content/docs/plugins/cors.mdx +2 -1
  65. package/site/content/docs/plugins/csrf.mdx +4 -1
  66. package/site/content/docs/plugins/headers.mdx +2 -1
  67. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  68. package/site/content/docs/plugins/magic-link.mdx +6 -0
  69. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  70. package/site/content/docs/plugins/oauth.mdx +1 -1
  71. package/site/content/docs/plugins/otp.mdx +12 -1
  72. package/site/content/docs/plugins/passkey.mdx +39 -15
  73. package/site/content/docs/plugins/two-factor.mdx +91 -34
  74. package/site/content/docs/plugins/username.mdx +1 -1
  75. package/site/content/docs/recipes/caddy.mdx +3 -3
  76. package/site/content/docs/recipes/index.mdx +18 -28
  77. package/site/content/docs/recipes/mailpit.mdx +1 -1
  78. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  79. package/site/content/docs/recipes/meta.json +1 -4
  80. package/site/content/docs/recipes/nginx.mdx +2 -2
  81. package/site/content/docs/recipes/openrouter.mdx +283 -0
  82. package/site/content/docs/recipes/pgdog.mdx +3 -3
  83. package/site/content/docs/recipes/postgres.mdx +1 -2
  84. package/site/content/docs/recipes/rustfs.mdx +2 -2
  85. package/site/content/docs/recipes/traefik.mdx +3 -3
  86. package/site/content/docs/reference/cli.mdx +231 -0
  87. package/site/content/docs/reference/configuration.mdx +30 -10
  88. package/site/content/docs/reference/environment-variables.mdx +93 -20
  89. package/site/content/docs/reference/errors.mdx +132 -29
  90. package/site/content/docs/reference/fx.mdx +137 -73
  91. package/site/content/docs/reference/index.mdx +13 -21
  92. package/site/content/docs/reference/meta.json +4 -5
  93. package/site/content/docs/reference/okid.mdx +42 -17
  94. package/site/content/docs/reference/plugins.mdx +4 -3
  95. package/site/content/docs/reference/security.mdx +197 -0
  96. package/site/content/docs/understand/meta.json +5 -0
  97. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  98. package/site/content/docs/understand/the-model.mdx +32 -0
  99. package/site/content/docs/understand/the-problem.mdx +74 -0
  100. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  101. package/src/auth/api-keys.ts +2 -1
  102. package/src/auth/bindings.ts +64 -16
  103. package/src/auth/gate-auth.test.ts +6 -1
  104. package/src/auth/identity.ts +152 -5
  105. package/src/auth/index.ts +29 -0
  106. package/src/auth/invites.ts +2 -1
  107. package/src/auth/method-context.ts +41 -0
  108. package/src/auth/oauth-as/crypto.ts +2 -1
  109. package/src/auth/oauth-as/stores.ts +2 -1
  110. package/src/auth/operator.ts +2 -1
  111. package/src/auth/sessions-jwt.test.ts +77 -0
  112. package/src/auth/sessions.ts +20 -4
  113. package/src/auth/tenants.ts +3 -2
  114. package/src/auth/two-factor-challenge.test.ts +61 -0
  115. package/src/auth/two-factor-challenge.ts +284 -0
  116. package/src/auth/verification.ts +5 -0
  117. package/src/bench/REPORT.md +49 -0
  118. package/src/bench/g03-signal-once.bench.ts +1 -1
  119. package/src/bench/g10-observability-contention.bench.ts +1 -1
  120. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  121. package/src/bench/load-app.ts +2 -10
  122. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  123. package/src/cli/ai-setup/apply.ts +294 -52
  124. package/src/cli/ai-setup/catalog.ts +238 -1342
  125. package/src/cli/ai-setup/index.ts +14 -99
  126. package/src/cli/ai-setup/prompts.ts +60 -443
  127. package/src/cli/ask-seed.test.ts +96 -0
  128. package/src/cli/ask-seed.ts +82 -0
  129. package/src/cli/ask-vault-gaps.test.ts +84 -0
  130. package/src/cli/ask-vault-gaps.ts +146 -0
  131. package/src/cli/build.ts +2 -2
  132. package/src/cli/client-add.test.ts +26 -1
  133. package/src/cli/client-add.ts +106 -27
  134. package/src/cli/competitor-mention-removal.test.ts +1 -1
  135. package/src/cli/db-seed.ts +2 -0
  136. package/src/cli/db.ts +150 -11
  137. package/src/cli/dev.ts +119 -211
  138. package/src/cli/docker-clean.ts +2 -2
  139. package/src/cli/docker-cli.test.ts +1 -1
  140. package/src/cli/doctor-diff.ts +4 -2
  141. package/src/cli/doctor-pii.test.ts +1 -1
  142. package/src/cli/doctor.ts +14 -1
  143. package/src/cli/hero-meta.test.ts +4 -2
  144. package/src/cli/load-config.images.test.ts +8 -10
  145. package/src/cli/load-config.ts +1 -1
  146. package/src/cli/registry.ts +22 -10
  147. package/src/cli/replay.ts +3 -1
  148. package/src/cli/start.ts +1 -1
  149. package/src/cli/tui/keys.ts +2 -3
  150. package/src/client/auth/cookies.ts +74 -0
  151. package/src/client/auth/create-auth-client.ts +636 -0
  152. package/src/client/auth/denials.ts +99 -0
  153. package/src/client/auth/session.ts +283 -0
  154. package/src/client/auth.test.ts +251 -0
  155. package/src/client/auth.ts +39 -114
  156. package/src/client/create-with-session.ts +245 -0
  157. package/src/client/create.ts +51 -5
  158. package/src/client/index.ts +11 -1
  159. package/src/client/live.ts +11 -79
  160. package/src/client/notes-contract.test.ts +5 -10
  161. package/src/client/sse.ts +137 -0
  162. package/src/client/stream.ts +147 -0
  163. package/src/client/transport.test.ts +32 -0
  164. package/src/client/transport.ts +123 -26
  165. package/src/client/types.ts +118 -14
  166. package/src/client-react/index.ts +185 -24
  167. package/src/client-react/use-live-query.ts +13 -3
  168. package/src/compiler/aot.test.ts +7 -4
  169. package/src/compiler/effects-embed.test.ts +56 -0
  170. package/src/compiler/effects-fetch.test.ts +43 -0
  171. package/src/compiler/effects-infer.ts +58 -2
  172. package/src/compiler/extract.test.ts +258 -44
  173. package/src/compiler/extract.ts +566 -48
  174. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  175. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  176. package/src/compiler/fixtures/skyport.expected.json +4 -4
  177. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  178. package/src/compiler/response.ts +2 -2
  179. package/src/compiler/search-writer-isolation.test.ts +40 -0
  180. package/src/config/index.ts +11 -0
  181. package/src/console/server/app.ts +17 -23
  182. package/src/console/server/bind.ts +4 -0
  183. package/src/console/server/console.test.ts +2 -2
  184. package/src/console/server/flows-invoke.test.ts +50 -34
  185. package/src/console/server/flows.ts +436 -255
  186. package/src/console/server/runs-ingest.test.ts +6 -3
  187. package/src/console/server/serve.ts +1 -1
  188. package/src/console/server/signals.test.ts +1 -5
  189. package/src/console/server/signals.ts +8 -6
  190. package/src/console/server/state.ts +6 -1
  191. package/src/console/server/store.ts +1 -1
  192. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  193. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  194. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  195. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  196. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  197. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  198. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  199. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  200. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  201. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  202. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  203. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  204. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  205. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  206. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  207. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  208. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  209. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  210. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  211. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  212. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  213. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  214. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  215. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  216. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  217. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  218. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  219. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  220. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  221. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  222. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  223. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  224. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  225. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  226. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  227. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  228. package/src/console/ui-next/dist/index.html +6 -13
  229. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  230. package/src/console/ui-next/src/client.ts +7 -2
  231. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  232. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  233. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  235. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  236. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  237. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  238. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  239. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  240. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  241. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  242. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  243. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  244. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  245. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  246. package/src/docker/ai-model-status.test.ts +1 -12
  247. package/src/docker/ai-model-status.ts +10 -38
  248. package/src/docker/compose-up.test.ts +100 -0
  249. package/src/docker/compose-up.ts +292 -0
  250. package/src/docker/compose.ts +4 -14
  251. package/src/docker/derive.ts +9 -34
  252. package/src/docker/docker.test.ts +16 -261
  253. package/src/docker/images-config.test.ts +13 -25
  254. package/src/docker/index.ts +1 -24
  255. package/src/docker/recipes/index.ts +0 -23
  256. package/src/docker/recipes/pgdog.ts +6 -4
  257. package/src/docker/stack-id.test.ts +2 -2
  258. package/src/docker/stack-id.ts +0 -1
  259. package/src/docker/types.ts +2 -2
  260. package/src/drivers/ai-anthropic.ts +10 -0
  261. package/src/drivers/ai-openai-compatible.ts +34 -7
  262. package/src/drivers/ai-providers.test.ts +0 -107
  263. package/src/drivers/ai-stream.test.ts +0 -34
  264. package/src/drivers/ai-types.ts +17 -8
  265. package/src/drivers/channel-fcm.ts +6 -1
  266. package/src/drivers/channel-msegat.ts +6 -1
  267. package/src/drivers/channel-resend.ts +5 -1
  268. package/src/drivers/channel-smtp.ts +5 -1
  269. package/src/drivers/channel-sndr.ts +10 -1
  270. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  271. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  272. package/src/drivers/channel-taqnyat.ts +6 -1
  273. package/src/drivers/channel-types.ts +10 -0
  274. package/src/drivers/channel-unifonic.ts +6 -1
  275. package/src/drivers/channel-wa-cloud.ts +6 -1
  276. package/src/drivers/channel-webpush.ts +6 -1
  277. package/src/drivers/external.ts +33 -0
  278. package/src/drivers/index.ts +0 -10
  279. package/src/drivers/meilisearch.ts +6 -0
  280. package/src/drivers/oauth-types.ts +4 -0
  281. package/src/drivers/postgres.test.ts +74 -3
  282. package/src/drivers/postgres.ts +175 -6
  283. package/src/drivers/signal-types.ts +5 -5
  284. package/src/drivers/types.ts +7 -0
  285. package/src/drivers/vault-types.ts +5 -0
  286. package/src/elements/ai/declare.ts +17 -4
  287. package/src/elements/ai/eval.ts +1 -1
  288. package/src/elements/ai/mcp-http.ts +1 -1
  289. package/src/elements/ai/pii.ts +1 -1
  290. package/src/elements/ai/providers.test.ts +289 -0
  291. package/src/elements/ai/providers.ts +169 -0
  292. package/src/elements/ai/runtime.ts +68 -5
  293. package/src/elements/ai/schema.ts +2 -2
  294. package/src/elements/ai.test.ts +7 -7
  295. package/src/elements/ai.ts +17 -1
  296. package/src/elements/channel/runtime.ts +105 -12
  297. package/src/elements/clock/cron-fields.test.ts +144 -0
  298. package/src/elements/clock/cron-fields.ts +185 -0
  299. package/src/elements/clock/declare.ts +247 -3
  300. package/src/elements/clock.ts +21 -1
  301. package/src/elements/index.ts +15 -0
  302. package/src/elements/signal/chaos-child.ts +1 -2
  303. package/src/elements/signal/declare.ts +65 -37
  304. package/src/elements/signal/delivery-modes.test.ts +11 -31
  305. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  306. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  307. package/src/elements/signal/key-ordering.test.ts +6 -26
  308. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  309. package/src/elements/signal/optional-emit.test.ts +6 -9
  310. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  311. package/src/elements/signal/orphan-messages.test.ts +4 -15
  312. package/src/elements/signal/reconcile.test.ts +2 -2
  313. package/src/elements/signal/runtime.ts +1 -1
  314. package/src/elements/signal/schema-emit.test.ts +4 -8
  315. package/src/elements/signal.test.ts +24 -49
  316. package/src/elements/signal.ts +9 -2
  317. package/src/elements/store/domain-ddl.test.ts +1 -1
  318. package/src/elements/store/live-http.test.ts +9 -4
  319. package/src/elements/store/prepare-row.test.ts +123 -0
  320. package/src/elements/store/resource.ts +43 -12
  321. package/src/elements/store/schema-decl.ts +117 -0
  322. package/src/elements/store/search-backfill.ts +205 -0
  323. package/src/elements/store/search-bind.ts +66 -0
  324. package/src/elements/store/search-bm25.ts +78 -0
  325. package/src/elements/store/search-ddl.ts +156 -0
  326. package/src/elements/store/search-embed-flow.ts +141 -0
  327. package/src/elements/store/search-errors.ts +34 -0
  328. package/src/elements/store/search-fusion.ts +112 -0
  329. package/src/elements/store/search-lsh.ts +179 -0
  330. package/src/elements/store/search-runtime.ts +300 -0
  331. package/src/elements/store/search.test.ts +144 -0
  332. package/src/elements/store/sql-session.test.ts +1 -0
  333. package/src/elements/store/sql-session.ts +83 -3
  334. package/src/elements/store/table.ts +23 -2
  335. package/src/elements/store/upsert-app.test.ts +1 -3
  336. package/src/elements/store.ts +50 -0
  337. package/src/full.ts +4 -2
  338. package/src/http.ts +4 -1
  339. package/src/i18n/catalogs/ar.ts +14 -6
  340. package/src/i18n/catalogs/en.ts +14 -6
  341. package/src/index.ts +6 -1
  342. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  343. package/src/kernel/adopt-routes.ts +9 -1
  344. package/src/kernel/app-auth.ts +5 -0
  345. package/src/kernel/app.ts +102 -9
  346. package/src/kernel/auto-cache.test.ts +6 -12
  347. package/src/kernel/auto-registry.test.ts +3 -3
  348. package/src/kernel/boot-bind/ai.test.ts +22 -19
  349. package/src/kernel/boot-bind/ai.ts +5 -43
  350. package/src/kernel/boot-bind/clock.ts +23 -5
  351. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  352. package/src/kernel/boot.test.ts +11 -6
  353. package/src/kernel/boot.ts +66 -16
  354. package/src/kernel/boundary-contract.ts +93 -0
  355. package/src/kernel/budget.test.ts +1 -1
  356. package/src/kernel/call.ts +89 -0
  357. package/src/kernel/capability.ts +6 -0
  358. package/src/kernel/client-descriptor.ts +123 -0
  359. package/src/kernel/clock-timezone.test.ts +80 -0
  360. package/src/kernel/correlation.test.ts +1 -1
  361. package/src/kernel/dry-run.ts +11 -8
  362. package/src/kernel/effects-stamping.test.ts +42 -9
  363. package/src/kernel/effects.test.ts +4 -2
  364. package/src/kernel/effects.ts +51 -9
  365. package/src/kernel/errors-channel.ts +17 -0
  366. package/src/kernel/errors-live-resume.ts +3 -2
  367. package/src/kernel/errors-tenant.ts +7 -4
  368. package/src/kernel/errors.registry-helpers.ts +96 -0
  369. package/src/kernel/errors.registry.test.ts +139 -25
  370. package/src/kernel/errors.ts +102 -23
  371. package/src/kernel/external-effects.test.ts +196 -0
  372. package/src/kernel/flow.test.ts +13 -4
  373. package/src/kernel/flow.ts +69 -71
  374. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  375. package/src/kernel/fx-dead-letters.test.ts +3 -3
  376. package/src/kernel/fx-fetch.ts +55 -0
  377. package/src/kernel/fx-live-stream.ts +2 -2
  378. package/src/kernel/fx-live.test.ts +8 -8
  379. package/src/kernel/fx.test.ts +17 -11
  380. package/src/kernel/fx.ts +180 -57
  381. package/src/kernel/horizontal-child.ts +1 -1
  382. package/src/kernel/http-query.test.ts +3 -6
  383. package/src/kernel/index.ts +13 -3
  384. package/src/kernel/instance-id.ts +1 -1
  385. package/src/kernel/instances.test.ts +40 -0
  386. package/src/kernel/instances.ts +47 -9
  387. package/src/kernel/live-http.test.ts +6 -7
  388. package/src/kernel/live-http.ts +6 -2
  389. package/src/kernel/live-resume.test.ts +1 -1
  390. package/src/kernel/mcp-tool.test.ts +3 -3
  391. package/src/kernel/on.ts +98 -4
  392. package/src/kernel/pipeline.test.ts +12 -15
  393. package/src/kernel/plugin-elements.test.ts +1 -1
  394. package/src/kernel/stamp-http.test.ts +3 -3
  395. package/src/kernel/triggers.ts +210 -95
  396. package/src/kernel-entry.ts +0 -1
  397. package/src/manifest/diff.ts +11 -1
  398. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  399. package/src/manifest/types.ts +46 -2
  400. package/src/mcp/docs-index.ts +3 -3
  401. package/src/mcp/docs-mcp.test.ts +2 -2
  402. package/src/mcp/docs-tools.ts +1 -1
  403. package/src/okid.test.ts +56 -0
  404. package/src/okid.ts +55 -11
  405. package/src/plugins/anonymous.ts +8 -4
  406. package/src/plugins/auth/shared.ts +27 -3
  407. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  408. package/src/plugins/auth-methods.security.test.ts +561 -23
  409. package/src/plugins/config-source.ts +1 -1
  410. package/src/plugins/csrf.test.ts +65 -0
  411. package/src/plugins/index.ts +2 -0
  412. package/src/plugins/magic-link.ts +36 -16
  413. package/src/plugins/oauth/shared.ts +3 -1
  414. package/src/plugins/oauth.ts +22 -14
  415. package/src/plugins/otp.ts +37 -21
  416. package/src/plugins/passkey-webauthn.ts +9 -6
  417. package/src/plugins/passkey.ts +98 -26
  418. package/src/plugins/pre-account-hijack.test.ts +223 -0
  419. package/src/plugins/two-factor.ts +466 -37
  420. package/src/plugins/username.ts +19 -7
  421. package/src/release/build-lib.ts +1 -0
  422. package/src/release/limits.ts +2 -2
  423. package/src/release/measure.ts +1 -1
  424. package/src/runs/collect.test.ts +0 -2
  425. package/src/runtime/json-code-block.test.ts +250 -20
  426. package/src/runtime/json-code-block.ts +1261 -41
  427. package/src/term.test.ts +1 -1
  428. package/src/test/create-test-app.test.ts +12 -12
  429. package/src/test/live-signals.test.ts +6 -6
  430. package/src/test/provisions.integration.test.ts +10 -14
  431. package/src/test/reset-element-registries.ts +1 -1
  432. package/src/test/tenant-isolation.test.ts +12 -6
  433. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  434. package/site/content/docs/deployment/docker.mdx +0 -227
  435. package/site/content/docs/deployment/index.mdx +0 -83
  436. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  437. package/site/content/docs/deployment/meta.json +0 -5
  438. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  439. package/site/content/docs/elements/ai.mdx +0 -385
  440. package/site/content/docs/elements/channel.mdx +0 -346
  441. package/site/content/docs/elements/clock.mdx +0 -244
  442. package/site/content/docs/elements/flow.mdx +0 -420
  443. package/site/content/docs/elements/gate.mdx +0 -436
  444. package/site/content/docs/elements/signal.mdx +0 -380
  445. package/site/content/docs/elements/store.mdx +0 -1099
  446. package/site/content/docs/elements/vault.mdx +0 -405
  447. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  448. package/site/content/docs/get-started/index.mdx +0 -43
  449. package/site/content/docs/get-started/installation.mdx +0 -220
  450. package/site/content/docs/get-started/introduction.mdx +0 -132
  451. package/site/content/docs/get-started/meta.json +0 -13
  452. package/site/content/docs/get-started/project-structure.mdx +0 -925
  453. package/site/content/docs/get-started/testing.mdx +0 -328
  454. package/site/content/docs/get-started/why.mdx +0 -162
  455. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  456. package/site/content/docs/recipes/ollama.mdx +0 -142
  457. package/site/content/docs/recipes/sglang.mdx +0 -105
  458. package/site/content/docs/recipes/vllm.mdx +0 -106
  459. package/site/content/docs/reference/cli.md +0 -232
  460. package/site/content/docs/reference/client.mdx +0 -469
  461. package/site/content/docs/reference/security.md +0 -74
  462. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  463. package/src/cli/ai-setup/recommend.test.ts +0 -225
  464. package/src/cli/ai-setup/recommend.ts +0 -225
  465. package/src/cli/dev-controls.test.ts +0 -122
  466. package/src/cli/dev-controls.ts +0 -164
  467. package/src/cli/tui/DevLive.tsx +0 -126
  468. package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
  469. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  470. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  471. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  472. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  473. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  474. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  475. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  476. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  477. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  478. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  479. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  480. package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
  481. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  482. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  483. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  484. package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
  485. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  486. package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
  487. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  488. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  489. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  490. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  491. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  492. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  493. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  494. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  495. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  496. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  497. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  498. package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
  499. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  500. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  501. package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
  502. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  503. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  504. package/src/docker/ollama-pull.ts +0 -232
  505. package/src/docker/recipes/llama-cpp.ts +0 -298
  506. package/src/docker/recipes/ollama.ts +0 -44
  507. package/src/docker/recipes/sglang.ts +0 -55
  508. package/src/docker/recipes/vllm.ts +0 -44
  509. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  510. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  511. package/src/drivers/ai-ollama.ts +0 -389
  512. package/src/drivers/ollama.ts +0 -14
@@ -18,8 +18,12 @@ import type {
18
18
  ChannelSendResult,
19
19
  SmsOtpTransport,
20
20
  } from "../../drivers/channel-types.ts";
21
+ import type { DriverExternal } from "../../drivers/external.ts";
21
22
  import type { ChannelMedium } from "../../manifest/types.ts";
23
+ import { CHANNEL_SCHEMA } from "../../kernel/errors-channel.ts";
24
+ import { OkeError } from "../../kernel/errors.ts";
22
25
  import { emitBootWarn } from "../../runtime/boot-warn.ts";
26
+ import { isStandardSchema, type SchemaInput } from "../../validation/standard-schema.ts";
23
27
  import type { ConsentStore } from "./consent.ts";
24
28
  import type { ChannelTemplateDecl } from "./declare.ts";
25
29
  import { DEFAULT_MEDIUM_COSTS, type MediumCosts } from "./costs.ts";
@@ -50,6 +54,33 @@ export type TemplateCatalog = Readonly<
50
54
  >
51
55
  >;
52
56
 
57
+ /**
58
+ * Resolve driver-reported egress for a provider label in a fallback chain.
59
+ *
60
+ * @param drivers - Ordered driver chain
61
+ * @param provider - Provider / driver id that handled (or failed) the attempt
62
+ * @param index - Optional transport index when known
63
+ */
64
+ function externalForChannelProvider(
65
+ drivers: readonly ChannelDriver[],
66
+ provider: string,
67
+ index?: number,
68
+ ): DriverExternal | undefined {
69
+ if (typeof index === "number") {
70
+ const at = drivers[index]?.external;
71
+ if (at) return at;
72
+ }
73
+ const match = drivers.find(
74
+ (d) =>
75
+ d.id === provider ||
76
+ d.external?.provider === provider ||
77
+ d.transport?.provider === provider ||
78
+ d.smsTransport?.provider === provider ||
79
+ d.channel?.provider === provider,
80
+ );
81
+ return match?.external;
82
+ }
83
+
53
84
  /** Options for {@link createChannelRuntime}. */
54
85
  export interface CreateChannelRuntimeOptions {
55
86
  /** Declared templates. */
@@ -266,27 +297,29 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
266
297
  const provider =
267
298
  transports[failedIndex]?.provider ?? chain[failedIndex]?.id ?? `driver-${failedIndex}`;
268
299
  recorded.push({ provider, error });
300
+ const external = externalForChannelProvider(chain, provider, failedIndex);
269
301
  attempts.push({
270
302
  driverId: provider,
271
303
  ok: false,
272
304
  error: error instanceof Error ? error.message : String(error),
273
305
  at: now(),
306
+ ...(external !== undefined ? { external } : {}),
274
307
  });
275
308
  },
276
309
  });
277
310
 
278
311
  try {
279
312
  const mailResult = await fallback.send(mail);
313
+ const providerIndex = mailResult.providerIndex ?? 0;
280
314
  const driverId =
281
- mailResult.provider ??
282
- transports[mailResult.providerIndex ?? 0]?.provider ??
283
- chain[0]?.id ??
284
- "email";
315
+ mailResult.provider ?? transports[providerIndex]?.provider ?? chain[0]?.id ?? "email";
316
+ const external = externalForChannelProvider(chain, driverId, providerIndex);
285
317
  attempts.push({
286
318
  driverId,
287
319
  ok: true,
288
320
  at: now(),
289
321
  messageId: mailResult.messageId,
322
+ ...(external !== undefined ? { external } : {}),
290
323
  });
291
324
  return {
292
325
  result: {
@@ -295,6 +328,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
295
328
  driverId,
296
329
  attempts,
297
330
  mail: mailResult,
331
+ ...(external !== undefined ? { external } : {}),
298
332
  },
299
333
  attempts,
300
334
  };
@@ -308,30 +342,37 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
308
342
  : recorded;
309
343
  for (const a of fbAttempts) {
310
344
  if (!attempts.some((x) => x.driverId === a.provider && !x.ok)) {
345
+ const external = externalForChannelProvider(chain, a.provider);
311
346
  attempts.push({
312
347
  driverId: a.provider,
313
348
  ok: false,
314
349
  error: a.error instanceof Error ? a.error.message : String(a.error),
315
350
  at: now(),
351
+ ...(external !== undefined ? { external } : {}),
316
352
  });
317
353
  }
318
354
  }
319
355
  // Permanent client errors abort failover by rethrowing the original error
320
356
  // (no FallbackError.attempts / onFallback) — still record the failed try.
321
357
  if (attempts.length === 0) {
358
+ const driverId = transports[0]?.provider ?? chain[0]?.id ?? "email";
359
+ const external = externalForChannelProvider(chain, driverId, 0);
322
360
  attempts.push({
323
- driverId: transports[0]?.provider ?? chain[0]?.id ?? "email",
361
+ driverId,
324
362
  ok: false,
325
363
  error: err instanceof Error ? err.message : String(err),
326
364
  at: now(),
365
+ ...(external !== undefined ? { external } : {}),
327
366
  });
328
367
  }
368
+ const lastExternal = attempts[attempts.length - 1]?.external;
329
369
  return {
330
370
  result: {
331
371
  ok: false,
332
372
  messageId: crypto.randomUUID(),
333
373
  driverId: "fallback",
334
374
  attempts,
375
+ ...(lastExternal !== undefined ? { external: lastExternal } : {}),
335
376
  },
336
377
  attempts,
337
378
  };
@@ -361,11 +402,17 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
361
402
  onFallback(failedIndex, error) {
362
403
  const provider =
363
404
  transports[failedIndex]?.provider ?? sms[failedIndex]?.driver.id ?? `sms-${failedIndex}`;
405
+ const external = externalForChannelProvider(
406
+ sms.map((s) => s.driver),
407
+ provider,
408
+ failedIndex,
409
+ );
364
410
  attempts.push({
365
411
  driverId: provider,
366
412
  ok: false,
367
413
  error: error instanceof Error ? error.message : String(error),
368
414
  at: now(),
415
+ ...(external !== undefined ? { external } : {}),
369
416
  });
370
417
  },
371
418
  });
@@ -378,16 +425,17 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
378
425
 
379
426
  try {
380
427
  const sendResult = await fallback.send(body);
428
+ const providerIndex = sendResult.providerIndex ?? 0;
381
429
  const driverId =
382
- sendResult.provider ??
383
- transports[sendResult.providerIndex ?? 0]?.provider ??
384
- sms[0]?.driver.id ??
385
- "sms";
430
+ sendResult.provider ?? transports[providerIndex]?.provider ?? sms[0]?.driver.id ?? "sms";
431
+ const smsDrivers = sms.map((s) => s.driver);
432
+ const external = externalForChannelProvider(smsDrivers, driverId, providerIndex);
386
433
  attempts.push({
387
434
  driverId,
388
435
  ok: true,
389
436
  at: now(),
390
437
  messageId: sendResult.messageId,
438
+ ...(external !== undefined ? { external } : {}),
391
439
  });
392
440
  return {
393
441
  result: {
@@ -395,10 +443,12 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
395
443
  messageId: sendResult.messageId,
396
444
  driverId,
397
445
  attempts,
446
+ ...(external !== undefined ? { external } : {}),
398
447
  },
399
448
  attempts,
400
449
  };
401
450
  } catch (err) {
451
+ const smsDrivers = sms.map((s) => s.driver);
402
452
  const fbAttempts =
403
453
  err &&
404
454
  typeof err === "object" &&
@@ -408,28 +458,35 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
408
458
  : [];
409
459
  for (const a of fbAttempts) {
410
460
  if (!attempts.some((x) => x.driverId === a.provider && !x.ok)) {
461
+ const external = externalForChannelProvider(smsDrivers, a.provider);
411
462
  attempts.push({
412
463
  driverId: a.provider,
413
464
  ok: false,
414
465
  error: a.error instanceof Error ? a.error.message : String(a.error),
415
466
  at: now(),
467
+ ...(external !== undefined ? { external } : {}),
416
468
  });
417
469
  }
418
470
  }
419
471
  if (attempts.length === 0) {
472
+ const driverId = transports[0]?.provider ?? sms[0]?.driver.id ?? "sms";
473
+ const external = externalForChannelProvider(smsDrivers, driverId, 0);
420
474
  attempts.push({
421
- driverId: transports[0]?.provider ?? sms[0]?.driver.id ?? "sms",
475
+ driverId,
422
476
  ok: false,
423
477
  error: err instanceof Error ? err.message : String(err),
424
478
  at: now(),
479
+ ...(external !== undefined ? { external } : {}),
425
480
  });
426
481
  }
482
+ const lastExternal = attempts[attempts.length - 1]?.external;
427
483
  return {
428
484
  result: {
429
485
  ok: false,
430
486
  messageId: crypto.randomUUID(),
431
487
  driverId: "fallback",
432
488
  attempts,
489
+ ...(lastExternal !== undefined ? { external: lastExternal } : {}),
433
490
  },
434
491
  attempts,
435
492
  };
@@ -450,10 +507,19 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
450
507
  if (!d.channel) continue;
451
508
  try {
452
509
  const r = await d.channel.send(message);
453
- attempts.push(...r.attempts);
510
+ const stampedAttempts = r.attempts.map((a) => ({
511
+ ...a,
512
+ ...(a.external === undefined && d.external !== undefined ? { external: d.external } : {}),
513
+ }));
514
+ attempts.push(...stampedAttempts);
454
515
  if (r.ok) {
516
+ const external = r.external ?? d.external;
455
517
  return {
456
- result: { ...r, attempts: [...attempts] },
518
+ result: {
519
+ ...r,
520
+ attempts: [...attempts],
521
+ ...(external !== undefined ? { external } : {}),
522
+ },
457
523
  attempts,
458
524
  };
459
525
  }
@@ -463,6 +529,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
463
529
  ok: false,
464
530
  error: err instanceof Error ? err.message : String(err),
465
531
  at: now(),
532
+ ...(d.external !== undefined ? { external: d.external } : {}),
466
533
  });
467
534
  }
468
535
  }
@@ -511,6 +578,31 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
511
578
  );
512
579
  }
513
580
 
581
+ async function validateChannelTemplateData(
582
+ template: string,
583
+ decl: ChannelTemplateDecl,
584
+ data: unknown,
585
+ ): Promise<void> {
586
+ const schema = decl.schema as SchemaInput | undefined;
587
+ if (schema === undefined || schema === null || !isStandardSchema(schema)) {
588
+ return;
589
+ }
590
+ const result = await schema["~standard"].validate(data ?? {});
591
+ if (!result.issues) return;
592
+ const detail =
593
+ result.issues
594
+ .map((i) => {
595
+ const path = Array.isArray(i.path)
596
+ ? i.path
597
+ .map((p) => (typeof p === "object" && p && "key" in p ? String(p.key) : String(p)))
598
+ .join(".")
599
+ : "";
600
+ return path.length > 0 ? `${path}: ${i.message}` : i.message;
601
+ })
602
+ .join("; ") || "invalid payload";
603
+ throw new OkeError(CHANNEL_SCHEMA, { resource: template, detail });
604
+ }
605
+
514
606
  async function sendTemplate(
515
607
  template: string,
516
608
  opts: ChannelSendOptions,
@@ -519,6 +611,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
519
611
  if (!decl) {
520
612
  throw new Error(`channel: unknown template "${template}"`);
521
613
  }
614
+ await validateChannelTemplateData(template, decl, opts.data);
522
615
  const medium = decl.medium;
523
616
  const checkMedium: ChannelMedium = medium === "any" ? "email" : medium;
524
617
 
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Structured cron fields + clock convenience helpers.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach } from "bun:test";
6
+
7
+ import { buildCronExpression, parseAtTime, serializeCronField } from "./cron-fields.ts";
8
+ import { applyClockTimezoneDefaults, clock, listClocks, resetClocks } from "./declare.ts";
9
+
10
+ describe("buildCronExpression", () => {
11
+ test("at sugar sets minute and hour", () => {
12
+ expect(buildCronExpression({ at: "06:00" })).toBe("0 6 * * *");
13
+ expect(buildCronExpression({ at: "6:00" })).toBe("0 6 * * *");
14
+ expect(buildCronExpression({ at: "08:30" })).toBe("30 8 * * *");
15
+ });
16
+
17
+ test("lists, steps, and weekday names", () => {
18
+ expect(
19
+ buildCronExpression({
20
+ minute: "*/15",
21
+ hour: [9, 12, 17],
22
+ dayOfWeek: "1-5",
23
+ }),
24
+ ).toBe("*/15 9,12,17 * * 1-5");
25
+
26
+ expect(
27
+ buildCronExpression({
28
+ at: "08:00",
29
+ dayOfWeek: ["mon", "wed", "fri"],
30
+ }),
31
+ ).toBe("0 8 * * 1,3,5");
32
+ });
33
+
34
+ test("at does not override explicit minute/hour", () => {
35
+ expect(buildCronExpression({ at: "06:00", minute: 15 })).toBe("15 6 * * *");
36
+ expect(buildCronExpression({ at: "06:00", hour: 9 })).toBe("0 9 * * *");
37
+ });
38
+
39
+ test("rejects empty bag and bad at", () => {
40
+ expect(() => buildCronExpression({})).toThrow(/require at least one/);
41
+ expect(() => parseAtTime("nope")).toThrow(/HH:MM/);
42
+ expect(() => parseAtTime("25:00")).toThrow(/hour/);
43
+ expect(() => parseAtTime("12:99")).toThrow(/minute/);
44
+ });
45
+
46
+ test("serializeCronField maps day names", () => {
47
+ expect(serializeCronField("tue", "dayOfWeek")).toBe("2");
48
+ expect(serializeCronField(["sun", "sat"], "dayOfWeek")).toBe("0,6");
49
+ });
50
+ });
51
+
52
+ describe("clock helpers", () => {
53
+ beforeEach(() => {
54
+ resetClocks();
55
+ });
56
+
57
+ test("daily / hourly / weekly / monthly / every / cron string", () => {
58
+ expect(clock.daily("reports.daily", { at: "06:00", timezone: "Asia/Riyadh" })).toMatchObject({
59
+ name: "reports.daily",
60
+ cron: "0 6 * * *",
61
+ timezone: "Asia/Riyadh",
62
+ });
63
+
64
+ expect(clock.hourly("metrics.rollup", { minute: 15 })).toMatchObject({
65
+ cron: "15 * * * *",
66
+ });
67
+
68
+ expect(clock.weekly("digest", { on: ["mon", "fri"], at: "09:00" })).toMatchObject({
69
+ cron: "0 9 * * 1,5",
70
+ });
71
+
72
+ expect(clock.monthly("billing", { on: [1, 15], at: "00:00" })).toMatchObject({
73
+ cron: "0 0 1,15 * *",
74
+ });
75
+
76
+ expect(clock.every("health.ping", "30s")).toMatchObject({
77
+ every: "30s",
78
+ cron: undefined,
79
+ });
80
+
81
+ expect(clock.cron("legacy", "@hourly")).toMatchObject({
82
+ cron: "@hourly",
83
+ });
84
+ });
85
+
86
+ test("clock.cron structured fields", () => {
87
+ const decl = clock.cron("ops.sweep", {
88
+ minute: "*/15",
89
+ hour: [9, 12, 17],
90
+ dayOfWeek: "1-5",
91
+ timezone: "UTC",
92
+ overridable: true,
93
+ description: "weekday sweeps",
94
+ });
95
+ expect(decl).toMatchObject({
96
+ name: "ops.sweep",
97
+ cron: "*/15 9,12,17 * * 1-5",
98
+ timezone: "UTC",
99
+ overridable: true,
100
+ description: "weekday sweeps",
101
+ });
102
+ });
103
+
104
+ test("invalid cron fails at declare", () => {
105
+ expect(() => clock.cron("bad", "not a cron")).toThrow(/invalid cron/);
106
+ expect(() => clock("bad", { cron: "99 99 * * *" })).toThrow(/invalid cron/);
107
+ });
108
+
109
+ test("helpers register into listClocks", () => {
110
+ clock.daily("a", { at: "01:00" });
111
+ clock.every("b", "1h");
112
+ expect(listClocks().map((c) => c.name)).toEqual(["a", "b"]);
113
+ });
114
+
115
+ test("omitted timezone is marked defaulted", () => {
116
+ const d = clock.daily("x", { at: "06:00" });
117
+ expect(d.timezone).toBe("UTC");
118
+ expect(d.timezoneDefaulted).toBe(true);
119
+ const explicit = clock.daily("y", { at: "06:00", timezone: "UTC" });
120
+ expect(explicit.timezoneDefaulted).toBeUndefined();
121
+ });
122
+ });
123
+
124
+ describe("applyClockTimezoneDefaults", () => {
125
+ beforeEach(() => {
126
+ resetClocks();
127
+ });
128
+
129
+ test("rewrites defaulted clocks; keeps explicit", () => {
130
+ const a = clock.daily("a", { at: "06:00" });
131
+ const b = clock.daily("b", { at: "07:00", timezone: "Europe/London" });
132
+ const out = applyClockTimezoneDefaults([a, b], "Asia/Riyadh");
133
+ expect(out[0]).toMatchObject({ name: "a", timezone: "Asia/Riyadh" });
134
+ expect(out[0]?.timezoneDefaulted).toBeUndefined();
135
+ expect(out[1]).toMatchObject({ name: "b", timezone: "Europe/London" });
136
+ });
137
+
138
+ test("no-op when default timezone omitted", () => {
139
+ const a = clock.daily("a", { at: "06:00" });
140
+ const out = applyClockTimezoneDefaults([a], undefined);
141
+ expect(out[0]?.timezone).toBe("UTC");
142
+ expect(out[0]?.timezoneDefaulted).toBe(true);
143
+ });
144
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Structured cron field bag → five-field expression.
3
+ *
4
+ * Used by `clock.cron` / `clock.daily` / … so Manifest + Store still store a
5
+ * plain cron string. Only Bun five-field + nicknames — no Quartz calendars.
6
+ */
7
+
8
+ /** One cron field: scalar, step/range string, or list. */
9
+ export type CronField = number | string | readonly (number | string)[];
10
+
11
+ /**
12
+ * Structured schedule fields for {@link buildCronExpression}.
13
+ *
14
+ * Omit a field to leave it `*`. `at: "HH:MM"` sets minute + hour when those
15
+ * are unset.
16
+ */
17
+ export interface CronFields {
18
+ /** Minute (`0` · `[0, 30]` · step string like every-15). */
19
+ readonly minute?: CronField;
20
+ /** Hour (`9` · `[9, 12, 17]` · `"9-17"`). */
21
+ readonly hour?: CronField;
22
+ /** Day of month (`1` · `[1, 15]` · `"1-7"`). */
23
+ readonly dayOfMonth?: CronField;
24
+ /** Month (`1` · `[1, 6]` · step string). */
25
+ readonly month?: CronField;
26
+ /** Day of week (`1` · `"mon"` · `["mon", "fri"]` · `"1-5"`). */
27
+ readonly dayOfWeek?: CronField;
28
+ /** Wall-clock sugar `"HH:MM"` / `"H:MM"` → minute + hour when unset. */
29
+ readonly at?: string;
30
+ }
31
+
32
+ const DAY_NAME_TO_NUM: Readonly<Record<string, number>> = {
33
+ sun: 0,
34
+ sunday: 0,
35
+ mon: 1,
36
+ monday: 1,
37
+ tue: 2,
38
+ tues: 2,
39
+ tuesday: 2,
40
+ wed: 3,
41
+ wednesday: 3,
42
+ thu: 4,
43
+ thur: 4,
44
+ thurs: 4,
45
+ thursday: 4,
46
+ fri: 5,
47
+ friday: 5,
48
+ sat: 6,
49
+ saturday: 6,
50
+ };
51
+
52
+ /**
53
+ * Parse `at: "06:00"` / `"6:00"` into `{ minute, hour }`.
54
+ *
55
+ * @param at - Wall-clock string
56
+ * @throws TypeError when the format is invalid
57
+ */
58
+ export function parseAtTime(at: string): { readonly minute: number; readonly hour: number } {
59
+ const trimmed = at.trim();
60
+ const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
61
+ if (!m) {
62
+ throw new TypeError(`cron at: expected "HH:MM", got ${JSON.stringify(at)}`);
63
+ }
64
+ const hour = Number(m[1]);
65
+ const minute = Number(m[2]);
66
+ if (!Number.isInteger(hour) || hour < 0 || hour > 23) {
67
+ throw new TypeError(`cron at: hour must be 0–23, got ${JSON.stringify(at)}`);
68
+ }
69
+ if (!Number.isInteger(minute) || minute < 0 || minute > 59) {
70
+ throw new TypeError(`cron at: minute must be 0–59, got ${JSON.stringify(at)}`);
71
+ }
72
+ return { hour, minute };
73
+ }
74
+
75
+ /**
76
+ * Normalize one field token (day names → 0–6; numbers → string).
77
+ *
78
+ * @param value - Scalar field piece
79
+ * @param kind - Which cron slot (day names only apply to `dayOfWeek`)
80
+ */
81
+ function normalizeToken(value: number | string, kind: "dayOfWeek" | "other"): string {
82
+ if (typeof value === "number") {
83
+ if (!Number.isInteger(value)) {
84
+ throw new TypeError(`cron field: expected integer, got ${value}`);
85
+ }
86
+ return String(value);
87
+ }
88
+ const trimmed = value.trim();
89
+ if (!trimmed) {
90
+ throw new TypeError("cron field: empty string");
91
+ }
92
+ if (kind === "dayOfWeek") {
93
+ const lower = trimmed.toLowerCase();
94
+ const mapped = DAY_NAME_TO_NUM[lower];
95
+ if (mapped !== undefined) return String(mapped);
96
+ }
97
+ return trimmed;
98
+ }
99
+
100
+ /**
101
+ * Serialize one cron field to a crontab token (`*` when omitted).
102
+ *
103
+ * @param field - Optional structured field
104
+ * @param kind - Slot kind for day-name mapping
105
+ */
106
+ export function serializeCronField(
107
+ field: CronField | undefined,
108
+ kind: "dayOfWeek" | "other" = "other",
109
+ ): string {
110
+ if (field === undefined) return "*";
111
+ if (Array.isArray(field)) {
112
+ if (field.length === 0) {
113
+ throw new TypeError("cron field: empty list");
114
+ }
115
+ return field.map((v) => normalizeToken(v as number | string, kind)).join(",");
116
+ }
117
+ return normalizeToken(field as number | string, kind);
118
+ }
119
+
120
+ /**
121
+ * Whether the bag has any schedule signal (fields or `at`).
122
+ *
123
+ * @param fields - Structured bag
124
+ */
125
+ function hasScheduleSignal(fields: CronFields): boolean {
126
+ return (
127
+ fields.at !== undefined ||
128
+ fields.minute !== undefined ||
129
+ fields.hour !== undefined ||
130
+ fields.dayOfMonth !== undefined ||
131
+ fields.month !== undefined ||
132
+ fields.dayOfWeek !== undefined
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Build a five-field cron expression from structured fields.
138
+ *
139
+ * @param fields - Field bag (`at` + minute/hour/dom/mon/dow)
140
+ * @throws TypeError when empty or `at` is invalid
141
+ */
142
+ export function buildCronExpression(fields: CronFields): string {
143
+ if (!hasScheduleSignal(fields)) {
144
+ throw new TypeError(
145
+ "cron fields: require at least one of at, minute, hour, dayOfMonth, month, dayOfWeek",
146
+ );
147
+ }
148
+
149
+ let minute = fields.minute;
150
+ let hour = fields.hour;
151
+ if (fields.at !== undefined) {
152
+ const parsed = parseAtTime(fields.at);
153
+ if (minute === undefined) minute = parsed.minute;
154
+ if (hour === undefined) hour = parsed.hour;
155
+ }
156
+
157
+ return [
158
+ serializeCronField(minute),
159
+ serializeCronField(hour),
160
+ serializeCronField(fields.dayOfMonth),
161
+ serializeCronField(fields.month),
162
+ serializeCronField(fields.dayOfWeek, "dayOfWeek"),
163
+ ].join(" ");
164
+ }
165
+
166
+ /**
167
+ * Assert `Bun.cron.parse` accepts the expression (fail loud at declare).
168
+ *
169
+ * @param expression - Five-field cron or Bun nickname
170
+ * @param label - Error prefix (clock name / helper)
171
+ */
172
+ export function assertValidCronExpression(expression: string, label: string): void {
173
+ try {
174
+ (
175
+ Bun.cron.parse as (
176
+ expression: string,
177
+ relativeDate?: Date | number,
178
+ options?: { readonly tz?: string },
179
+ ) => Date | null
180
+ )(expression, Date.now(), { tz: "UTC" });
181
+ } catch (err) {
182
+ const msg = err instanceof Error ? err.message : String(err);
183
+ throw new TypeError(`${label}: invalid cron ${JSON.stringify(expression)} (${msg})`);
184
+ }
185
+ }