okengine 0.2.6 → 0.3.2

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 (681) hide show
  1. package/AGENTS.md +33 -30
  2. package/README.md +74 -340
  3. package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +40 -9
  4. package/package.json +47 -44
  5. package/site/content/docs/ai/llms-txt.mdx +54 -0
  6. package/site/content/docs/ai/mcp.mdx +123 -0
  7. package/site/content/docs/ai/meta.json +5 -0
  8. package/site/content/docs/ai/skills.mdx +53 -0
  9. package/site/content/docs/console/access.mdx +29 -0
  10. package/site/content/docs/console/ai.mdx +35 -0
  11. package/site/content/docs/console/architecture.mdx +35 -0
  12. package/site/content/docs/console/channels.mdx +37 -0
  13. package/site/content/docs/console/clock.mdx +31 -0
  14. package/site/content/docs/console/flows.mdx +31 -0
  15. package/site/content/docs/console/gates.mdx +35 -0
  16. package/site/content/docs/console/manifest-diff.mdx +34 -0
  17. package/site/content/docs/console/meta.json +23 -0
  18. package/site/content/docs/console/overview.mdx +40 -0
  19. package/site/content/docs/console/plugins.mdx +41 -0
  20. package/site/content/docs/console/privacy.mdx +32 -0
  21. package/site/content/docs/console/runs.mdx +40 -0
  22. package/site/content/docs/console/signals.mdx +31 -0
  23. package/site/content/docs/console/store.mdx +32 -0
  24. package/site/content/docs/console/tenancy.mdx +32 -0
  25. package/site/content/docs/console/traces.mdx +34 -0
  26. package/site/content/docs/console/vault.mdx +37 -0
  27. package/site/content/docs/elements/ai.mdx +180 -0
  28. package/site/content/docs/elements/channel.mdx +167 -0
  29. package/site/content/docs/elements/clock.mdx +182 -0
  30. package/site/content/docs/elements/flow.mdx +288 -0
  31. package/site/content/docs/elements/gate.mdx +171 -0
  32. package/site/content/docs/elements/meta.json +5 -0
  33. package/site/content/docs/elements/signal.mdx +171 -0
  34. package/site/content/docs/elements/store.mdx +320 -0
  35. package/site/content/docs/elements/vault.mdx +263 -0
  36. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  37. package/site/content/docs/get-started/comparison.mdx +65 -0
  38. package/site/content/docs/get-started/installation.mdx +113 -0
  39. package/site/content/docs/get-started/introduction.mdx +123 -0
  40. package/site/content/docs/get-started/meta.json +5 -0
  41. package/site/content/docs/index.mdx +63 -0
  42. package/site/content/docs/meta.json +5 -0
  43. package/site/content/docs/plugins/compression.mdx +60 -0
  44. package/site/content/docs/plugins/cors.mdx +92 -0
  45. package/site/content/docs/plugins/csrf.mdx +96 -0
  46. package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
  47. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  48. package/site/content/docs/plugins/meta.json +15 -0
  49. package/site/content/docs/plugins/security-headers.mdx +136 -0
  50. package/site/content/docs/reference/cli.md +101 -0
  51. package/site/content/docs/reference/configuration.mdx +159 -0
  52. package/site/content/docs/reference/environment-variables.mdx +87 -0
  53. package/site/content/docs/reference/errors.mdx +80 -0
  54. package/site/content/docs/reference/fx.mdx +117 -0
  55. package/site/content/docs/reference/meta.json +5 -0
  56. package/site/content/docs/reference/plugins.mdx +249 -0
  57. package/site/content/docs/reference/security.md +63 -0
  58. package/src/auth/api-keys.ts +2 -5
  59. package/src/auth/attenuation.ts +2 -6
  60. package/src/auth/auth.test.ts +56 -31
  61. package/src/auth/invoke-as.ts +1 -3
  62. package/src/auth/operator.ts +8 -26
  63. package/src/auth/planes.ts +2 -7
  64. package/src/auth/plugin.ts +4 -7
  65. package/src/auth/roles.ts +1 -5
  66. package/src/auth/sessions.ts +10 -39
  67. package/src/cli/args.ts +1 -5
  68. package/src/cli/ask-dev-mode.ts +40 -0
  69. package/src/cli/build.ts +1 -2
  70. package/src/cli/client-add.test.ts +8 -15
  71. package/src/cli/client-add.ts +5 -14
  72. package/src/cli/completion.test.ts +1 -5
  73. package/src/cli/completion.ts +4 -17
  74. package/src/cli/db-auto-push.test.ts +64 -0
  75. package/src/cli/db-auto-push.ts +72 -0
  76. package/src/cli/db.test.ts +189 -0
  77. package/src/cli/db.ts +456 -0
  78. package/src/cli/dev-app-runner.ts +2 -8
  79. package/src/cli/dev-db-push.test.ts +193 -0
  80. package/src/cli/dev-mode.test.ts +45 -0
  81. package/src/cli/dev-mode.ts +78 -0
  82. package/src/cli/dev-schema-sync.test.ts +66 -0
  83. package/src/cli/dev-schema-sync.ts +139 -0
  84. package/src/cli/dev.test.ts +264 -15
  85. package/src/cli/dev.ts +355 -139
  86. package/src/cli/doc-staleness.test.ts +69 -0
  87. package/src/cli/docker-cli.test.ts +27 -8
  88. package/src/cli/docker.ts +11 -3
  89. package/src/cli/doctor-diff.test.ts +1 -5
  90. package/src/cli/doctor-diff.ts +17 -46
  91. package/src/cli/doctor-pii.ts +2 -7
  92. package/src/cli/doctor.test.ts +26 -4
  93. package/src/cli/doctor.ts +35 -11
  94. package/src/cli/drizzle-env.test.ts +67 -0
  95. package/src/cli/drizzle-env.ts +78 -0
  96. package/src/cli/ensure-drizzle-config.ts +50 -0
  97. package/src/cli/eval.ts +4 -14
  98. package/src/cli/gates-list.ts +1 -4
  99. package/src/cli/hero-meta.test.ts +17 -19
  100. package/src/cli/hero-meta.ts +20 -30
  101. package/src/cli/images.ts +3 -8
  102. package/src/cli/index.ts +11 -2
  103. package/src/cli/load-config.images.test.ts +14 -14
  104. package/src/cli/load-config.ts +47 -47
  105. package/src/cli/mcp-from-console.ts +1 -4
  106. package/src/cli/mode.ts +65 -0
  107. package/src/cli/openbao-bootstrap.test.ts +147 -0
  108. package/src/cli/openbao-bootstrap.ts +280 -0
  109. package/src/cli/openbao-restart.integration.test.ts +136 -0
  110. package/src/cli/ports.test.ts +7 -14
  111. package/src/cli/ports.ts +7 -5
  112. package/src/cli/privacy-erase.ts +1 -3
  113. package/src/cli/registry.ts +75 -25
  114. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  115. package/src/cli/resolve-dev-sql-env.ts +42 -0
  116. package/src/cli/schema.ts +3 -10
  117. package/src/cli/stack.ts +8 -10
  118. package/src/cli/start.ts +4 -15
  119. package/src/cli/upgrade.ts +1 -4
  120. package/src/cli/vault-cmd.ts +63 -0
  121. package/src/client/budget-entry.ts +1 -6
  122. package/src/client/create.test.ts +6 -12
  123. package/src/client/create.ts +2 -8
  124. package/src/client/errors.ts +1 -4
  125. package/src/client/index.ts +1 -6
  126. package/src/client/notes-contract.test.ts +4 -13
  127. package/src/client/transport.ts +9 -30
  128. package/src/client/types.ts +15 -30
  129. package/src/compiler/aot.test.ts +2 -9
  130. package/src/compiler/aot.ts +1 -4
  131. package/src/compiler/differential.test.ts +3 -11
  132. package/src/compiler/dynamic.ts +1 -4
  133. package/src/compiler/effects-infer.ts +8 -34
  134. package/src/compiler/emit.ts +1 -3
  135. package/src/compiler/extract.test.ts +136 -30
  136. package/src/compiler/extract.ts +485 -157
  137. package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
  138. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -3
  139. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -3
  140. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +4 -1
  141. package/src/compiler/fixtures/skyport.expected.json +24 -83
  142. package/src/compiler/fixtures/triggers/five-triggers.ts +1 -2
  143. package/src/compiler/http-parse.ts +2 -9
  144. package/src/compiler/index.ts +1 -5
  145. package/src/compiler/response.ts +16 -7
  146. package/src/compiler/sucrose.ts +11 -31
  147. package/src/config/define-config.test.ts +55 -34
  148. package/src/config/index.ts +177 -46
  149. package/src/config/resolve-driver.test.ts +28 -17
  150. package/src/console/budget.test.ts +1 -4
  151. package/src/console/index.ts +1 -4
  152. package/src/console/server/access.test.ts +15 -43
  153. package/src/console/server/access.ts +19 -49
  154. package/src/console/server/ai.test.ts +3 -11
  155. package/src/console/server/ai.ts +23 -46
  156. package/src/console/server/app.ts +16 -45
  157. package/src/console/server/auth-rate.test.ts +4 -12
  158. package/src/console/server/bind.ts +1 -5
  159. package/src/console/server/channels.test.ts +2 -6
  160. package/src/console/server/channels.ts +10 -35
  161. package/src/console/server/claim.ts +2 -8
  162. package/src/console/server/clock.test.ts +2 -6
  163. package/src/console/server/clock.ts +8 -26
  164. package/src/console/server/console.test.ts +1 -3
  165. package/src/console/server/diff.test.ts +5 -17
  166. package/src/console/server/diff.ts +10 -31
  167. package/src/console/server/dry-run.audit.test.ts +2 -8
  168. package/src/console/server/flows.ts +64 -233
  169. package/src/console/server/gates.test.ts +14 -50
  170. package/src/console/server/gates.ts +13 -48
  171. package/src/console/server/index.ts +2 -9
  172. package/src/console/server/live.ts +3 -12
  173. package/src/console/server/operator-db.test.ts +4 -15
  174. package/src/console/server/operator-db.ts +13 -32
  175. package/src/console/server/plugin.ts +50 -53
  176. package/src/console/server/plugins.test.ts +2 -6
  177. package/src/console/server/plugins.ts +17 -56
  178. package/src/console/server/runs-pii.ts +2 -7
  179. package/src/console/server/security.gate.test.ts +18 -40
  180. package/src/console/server/serve.ts +10 -25
  181. package/src/console/server/signals.test.ts +1 -3
  182. package/src/console/server/signals.ts +17 -40
  183. package/src/console/server/state.ts +21 -59
  184. package/src/console/server/store.test.ts +20 -22
  185. package/src/console/server/store.ts +31 -76
  186. package/src/console/server/vault.test.ts +12 -29
  187. package/src/console/server/vault.ts +44 -66
  188. package/src/console/ui/access/AccessA11yView.tsx +6 -21
  189. package/src/console/ui/access/a11y.test.tsx +3 -12
  190. package/src/console/ui/access/acknowledgement.test.ts +1 -4
  191. package/src/console/ui/access/acknowledgement.ts +1 -3
  192. package/src/console/ui/access/blast-radius.ts +1 -3
  193. package/src/console/ui/access/confirmation.test.ts +1 -5
  194. package/src/console/ui/access/confirmation.ts +1 -4
  195. package/src/console/ui/access/fixture.ts +1 -5
  196. package/src/console/ui/access/grantable.test.ts +1 -3
  197. package/src/console/ui/access/grantable.ts +2 -7
  198. package/src/console/ui/access/index.ts +2 -9
  199. package/src/console/ui/access/provenance.test.ts +1 -3
  200. package/src/console/ui/access/provenance.ts +2 -6
  201. package/src/console/ui/access/search.test.ts +1 -5
  202. package/src/console/ui/access/search.ts +2 -6
  203. package/src/console/ui/ai/AiA11yView.tsx +6 -17
  204. package/src/console/ui/ai/a11y.test.tsx +2 -5
  205. package/src/console/ui/ai/fixture.ts +2 -7
  206. package/src/console/ui/ai/format.ts +1 -3
  207. package/src/console/ui/ai/group.test.ts +1 -3
  208. package/src/console/ui/ai/group.ts +4 -12
  209. package/src/console/ui/ai/index.ts +2 -9
  210. package/src/console/ui/ai/promotion.test.ts +2 -7
  211. package/src/console/ui/ai/promotion.ts +4 -13
  212. package/src/console/ui/ai/search.test.ts +2 -11
  213. package/src/console/ui/ai/search.ts +3 -13
  214. package/src/console/ui/ai/types.ts +1 -8
  215. package/src/console/ui/architecture/ArchitectureA11yView.tsx +8 -29
  216. package/src/console/ui/architecture/a11y.test.tsx +1 -2
  217. package/src/console/ui/architecture/declared.ts +1 -5
  218. package/src/console/ui/architecture/index.ts +7 -35
  219. package/src/console/ui/architecture/layout.ts +1 -3
  220. package/src/console/ui/architecture/pathologies.ts +10 -19
  221. package/src/console/ui/architecture/search.ts +6 -15
  222. package/src/console/ui/architecture/traffic.ts +4 -14
  223. package/src/console/ui/architecture/types.ts +2 -10
  224. package/src/console/ui/architecture/view.test.ts +7 -19
  225. package/src/console/ui/architecture/view.ts +9 -36
  226. package/src/console/ui/channels/ChannelsA11yView.tsx +4 -13
  227. package/src/console/ui/channels/a11y.test.tsx +1 -2
  228. package/src/console/ui/channels/confirmation.test.ts +1 -4
  229. package/src/console/ui/channels/confirmation.ts +1 -4
  230. package/src/console/ui/channels/findings.ts +1 -5
  231. package/src/console/ui/channels/index.ts +2 -8
  232. package/src/console/ui/channels/locale.test.ts +1 -3
  233. package/src/console/ui/channels/locale.ts +2 -8
  234. package/src/console/ui/channels/search.ts +4 -13
  235. package/src/console/ui/channels/taxonomy.ts +1 -3
  236. package/src/console/ui/clock/ClockA11yView.tsx +9 -26
  237. package/src/console/ui/clock/a11y.test.tsx +2 -5
  238. package/src/console/ui/clock/confirmation.test.ts +1 -3
  239. package/src/console/ui/clock/confirmation.ts +1 -4
  240. package/src/console/ui/clock/findings.ts +1 -4
  241. package/src/console/ui/clock/health.ts +2 -7
  242. package/src/console/ui/clock/index.ts +2 -10
  243. package/src/console/ui/clock/search.test.ts +1 -3
  244. package/src/console/ui/clock/search.ts +2 -6
  245. package/src/console/ui/clock/timeline.test.ts +1 -3
  246. package/src/console/ui/clock/waiting-on.test.ts +1 -5
  247. package/src/console/ui/clock/waiting-on.ts +2 -7
  248. package/src/console/ui/diff/DiffA11yView.tsx +8 -17
  249. package/src/console/ui/diff/a11y.test.tsx +2 -5
  250. package/src/console/ui/diff/group.test.ts +3 -13
  251. package/src/console/ui/diff/group.ts +2 -8
  252. package/src/console/ui/diff/index.ts +2 -8
  253. package/src/console/ui/diff/search.test.ts +1 -5
  254. package/src/console/ui/diff/search.ts +4 -16
  255. package/src/console/ui/diff/summary.ts +1 -2
  256. package/src/console/ui/dist/assets/{index-Dy4jht9P.js → index-BWo8R7NR.js} +2 -2
  257. package/src/console/ui/dist/assets/{panel-access-Dd37LU2c.js → panel-access-C0J2D-a2.js} +1 -1
  258. package/src/console/ui/dist/assets/{panel-ai-CC7LR6-J.js → panel-ai-D_m6WQI8.js} +1 -1
  259. package/src/console/ui/dist/assets/{panel-architecture-B5b3iKCz.js → panel-architecture-CKnXFyUx.js} +1 -1
  260. package/src/console/ui/dist/assets/{panel-channels-CeNjTKXp.js → panel-channels-BOmQ-onL.js} +1 -1
  261. package/src/console/ui/dist/assets/{panel-clock-DgFTLoHV.js → panel-clock-giAq0Ccv.js} +1 -1
  262. package/src/console/ui/dist/assets/{panel-diff-DxehccqB.js → panel-diff-cdonmH8c.js} +1 -1
  263. package/src/console/ui/dist/assets/{panel-flows-BtrVn-Eg.js → panel-flows-DlCU5zjA.js} +2 -2
  264. package/src/console/ui/dist/assets/{panel-gates-Z9MKRGdH.js → panel-gates-XclZxWD5.js} +1 -1
  265. package/src/console/ui/dist/assets/{panel-overview-Dt_AeXgd.js → panel-overview-BznEOTnb.js} +1 -1
  266. package/src/console/ui/dist/assets/{panel-plugins-DWd0TowH.js → panel-plugins-CcGM1g64.js} +1 -1
  267. package/src/console/ui/dist/assets/{panel-runs-DGstFHeq.js → panel-runs-CGWNHLR4.js} +1 -1
  268. package/src/console/ui/dist/assets/{panel-signals-DzEa2Fnt.js → panel-signals-CNywkdak.js} +1 -1
  269. package/src/console/ui/dist/assets/{panel-store-BJkbNgxx.js → panel-store-KmTbFHMH.js} +1 -1
  270. package/src/console/ui/dist/assets/{panel-traces-BaRVO3gM.js → panel-traces-DBLx2ilD.js} +1 -1
  271. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +1 -0
  272. package/src/console/ui/dist/index.html +1 -1
  273. package/src/console/ui/flows/FlowsA11yView.tsx +7 -22
  274. package/src/console/ui/flows/a11y.test.tsx +2 -8
  275. package/src/console/ui/flows/buffer.test.ts +3 -11
  276. package/src/console/ui/flows/buffer.ts +4 -11
  277. package/src/console/ui/flows/confirmation.ts +1 -3
  278. package/src/console/ui/flows/contract.test.ts +3 -7
  279. package/src/console/ui/flows/contract.ts +12 -36
  280. package/src/console/ui/flows/graph.test.ts +3 -11
  281. package/src/console/ui/flows/graph.ts +5 -15
  282. package/src/console/ui/flows/index.ts +1 -6
  283. package/src/console/ui/flows/save-as-test.ts +1 -3
  284. package/src/console/ui/flows/search.ts +8 -25
  285. package/src/console/ui/flows/tiers.ts +2 -9
  286. package/src/console/ui/gates/GatesA11yView.tsx +4 -13
  287. package/src/console/ui/gates/a11y.test.tsx +2 -5
  288. package/src/console/ui/gates/audit.ts +1 -3
  289. package/src/console/ui/gates/denial.test.ts +1 -3
  290. package/src/console/ui/gates/denial.ts +4 -12
  291. package/src/console/ui/gates/findings.ts +1 -3
  292. package/src/console/ui/gates/group.test.ts +3 -3
  293. package/src/console/ui/gates/group.ts +3 -11
  294. package/src/console/ui/gates/index.ts +2 -10
  295. package/src/console/ui/gates/search.ts +3 -10
  296. package/src/console/ui/overview/OverviewA11yView.tsx +9 -27
  297. package/src/console/ui/overview/a11y.test.tsx +1 -2
  298. package/src/console/ui/overview/busiest.ts +1 -3
  299. package/src/console/ui/overview/compose.ts +1 -13
  300. package/src/console/ui/overview/cost.ts +34 -38
  301. package/src/console/ui/overview/fixture.ts +21 -26
  302. package/src/console/ui/overview/golden.ts +4 -14
  303. package/src/console/ui/overview/index.ts +2 -9
  304. package/src/console/ui/overview/rank.test.ts +3 -10
  305. package/src/console/ui/overview/rank.ts +2 -7
  306. package/src/console/ui/overview/slo.test.ts +5 -9
  307. package/src/console/ui/overview/slo.ts +6 -23
  308. package/src/console/ui/overview/verdict.ts +3 -10
  309. package/src/console/ui/plugins/PluginsA11yView.tsx +8 -18
  310. package/src/console/ui/plugins/a11y.test.tsx +1 -2
  311. package/src/console/ui/plugins/fixture.ts +2 -4
  312. package/src/console/ui/plugins/group.test.ts +1 -3
  313. package/src/console/ui/plugins/group.ts +1 -6
  314. package/src/console/ui/plugins/index.ts +3 -13
  315. package/src/console/ui/plugins/search.ts +7 -19
  316. package/src/console/ui/runs/RunsA11yView.tsx +5 -20
  317. package/src/console/ui/runs/a11y.test.tsx +2 -5
  318. package/src/console/ui/runs/explain.ts +2 -6
  319. package/src/console/ui/runs/group.ts +3 -12
  320. package/src/console/ui/runs/histogram.test.ts +2 -11
  321. package/src/console/ui/runs/histogram.ts +1 -4
  322. package/src/console/ui/runs/index.ts +4 -18
  323. package/src/console/ui/runs/project.ts +2 -6
  324. package/src/console/ui/runs/query.test.ts +1 -3
  325. package/src/console/ui/runs/query.ts +12 -35
  326. package/src/console/ui/runs/search.ts +5 -19
  327. package/src/console/ui/runs/trace-link.test.ts +2 -6
  328. package/src/console/ui/runs/trace-link.ts +4 -16
  329. package/src/console/ui/runs/types.ts +1 -3
  330. package/src/console/ui/shell/App.tsx +2 -10
  331. package/src/console/ui/shell/client.ts +22 -115
  332. package/src/console/ui/shell/components/ui.tsx +1 -4
  333. package/src/console/ui/shell/layout/Shell.tsx +2 -4
  334. package/src/console/ui/shell/main.tsx +31 -96
  335. package/src/console/ui/shell/panels/access/AccessPanel.tsx +34 -98
  336. package/src/console/ui/shell/panels/ai/AiPanel.tsx +35 -106
  337. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +18 -66
  338. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +20 -57
  339. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +22 -54
  340. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +20 -61
  341. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +6 -23
  342. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +13 -28
  343. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +9 -29
  344. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +24 -80
  345. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +20 -58
  346. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +16 -41
  347. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +31 -99
  348. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +38 -101
  349. package/src/console/ui/shell/panels/store/StorePanel.tsx +27 -66
  350. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +28 -89
  351. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +22 -53
  352. package/src/console/ui/shell/setup/Wizard.tsx +3 -5
  353. package/src/console/ui/shell/styles.css +1 -2
  354. package/src/console/ui/signals/SignalsA11yView.tsx +9 -33
  355. package/src/console/ui/signals/a11y.test.tsx +2 -5
  356. package/src/console/ui/signals/confirmation.test.ts +1 -5
  357. package/src/console/ui/signals/confirmation.ts +2 -6
  358. package/src/console/ui/signals/dry-run.ts +1 -3
  359. package/src/console/ui/signals/durable.ts +1 -3
  360. package/src/console/ui/signals/findings.ts +1 -3
  361. package/src/console/ui/signals/group.test.ts +2 -8
  362. package/src/console/ui/signals/group.ts +2 -10
  363. package/src/console/ui/signals/index.ts +1 -4
  364. package/src/console/ui/signals/monitor.ts +2 -7
  365. package/src/console/ui/signals/schema-form.test.ts +3 -14
  366. package/src/console/ui/signals/schema-form.ts +1 -4
  367. package/src/console/ui/signals/search.ts +4 -13
  368. package/src/console/ui/store/StoreA11yView.tsx +3 -9
  369. package/src/console/ui/store/a11y.test.tsx +2 -5
  370. package/src/console/ui/store/confirmation.test.ts +1 -3
  371. package/src/console/ui/store/confirmation.ts +1 -4
  372. package/src/console/ui/store/dry-run.ts +2 -5
  373. package/src/console/ui/store/fixture.ts +1 -2
  374. package/src/console/ui/store/group.test.ts +1 -6
  375. package/src/console/ui/store/group.ts +1 -4
  376. package/src/console/ui/store/index.ts +1 -5
  377. package/src/console/ui/store/search.ts +2 -6
  378. package/src/console/ui/store/will-not-fire.ts +1 -2
  379. package/src/console/ui/traces/TracesA11yView.tsx +10 -29
  380. package/src/console/ui/traces/a11y.test.tsx +1 -2
  381. package/src/console/ui/traces/chain.test.ts +5 -19
  382. package/src/console/ui/traces/chain.ts +1 -3
  383. package/src/console/ui/traces/critical-path.test.ts +1 -3
  384. package/src/console/ui/traces/critical-path.ts +2 -6
  385. package/src/console/ui/traces/filter.test.ts +2 -6
  386. package/src/console/ui/traces/filter.ts +5 -13
  387. package/src/console/ui/traces/fold.test.ts +4 -12
  388. package/src/console/ui/traces/fold.ts +1 -3
  389. package/src/console/ui/traces/mini.ts +1 -3
  390. package/src/console/ui/traces/replay.test.ts +1 -3
  391. package/src/console/ui/traces/replay.ts +1 -3
  392. package/src/console/ui/traces/sampling.ts +1 -4
  393. package/src/console/ui/traces/search.ts +4 -17
  394. package/src/console/ui/traces/tier.ts +1 -3
  395. package/src/console/ui/vault/VaultA11yView.tsx +5 -17
  396. package/src/console/ui/vault/a11y.test.tsx +1 -2
  397. package/src/console/ui/vault/blast-radius.ts +1 -3
  398. package/src/console/ui/vault/confirmation.test.ts +1 -5
  399. package/src/console/ui/vault/confirmation.ts +1 -4
  400. package/src/console/ui/vault/dormant.test.ts +1 -4
  401. package/src/console/ui/vault/export-safe.test.ts +1 -5
  402. package/src/console/ui/vault/export-safe.ts +1 -4
  403. package/src/console/ui/vault/fixture.ts +3 -3
  404. package/src/console/ui/vault/group.ts +1 -2
  405. package/src/console/ui/vault/index.ts +2 -10
  406. package/src/console/ui/vault/search.ts +2 -6
  407. package/src/console/ui/vault/types.ts +1 -1
  408. package/src/console/xss.gate.test.ts +1 -3
  409. package/src/docker/compose.ts +215 -27
  410. package/src/docker/derive.ts +6 -14
  411. package/src/docker/docker.test.ts +118 -15
  412. package/src/docker/dockerfile.integration.test.ts +12 -21
  413. package/src/docker/helpers.ts +5 -4
  414. package/src/docker/index.ts +9 -6
  415. package/src/docker/pin.ts +1 -9
  416. package/src/docker/recipes/index.ts +6 -6
  417. package/src/docker/recipes/mailpit.ts +22 -0
  418. package/src/docker/recipes/openbao.ts +47 -0
  419. package/src/docker/recipes/postgres.ts +17 -6
  420. package/src/docker/recipes/redis.ts +11 -2
  421. package/src/docker/recipes/rustfs.ts +32 -0
  422. package/src/docker/stack-id.test.ts +73 -5
  423. package/src/docker/stack-id.ts +124 -33
  424. package/src/docker/stack.integration.test.ts +6 -21
  425. package/src/docker/stack.ts +36 -4
  426. package/src/docker/types.ts +16 -4
  427. package/src/docs-origin.ts +4 -4
  428. package/src/drivers/ai-anthropic.ts +5 -14
  429. package/src/drivers/ai-mock.ts +2 -5
  430. package/src/drivers/ai-openai-compatible.ts +4 -10
  431. package/src/drivers/ai-providers.test.ts +6 -13
  432. package/src/drivers/bun-native-completeness.test.ts +6 -15
  433. package/src/drivers/channel-console.ts +6 -7
  434. package/src/drivers/channel-fcm.ts +20 -30
  435. package/src/drivers/channel-resend.ts +1 -3
  436. package/src/drivers/channel-smtp.ts +2 -6
  437. package/src/drivers/channel-types.ts +2 -12
  438. package/src/drivers/channel-unifonic.ts +2 -6
  439. package/src/drivers/channel-wa-cloud.ts +2 -6
  440. package/src/drivers/channel-webpush.ts +21 -60
  441. package/src/drivers/conformance.test.ts +2 -10
  442. package/src/drivers/conformance.ts +8 -21
  443. package/src/drivers/drizzle-dialect.test.ts +20 -0
  444. package/src/drivers/drizzle-dialect.ts +37 -0
  445. package/src/drivers/fs.ts +2 -8
  446. package/src/drivers/index.ts +12 -57
  447. package/src/drivers/memory.ts +287 -75
  448. package/src/drivers/pgvector.ts +7 -11
  449. package/src/drivers/postgres.ts +7 -13
  450. package/src/drivers/redis.ts +13 -37
  451. package/src/drivers/s3.ts +15 -18
  452. package/src/drivers/signal-engine.ts +12 -49
  453. package/src/drivers/signal-memory.ts +2 -8
  454. package/src/drivers/signal-nats.ts +4 -9
  455. package/src/drivers/signal-postgres.ts +62 -129
  456. package/src/drivers/signal-redis.ts +7 -21
  457. package/src/drivers/signal-types.ts +1 -4
  458. package/src/drivers/sqlite.ts +3 -12
  459. package/src/drivers/types.ts +4 -23
  460. package/src/drivers/vault-driver-removal.test.ts +55 -0
  461. package/src/drivers/vault-env.ts +2 -6
  462. package/src/drivers/vault-infisical.ts +1 -5
  463. package/src/drivers/vault-managed.ts +1 -5
  464. package/src/drivers/vault-memory.ts +1 -5
  465. package/src/drivers/vault-openbao.test.ts +97 -0
  466. package/src/drivers/vault-openbao.ts +103 -40
  467. package/src/drivers/vault-types.ts +3 -16
  468. package/src/elements/ai/declare.ts +4 -14
  469. package/src/elements/ai/eval.ts +2 -6
  470. package/src/elements/ai/pii.ts +1 -3
  471. package/src/elements/ai/runtime.ts +12 -50
  472. package/src/elements/ai/schema.ts +4 -15
  473. package/src/elements/ai.test.ts +13 -24
  474. package/src/elements/ai.ts +1 -5
  475. package/src/elements/channel/consent.ts +3 -12
  476. package/src/elements/channel/costs.ts +4 -15
  477. package/src/elements/channel/declare.ts +1 -4
  478. package/src/elements/channel/email-auth.ts +1 -4
  479. package/src/elements/channel/locale.ts +2 -10
  480. package/src/elements/channel/mask.ts +1 -3
  481. package/src/elements/channel/mime.ts +2 -12
  482. package/src/elements/channel/outcomes.test.ts +1 -5
  483. package/src/elements/channel/outcomes.ts +5 -15
  484. package/src/elements/channel/receipts.ts +2 -7
  485. package/src/elements/channel/runtime.ts +28 -74
  486. package/src/elements/channel/suppression.ts +8 -14
  487. package/src/elements/channel.test.ts +8 -31
  488. package/src/elements/channel.ts +3 -14
  489. package/src/elements/clock/actions.ts +6 -18
  490. package/src/elements/clock/declare.ts +1 -3
  491. package/src/elements/clock/dst.ts +1 -5
  492. package/src/elements/clock/durable.ts +4 -16
  493. package/src/elements/clock/health.ts +2 -7
  494. package/src/elements/clock/leader.ts +2 -9
  495. package/src/elements/clock/reconcile.ts +4 -15
  496. package/src/elements/clock/runtime.ts +2 -5
  497. package/src/elements/clock/schedule.ts +4 -20
  498. package/src/elements/clock.test.ts +2 -7
  499. package/src/elements/clock.ts +5 -23
  500. package/src/elements/gate/permissions.ts +1 -4
  501. package/src/elements/gate/runtime.ts +7 -28
  502. package/src/elements/gate.ts +1 -5
  503. package/src/elements/index.ts +9 -7
  504. package/src/elements/signal/declare.ts +2 -7
  505. package/src/elements/signal/reconcile.test.ts +1 -4
  506. package/src/elements/signal/runtime.ts +1 -3
  507. package/src/elements/signal.test.ts +3 -15
  508. package/src/elements/signal.ts +2 -8
  509. package/src/elements/store/cache.test.ts +46 -0
  510. package/src/elements/store/cache.ts +3 -11
  511. package/src/elements/store/classify.ts +5 -22
  512. package/src/elements/store/declare.ts +11 -17
  513. package/src/elements/store/domain-ddl.test.ts +86 -0
  514. package/src/elements/store/emit-drizzle.ts +363 -0
  515. package/src/elements/store/files-policy.test.ts +1 -5
  516. package/src/elements/store/files-policy.ts +4 -12
  517. package/src/elements/store/load-plugin-tables.ts +103 -0
  518. package/src/elements/store/missing-relation.test.ts +83 -0
  519. package/src/elements/store/missing-relation.ts +38 -0
  520. package/src/elements/store/replica.ts +1 -4
  521. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  522. package/src/elements/store/resource-list-docs.test.ts +79 -0
  523. package/src/elements/store/resource.test.ts +253 -0
  524. package/src/elements/store/resource.ts +786 -0
  525. package/src/elements/store/runtime.ts +25 -28
  526. package/src/elements/store/schema-decl.test.ts +227 -0
  527. package/src/elements/store/schema-decl.ts +516 -0
  528. package/src/elements/store/sql-condition.test.ts +132 -0
  529. package/src/elements/store/sql-condition.ts +293 -65
  530. package/src/elements/store/sql-session.test.ts +135 -0
  531. package/src/elements/store/sql-session.ts +241 -84
  532. package/src/elements/store/table.ts +69 -23
  533. package/src/elements/store.test.ts +17 -39
  534. package/src/elements/store.ts +53 -31
  535. package/src/elements/vault/chain.ts +34 -0
  536. package/src/elements/vault/declare.ts +22 -32
  537. package/src/elements/vault/fingerprint.ts +2 -7
  538. package/src/elements/vault/redact.ts +2 -6
  539. package/src/elements/vault/runtime.ts +19 -33
  540. package/src/elements/vault.test.ts +12 -55
  541. package/src/elements/vault.ts +9 -13
  542. package/src/index.ts +5 -2
  543. package/src/kernel/adopt-routes.ts +15 -29
  544. package/src/kernel/app.ts +92 -118
  545. package/src/kernel/auth-resolve.ts +2 -9
  546. package/src/kernel/boot-bind/ai.ts +1 -4
  547. package/src/kernel/boot-bind/channel.test.ts +60 -0
  548. package/src/kernel/boot-bind/channel.ts +61 -5
  549. package/src/kernel/boot-bind/clock.ts +2 -6
  550. package/src/kernel/boot-bind/gate.ts +2 -8
  551. package/src/kernel/boot-bind/runs.ts +1 -3
  552. package/src/kernel/boot-bind/signal.ts +3 -10
  553. package/src/kernel/boot-bind/store.test.ts +27 -18
  554. package/src/kernel/boot-bind/store.ts +93 -52
  555. package/src/kernel/boot-bind/vault.ts +2 -8
  556. package/src/kernel/boot.test.ts +4 -10
  557. package/src/kernel/boot.ts +30 -77
  558. package/src/kernel/budget.test.ts +1 -3
  559. package/src/kernel/capability.ts +3 -10
  560. package/src/kernel/dry-run.test.ts +1 -4
  561. package/src/kernel/dry-run.ts +3 -11
  562. package/src/kernel/edge.test.ts +68 -0
  563. package/src/kernel/effects.test.ts +1 -5
  564. package/src/kernel/effects.ts +4 -11
  565. package/src/kernel/errors.registry.test.ts +2 -8
  566. package/src/kernel/errors.ts +23 -32
  567. package/src/kernel/flow.test.ts +12 -25
  568. package/src/kernel/flow.ts +13 -25
  569. package/src/kernel/fx.test.ts +31 -40
  570. package/src/kernel/fx.ts +135 -90
  571. package/src/kernel/hook-timing.test.ts +3 -6
  572. package/src/kernel/hook-timing.ts +5 -14
  573. package/src/kernel/hooks.test.ts +43 -10
  574. package/src/kernel/hooks.ts +25 -12
  575. package/src/kernel/index.ts +11 -17
  576. package/src/kernel/journal.ts +7 -31
  577. package/src/kernel/on.ts +45 -9
  578. package/src/kernel/pipeline.test.ts +10 -19
  579. package/src/kernel/pipeline.ts +5 -17
  580. package/src/kernel/plug.ts +4 -11
  581. package/src/kernel/plugin/conflicts.test.ts +13 -25
  582. package/src/kernel/plugin/decorate.test.ts +8 -13
  583. package/src/kernel/plugin/scoping.test.ts +4 -11
  584. package/src/kernel/plugin.ts +81 -27
  585. package/src/kernel/registry-isolation.test.ts +74 -0
  586. package/src/kernel/registry.ts +102 -30
  587. package/src/kernel/router.test.ts +15 -13
  588. package/src/kernel/router.ts +9 -29
  589. package/src/kernel/routing-budget.test.ts +1 -3
  590. package/src/kernel/run-telemetry.ts +1 -3
  591. package/src/kernel/triggers.ts +63 -21
  592. package/src/manifest/diff.test.ts +26 -91
  593. package/src/manifest/diff.ts +95 -225
  594. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  595. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  596. package/src/manifest/index.ts +3 -10
  597. package/src/manifest/types.ts +30 -6
  598. package/src/manifest/undeclared.test.ts +4 -12
  599. package/src/manifest/undeclared.ts +1 -2
  600. package/src/manifest/validate.test.ts +2 -6
  601. package/src/manifest/validate.ts +5 -15
  602. package/src/mcp/authorization.ts +8 -27
  603. package/src/mcp/confirmation.ts +3 -10
  604. package/src/mcp/data.ts +2 -6
  605. package/src/mcp/docs-index.ts +4 -7
  606. package/src/mcp/docs-mcp.test.ts +3 -12
  607. package/src/mcp/docs-server.ts +7 -27
  608. package/src/mcp/docs-tools.ts +2 -6
  609. package/src/mcp/index.ts +1 -6
  610. package/src/mcp/injection.gate.test.ts +4 -18
  611. package/src/mcp/mcp.test.ts +18 -49
  612. package/src/mcp/protocol.ts +3 -10
  613. package/src/mcp/server.ts +11 -52
  614. package/src/mcp/session.ts +4 -14
  615. package/src/mcp/tools.ts +14 -50
  616. package/src/plugins/catalogue.test.ts +3 -7
  617. package/src/plugins/catalogue.ts +1 -3
  618. package/src/plugins/compression.test.ts +127 -0
  619. package/src/plugins/compression.ts +94 -0
  620. package/src/plugins/config-source.test.ts +204 -0
  621. package/src/plugins/config-source.ts +209 -0
  622. package/src/plugins/cors.test.ts +138 -0
  623. package/src/plugins/cors.ts +129 -0
  624. package/src/plugins/csrf.test.ts +102 -0
  625. package/src/plugins/csrf.ts +86 -0
  626. package/src/plugins/headers.ts +54 -0
  627. package/src/plugins/index.ts +26 -0
  628. package/src/plugins/ip-allowlist.test.ts +105 -0
  629. package/src/plugins/ip-allowlist.ts +76 -0
  630. package/src/plugins/maintenance-mode.test.ts +91 -0
  631. package/src/plugins/maintenance-mode.ts +85 -0
  632. package/src/plugins/node-import-scan.ts +2 -5
  633. package/src/plugins/security-headers.test.ts +243 -0
  634. package/src/plugins/security-headers.ts +255 -0
  635. package/src/plugins/supply-chain.ts +5 -17
  636. package/src/release/exports.ts +3 -8
  637. package/src/release/measure.exports.test.ts +1 -3
  638. package/src/release/measure.ts +16 -45
  639. package/src/runs/bench.test.ts +1 -3
  640. package/src/runs/collect.ts +8 -25
  641. package/src/runs/drivers/clickhouse.ts +2 -9
  642. package/src/runs/drivers/files.ts +9 -31
  643. package/src/runs/drivers/memory.ts +5 -22
  644. package/src/runs/drivers/postgres.ts +8 -20
  645. package/src/runs/duckdb.ts +2 -8
  646. package/src/runs/index.ts +3 -14
  647. package/src/runs/outlier.ts +3 -10
  648. package/src/runs/parquet.ts +8 -26
  649. package/src/runs/privacy.ts +2 -8
  650. package/src/runs/runs.test.ts +3 -9
  651. package/src/runs/runtime.ts +7 -32
  652. package/src/runs/shred.ts +10 -29
  653. package/src/runs/types.ts +1 -4
  654. package/src/runtime/bun.ts +4 -15
  655. package/src/runtime/cold-start.bench.ts +1 -3
  656. package/src/runtime/dev-request-log.test.ts +1 -4
  657. package/src/runtime/dev-request-log.ts +1 -4
  658. package/src/runtime/index.ts +1 -6
  659. package/src/runtime/primitives.ts +5 -4
  660. package/src/runtime/security.ts +3 -13
  661. package/src/runtime/serve.test.ts +4 -7
  662. package/src/runtime/types.ts +1 -4
  663. package/src/runtime/web-standard.ts +3 -15
  664. package/src/term.test.ts +4 -6
  665. package/src/term.ts +49 -98
  666. package/src/test/create-test-app.test.ts +4 -14
  667. package/src/test/create-test-app.ts +27 -33
  668. package/src/test/provisions.integration.test.ts +3 -11
  669. package/src/upgrade/codemods.test.ts +25 -6
  670. package/src/upgrade/codemods.ts +116 -3
  671. package/src/validation/standard-schema.test.ts +2 -5
  672. package/src/validation/standard-schema.ts +13 -33
  673. package/docs/spec/console.md +0 -739
  674. package/docs/spec/example.md +0 -1196
  675. package/docs/spec/four-applications.md +0 -1195
  676. package/docs/spec/unified-theory.md +0 -489
  677. package/src/cli/doc-drift.test.ts +0 -146
  678. package/src/cli/doc-drift.ts +0 -430
  679. package/src/cli/doctor-diff-examples.ts +0 -92
  680. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +0 -1
  681. package/src/drivers/vault-sops.ts +0 -271
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Official `cors` plugin — cross-origin rules at the edge: preflight
3
+ * `OPTIONS` answered even for paths bound to other methods, plus CORS
4
+ * headers on matched responses. Uses the public plugin API (§14) —
5
+ * including the edge contribution for requests that match no flow.
6
+ */
7
+
8
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
9
+ import {
10
+ pluginConfigSnapshot,
11
+ resolvePluginOptions,
12
+ withConfigTable,
13
+ isConfigSource,
14
+ type ConfigSource,
15
+ } from "./config-source.ts";
16
+ import { appendVary, withHeaders } from "./headers.ts";
17
+
18
+ /** Options for {@link cors}. */
19
+ export interface CorsOptions {
20
+ /**
21
+ * Origins allowed cross-origin: `"*"` for any, one origin string, or an
22
+ * exact-match list (`"https://app.example.com"`). Default **none** —
23
+ * cross-origin is closed until you open it. Same-origin traffic needs
24
+ * no CORS headers at all.
25
+ */
26
+ readonly origin?: "*" | string | readonly string[];
27
+ /**
28
+ * Methods answered on preflight. Default covers the verbs `http.*`
29
+ * triggers bind: GET · HEAD · POST · PUT · PATCH · DELETE · OPTIONS.
30
+ */
31
+ readonly methods?: readonly string[];
32
+ /**
33
+ * `Access-Control-Allow-Headers` on preflight. Default reflects the
34
+ * request's `Access-Control-Request-Headers` back to it.
35
+ */
36
+ readonly allowedHeaders?: readonly string[];
37
+ /** `Access-Control-Expose-Headers` on actual responses. Omitted unless provided. */
38
+ readonly exposedHeaders?: readonly string[];
39
+ /**
40
+ * Send `Access-Control-Allow-Credentials: true`. When set, an `origin`
41
+ * of `"*"` is answered by reflecting the request origin — browsers
42
+ * reject `"*"` together with credentials.
43
+ */
44
+ readonly credentials?: boolean;
45
+ /** `Access-Control-Max-Age` seconds on preflight. Omitted unless provided. */
46
+ readonly maxAge?: number;
47
+ }
48
+
49
+ const DEFAULT_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] as const;
50
+
51
+ /** True when `origin` is permitted by the configured origin rule. */
52
+ export function originAllowed(origin: string, rule: CorsOptions["origin"]): boolean {
53
+ if (rule === undefined) return false;
54
+ if (rule === "*") return true;
55
+ if (typeof rule === "string") return origin === rule;
56
+ return rule.includes(origin);
57
+ }
58
+
59
+ /** The `Access-Control-Allow-Origin` value for a permitted origin. */
60
+ function allowOriginValue(origin: string, options: CorsOptions): string {
61
+ return options.origin === "*" && options.credentials !== true ? "*" : origin;
62
+ }
63
+
64
+ /** Is this request a CORS preflight? */
65
+ function isPreflight(request: Request, method: string): boolean {
66
+ return (
67
+ method === "OPTIONS" &&
68
+ request.headers.get("origin") !== null &&
69
+ request.headers.get("access-control-request-method") !== null
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Enforce cross-origin rules. A preflight that matches no flow is answered
75
+ * by the plugin's edge handler; matched responses get their CORS headers
76
+ * at `onResponse`. A denied preflight returns `204` **without** CORS
77
+ * headers — the browser blocks it, which is the correct, quiet failure.
78
+ *
79
+ * Accepts static options or a {@link ConfigSource} for DB-driven origin
80
+ * lists (open an emergency integration origin without a redeploy).
81
+ *
82
+ * @param options - Origin rules and friends, or a config source
83
+ */
84
+ export function cors(options: CorsOptions | ConfigSource<CorsOptions> = {}): PluginDef {
85
+ const def = plugin("cors", { version: "0.0.1", config: pluginConfigSnapshot(options) })
86
+ .edge((request, info) => {
87
+ if (!isPreflight(request, info.method)) return undefined;
88
+ const resolved = resolvePluginOptions(options);
89
+ const origin = request.headers.get("origin")!;
90
+ if (!originAllowed(origin, resolved.origin)) {
91
+ return new Response(null, { status: 204 });
92
+ }
93
+
94
+ const headers = new Headers();
95
+ headers.set("access-control-allow-origin", allowOriginValue(origin, resolved));
96
+ headers.set("access-control-allow-methods", (resolved.methods ?? DEFAULT_METHODS).join(", "));
97
+ const allowed =
98
+ resolved.allowedHeaders?.join(", ") ??
99
+ request.headers.get("access-control-request-headers");
100
+ if (allowed !== null && allowed !== undefined && allowed.length > 0) {
101
+ headers.set("access-control-allow-headers", allowed);
102
+ }
103
+ if (resolved.credentials === true) headers.set("access-control-allow-credentials", "true");
104
+ if (resolved.maxAge !== undefined)
105
+ headers.set("access-control-max-age", String(resolved.maxAge));
106
+ appendVary(headers, "origin");
107
+ appendVary(headers, "access-control-request-method");
108
+ appendVary(headers, "access-control-request-headers");
109
+ return new Response(null, { status: 204, headers });
110
+ })
111
+ .hook("onResponse", (ctx) => {
112
+ if (!ctx.request || !ctx.response) return;
113
+ const origin = ctx.request.headers.get("origin");
114
+ if (origin === null) return;
115
+ const resolved = resolvePluginOptions(options);
116
+ if (!originAllowed(origin, resolved.origin)) return;
117
+
118
+ ctx.response = withHeaders(ctx.response, (headers) => {
119
+ headers.set("access-control-allow-origin", allowOriginValue(origin, resolved));
120
+ if (resolved.credentials === true) headers.set("access-control-allow-credentials", "true");
121
+ if (resolved.exposedHeaders !== undefined && resolved.exposedHeaders.length > 0) {
122
+ headers.set("access-control-expose-headers", resolved.exposedHeaders.join(", "));
123
+ }
124
+ appendVary(headers, "origin");
125
+ });
126
+ });
127
+
128
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
129
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `csrf` plugin — fetch-metadata defense with an Origin fallback, through
3
+ * the real pipeline.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, test } from "bun:test";
7
+ import { oke } from "../kernel/app.ts";
8
+ import { flow, resetFlowSeq } from "../kernel/flow.ts";
9
+ import { on, resetBindings } from "../kernel/on.ts";
10
+ import { http } from "../kernel/triggers.ts";
11
+ import { csrf } from "./csrf.ts";
12
+
13
+ beforeEach(() => {
14
+ resetBindings();
15
+ resetFlowSeq();
16
+ });
17
+
18
+ function mutating(headers: Record<string, string> = {}): Request {
19
+ return new Request("http://localhost/transfer", { method: "POST", headers });
20
+ }
21
+
22
+ function appWith(options: Parameters<typeof csrf>[0]) {
23
+ on(http.post("/transfer"), flow({ name: "transfer.create", do: () => ({ moved: true }) }));
24
+ on(http.get("/balance"), flow({ name: "balance.get", do: () => ({ balance: 5 }) }));
25
+ return oke({ name: `csrf-${crypto.randomUUID()}` }).plug(csrf(options));
26
+ }
27
+
28
+ describe("csrf plugin", () => {
29
+ test("safe methods always pass", async () => {
30
+ const app = appWith({});
31
+ const res = await app.fetch(
32
+ new Request("http://localhost/balance", {
33
+ headers: { "sec-fetch-site": "cross-site", origin: "https://evil.com" },
34
+ }),
35
+ );
36
+ expect(res.status).toBe(200);
37
+ });
38
+
39
+ test("same-origin and none pass; cross-site is denied with typed Forbidden", async () => {
40
+ const app = appWith({});
41
+
42
+ const same = await app.fetch(mutating({ "sec-fetch-site": "same-origin" }));
43
+ expect(same.status).toBe(200);
44
+
45
+ const cross = await app.fetch(
46
+ mutating({ "sec-fetch-site": "cross-site", origin: "https://evil.com" }),
47
+ );
48
+ expect(cross.status).toBe(403);
49
+ const body = (await cross.json()) as { error: { code: string; data: { reason: string } } };
50
+ expect(body.error.code).toBe("Forbidden");
51
+ expect(body.error.data.reason).toBe("csrf");
52
+ });
53
+
54
+ test("same-site passes by default, blocked when allowSameSite: false", async () => {
55
+ const lax = appWith({});
56
+ expect(
57
+ (
58
+ await lax.fetch(
59
+ mutating({ "sec-fetch-site": "same-site", origin: "https://sub.localhost" }),
60
+ )
61
+ ).status,
62
+ ).toBe(200);
63
+
64
+ const strict = appWith({ allowSameSite: false });
65
+ const res = await strict.fetch(
66
+ mutating({ "sec-fetch-site": "same-site", origin: "https://sub.localhost" }),
67
+ );
68
+ expect(res.status).toBe(403);
69
+ });
70
+
71
+ test("Origin fallback: same-origin host passes, allow-listed origins pass, others fail", async () => {
72
+ const app = appWith({ allowOrigins: ["https://admin.example.com"] });
73
+
74
+ const sameHost = await app.fetch(mutating({ origin: "http://localhost" }));
75
+ expect(sameHost.status).toBe(200);
76
+
77
+ const listed = await app.fetch(mutating({ origin: "https://admin.example.com" }));
78
+ expect(listed.status).toBe(200);
79
+
80
+ const evil = await app.fetch(mutating({ origin: "https://evil.com" }));
81
+ expect(evil.status).toBe(403);
82
+ });
83
+
84
+ test("headerless mutating requests pass by default (curl, webhooks), fail closed on demand", async () => {
85
+ const lax = appWith({});
86
+ expect((await lax.fetch(mutating())).status).toBe(200);
87
+
88
+ const closed = appWith({ allowNoHeader: false });
89
+ const res = await closed.fetch(mutating());
90
+ expect(res.status).toBe(403);
91
+ const body = (await res.json()) as { error: { code: string } };
92
+ expect(body.error.code).toBe("Forbidden");
93
+ });
94
+
95
+ test("cross-site with an allow-listed origin passes (metadata wins are not required)", async () => {
96
+ const app = appWith({ allowOrigins: ["https://admin.example.com"] });
97
+ const res = await app.fetch(
98
+ mutating({ "sec-fetch-site": "cross-site", origin: "https://admin.example.com" }),
99
+ );
100
+ expect(res.status).toBe(200);
101
+ });
102
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Official `csrf` plugin — cross-site request forgery defense using fetch
3
+ * metadata (`Sec-Fetch-Site`) with an `Origin` fallback. Stateless: no
4
+ * tokens, no cookies, no session reads. Uses only the public plugin API.
5
+ */
6
+
7
+ import { fail } from "../kernel/errors.ts";
8
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
9
+ import {
10
+ pluginConfigSnapshot,
11
+ resolvePluginOptions,
12
+ withConfigTable,
13
+ isConfigSource,
14
+ type ConfigSource,
15
+ } from "./config-source.ts";
16
+
17
+ /** Options for {@link csrf}. */
18
+ export interface CsrfOptions {
19
+ /**
20
+ * Absolute origins allowed to send mutating requests from another site
21
+ * (e.g. `"https://admin.example.com"` for a separately-hosted console).
22
+ * Same-origin is always allowed.
23
+ */
24
+ readonly allowOrigins?: readonly string[];
25
+ /**
26
+ * Allow `Sec-Fetch-Site: same-site` (subdomains of your site). Default
27
+ * `true`. Set `false` when subdomains host untrusted content.
28
+ */
29
+ readonly allowSameSite?: boolean;
30
+ /**
31
+ * Allow mutating requests carrying neither `Sec-Fetch-Site` nor `Origin`
32
+ * — the shape of curl, server-to-server calls, and webhooks, which are
33
+ * not browser CSRF vectors. Default `true`. Set `false` to fail closed
34
+ * when your auth is purely cookie-based.
35
+ */
36
+ readonly allowNoHeader?: boolean;
37
+ }
38
+
39
+ /** Methods browsers can mutate state with (safe verbs pass untouched). */
40
+ function isMutating(method: string): boolean {
41
+ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
42
+ }
43
+
44
+ /**
45
+ * Block cross-site state changes at `onAuth`, before gate policies and the
46
+ * flow body. Decision order: safe methods pass → `Sec-Fetch-Site`
47
+ * (`same-origin` / `none` pass; `same-site` per option) → `Origin`
48
+ * fallback (must be same-origin or allow-listed) → headerless clients per
49
+ * `allowNoHeader`. Denials are the gate element's typed `Forbidden`.
50
+ *
51
+ * Accepts static options or a {@link ConfigSource} for DB-driven origin
52
+ * rules.
53
+ *
54
+ * @param options - Origin rules, or a config source
55
+ */
56
+ export function csrf(options: CsrfOptions | ConfigSource<CsrfOptions> = {}): PluginDef {
57
+ const def = plugin("csrf", { version: "0.0.1", config: pluginConfigSnapshot(options) }).hook(
58
+ "onAuth",
59
+ (ctx) => {
60
+ if (!ctx.request) return;
61
+ const method = ctx.request.method.toUpperCase();
62
+ if (!isMutating(method)) return;
63
+
64
+ const resolved = resolvePluginOptions(options);
65
+ const site = ctx.request.headers.get("sec-fetch-site")?.toLowerCase();
66
+ if (site === "same-origin" || site === "none") return;
67
+ if (site === "same-site" && (resolved.allowSameSite ?? true)) return;
68
+
69
+ const origin = ctx.request.headers.get("origin");
70
+ if (origin !== null) {
71
+ const host = new URL(ctx.request.url).origin;
72
+ if (origin === host || resolved.allowOrigins?.includes(origin) === true) return;
73
+ return fail("Forbidden", {
74
+ reason: "csrf",
75
+ site: site ?? "unknown",
76
+ origin,
77
+ });
78
+ }
79
+
80
+ if (resolved.allowNoHeader ?? true) return;
81
+ return fail("Forbidden", { reason: "csrf", site: site ?? "unknown" });
82
+ },
83
+ );
84
+
85
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
86
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared response-header mutation for HTTP middleware plugins.
3
+ * `Headers` are immutable-in-place on a built `Response` — rebuild instead.
4
+ */
5
+
6
+ /**
7
+ * Rebuild a response with headers mutated by `fn`.
8
+ *
9
+ * @param response - Original response
10
+ * @param fn - Header mutation
11
+ */
12
+ export function withHeaders(response: Response, fn: (headers: Headers) => void): Response {
13
+ const headers = new Headers(response.headers);
14
+ fn(headers);
15
+ return new Response(response.body, {
16
+ status: response.status,
17
+ statusText: response.statusText,
18
+ headers,
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Set a header only when absent (or when `override` is on).
24
+ *
25
+ * @param headers - Mutable headers
26
+ * @param name - Header name
27
+ * @param value - Header value
28
+ * @param override - Replace an app-set value
29
+ */
30
+ export function setUnlessPresent(
31
+ headers: Headers,
32
+ name: string,
33
+ value: string,
34
+ override: boolean,
35
+ ): void {
36
+ if (!override && headers.has(name)) return;
37
+ headers.set(name, value);
38
+ }
39
+
40
+ /**
41
+ * Append a token to `Vary` without duplicating it.
42
+ *
43
+ * @param headers - Mutable headers
44
+ * @param token - Vary token (e.g. `"origin"`)
45
+ */
46
+ export function appendVary(headers: Headers, token: string): void {
47
+ const vary = headers.get("vary");
48
+ if (vary === null) {
49
+ headers.set("vary", token);
50
+ return;
51
+ }
52
+ const pattern = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
53
+ if (!pattern.test(vary)) headers.set("vary", `${vary}, ${token}`);
54
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Official plugins — first-party extensions built entirely on the public
3
+ * plugin API (`okengine/plugins`). Import what you need; attach with
4
+ * `.plug()`.
5
+ */
6
+
7
+ export { compression, type CompressionOptions } from "./compression.ts";
8
+ export {
9
+ configSource,
10
+ isConfigSource,
11
+ resolvePluginOptions,
12
+ type ConfigSource,
13
+ type ConfigSourceDb,
14
+ type ConfigSourceOptions,
15
+ } from "./config-source.ts";
16
+ export { cors, type CorsOptions } from "./cors.ts";
17
+ export { csrf, type CsrfOptions } from "./csrf.ts";
18
+ export { ipAllowlist, type IpAllowlistOptions } from "./ip-allowlist.ts";
19
+ export { maintenanceMode, type MaintenanceModeOptions } from "./maintenance-mode.ts";
20
+ export {
21
+ defaultCspDirectives,
22
+ securityHeaders,
23
+ type CspOptions,
24
+ type HstsOptions,
25
+ type SecurityHeadersOptions,
26
+ } from "./security-headers.ts";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `ip-allowlist` plugin — allow/deny semantics, XFF parsing, and
3
+ * missing-header behavior through the real pipeline.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, test } from "bun:test";
7
+ import { oke } from "../kernel/app.ts";
8
+ import { flow, resetFlowSeq } from "../kernel/flow.ts";
9
+ import { on, resetBindings } from "../kernel/on.ts";
10
+ import { http } from "../kernel/triggers.ts";
11
+ import { ipAllowlist } from "./ip-allowlist.ts";
12
+
13
+ beforeEach(() => {
14
+ resetBindings();
15
+ resetFlowSeq();
16
+ });
17
+
18
+ /** Shorthand: GET /x with an optional x-forwarded-for header. */
19
+ function get(ip?: string): Request {
20
+ return new Request(
21
+ "http://localhost/x",
22
+ ip === undefined ? undefined : { headers: { "x-forwarded-for": ip } },
23
+ );
24
+ }
25
+
26
+ describe("ipAllowlist plugin", () => {
27
+ test("allow list: listed IPs pass, others get 403 Forbidden", async () => {
28
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
29
+ const app = oke({ name: "ips" }).plug(ipAllowlist({ allow: ["203.0.113.7"] }));
30
+
31
+ const ok = await app.fetch(get("203.0.113.7"));
32
+ expect(ok.status).toBe(200);
33
+
34
+ const denied = await app.fetch(get("198.51.100.9"));
35
+ expect(denied.status).toBe(403);
36
+ const body = (await denied.json()) as { error: { code: string; data: { reason: string } } };
37
+ expect(body.error.code).toBe("Forbidden");
38
+ expect(body.error.data.reason).toBe("ip_not_allowed");
39
+ });
40
+
41
+ test("deny list: blocked IPs get 403 ip_denied, others pass", async () => {
42
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
43
+ const app = oke({ name: "ips-deny" }).plug(ipAllowlist({ deny: ["198.51.100.9"] }));
44
+
45
+ const blocked = await app.fetch(get("198.51.100.9"));
46
+ expect(blocked.status).toBe(403);
47
+ const body = (await blocked.json()) as { error: { data: { reason: string } } };
48
+ expect(body.error.data.reason).toBe("ip_denied");
49
+
50
+ const ok = await app.fetch(get("203.0.113.7"));
51
+ expect(ok.status).toBe(200);
52
+ });
53
+
54
+ test("deny wins over allow on overlap", async () => {
55
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
56
+ const app = oke({ name: "ips-both" }).plug(
57
+ ipAllowlist({ allow: ["203.0.113.7"], deny: ["203.0.113.7"] }),
58
+ );
59
+
60
+ const res = await app.fetch(get("203.0.113.7"));
61
+ expect(res.status).toBe(403);
62
+ });
63
+
64
+ test("XFF first hop is the client; later hops are ignored", async () => {
65
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
66
+ const app = oke({ name: "ips-xff" }).plug(ipAllowlist({ allow: ["203.0.113.7"] }));
67
+
68
+ const proxied = await app.fetch(get("203.0.113.7, 10.0.0.1, 10.0.0.2"));
69
+ expect(proxied.status).toBe(200);
70
+
71
+ const spoofedTail = await app.fetch(get("198.51.100.9, 203.0.113.7"));
72
+ expect(spoofedTail.status).toBe(403);
73
+ });
74
+
75
+ test("missing header: denied when allow is set, permitted for deny-only", async () => {
76
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
77
+ const strict = oke({ name: "ips-missing-allow" }).plug(ipAllowlist({ allow: ["203.0.113.7"] }));
78
+
79
+ const denied = await strict.fetch(get());
80
+ expect(denied.status).toBe(403);
81
+
82
+ resetBindings();
83
+ resetFlowSeq();
84
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
85
+
86
+ const lax = oke({ name: "ips-missing-deny" }).plug(ipAllowlist({ deny: ["198.51.100.9"] }));
87
+ const ok = await lax.fetch(get());
88
+ expect(ok.status).toBe(200);
89
+ });
90
+
91
+ test("custom header name is honored", async () => {
92
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
93
+ const app = oke({ name: "ips-custom" }).plug(
94
+ ipAllowlist({ allow: ["203.0.113.7"], header: "x-real-ip" }),
95
+ );
96
+
97
+ const res = await app.fetch(
98
+ new Request("http://localhost/x", { headers: { "x-real-ip": "203.0.113.7" } }),
99
+ );
100
+ expect(res.status).toBe(200);
101
+
102
+ const wrongHeader = await app.fetch(get("203.0.113.7"));
103
+ expect(wrongHeader.status).toBe(403);
104
+ });
105
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Official `ip-allowlist` plugin — client-IP allow/deny rules at the edge
3
+ * of the pipeline. Uses only the public plugin API (unified-theory §14).
4
+ */
5
+
6
+ import { fail } from "../kernel/errors.ts";
7
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
8
+ import {
9
+ isConfigSource,
10
+ pluginConfigSnapshot,
11
+ resolvePluginOptions,
12
+ withConfigTable,
13
+ type ConfigSource,
14
+ } from "./config-source.ts";
15
+
16
+ /** Options for {@link ipAllowlist}. */
17
+ export interface IpAllowlistOptions {
18
+ /**
19
+ * Client IPs permitted to call any flow. When set, every other client
20
+ * gets `Forbidden`. Exact IPv4/IPv6 strings.
21
+ */
22
+ readonly allow?: readonly string[];
23
+ /**
24
+ * Client IPs blocked from calling any flow. Checked before `allow`, so
25
+ * a deny always wins on overlap.
26
+ */
27
+ readonly deny?: readonly string[];
28
+ /**
29
+ * Header carrying the client IP. Default `"x-forwarded-for"` (first hop
30
+ * wins, the value your proxy appends). Only trustworthy behind a proxy
31
+ * that sets or overwrites this header — a direct client can lie.
32
+ */
33
+ readonly header?: string;
34
+ }
35
+
36
+ /** Extract the client IP from the configured header (first hop for XFF). */
37
+ function clientIp(request: Request, header: string): string | undefined {
38
+ const raw = request.headers.get(header);
39
+ if (raw === null) return undefined;
40
+ const first = raw.split(",")[0]?.trim();
41
+ return first === undefined || first.length === 0 ? undefined : first;
42
+ }
43
+
44
+ /**
45
+ * Enforce IP allow/deny rules at `onAuth`, before any gate policy or flow
46
+ * body runs. Denied clients get the same typed `Forbidden` denial the gate
47
+ * element produces. A missing/empty IP header is denied when `allow` is
48
+ * set, permitted otherwise. Non-HTTP triggers no-op.
49
+ *
50
+ * Accepts static options or a {@link ConfigSource} — block an abusive IP
51
+ * from the database and have every instance pick it up on the next sync.
52
+ *
53
+ * @param options - Allow/deny lists and header name, or a config source
54
+ */
55
+ export function ipAllowlist(
56
+ options: IpAllowlistOptions | ConfigSource<IpAllowlistOptions>,
57
+ ): PluginDef {
58
+ const def = plugin("ip-allowlist", {
59
+ version: "0.0.2",
60
+ config: pluginConfigSnapshot(options),
61
+ }).hook("onAuth", (ctx) => {
62
+ if (!ctx.request) return;
63
+ const resolved = resolvePluginOptions(options);
64
+ const header = (resolved.header ?? "x-forwarded-for").toLowerCase();
65
+ const ip = clientIp(ctx.request, header);
66
+
67
+ if (ip !== undefined && (resolved.deny ?? []).includes(ip)) {
68
+ return fail("Forbidden", { reason: "ip_denied", ip });
69
+ }
70
+ if (resolved.allow !== undefined && (ip === undefined || !resolved.allow.includes(ip))) {
71
+ return fail("Forbidden", { reason: "ip_not_allowed", ip });
72
+ }
73
+ });
74
+
75
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
76
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `maintenance-mode` plugin — 503 drain behavior, allow-paths, bypass
3
+ * header, and non-HTTP no-op through the real pipeline.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, test } from "bun:test";
7
+ import { oke } from "../kernel/app.ts";
8
+ import { flow, resetFlowSeq } from "../kernel/flow.ts";
9
+ import { on, resetBindings } from "../kernel/on.ts";
10
+ import { http } from "../kernel/triggers.ts";
11
+ import { maintenanceMode } from "./maintenance-mode.ts";
12
+
13
+ beforeEach(() => {
14
+ resetBindings();
15
+ resetFlowSeq();
16
+ });
17
+
18
+ describe("maintenanceMode plugin", () => {
19
+ test("returns 503 with ServiceUnavailable envelope and Retry-After", async () => {
20
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
21
+ const app = oke({ name: "maint" }).plug(maintenanceMode({ retryAfter: 120 }));
22
+
23
+ const res = await app.fetch(new Request("http://localhost/x"));
24
+
25
+ expect(res.status).toBe(503);
26
+ expect(res.headers.get("retry-after")).toBe("120");
27
+ const body = (await res.json()) as {
28
+ data: null;
29
+ error: { code: string; data: { retryAfter?: number }; message?: string };
30
+ };
31
+ expect(body.data).toBeNull();
32
+ expect(body.error.code).toBe("ServiceUnavailable");
33
+ expect(body.error.data.retryAfter).toBe(120);
34
+ expect(body.error.message).toBe("Service is under maintenance.");
35
+ });
36
+
37
+ test("enabled: false passes traffic through", async () => {
38
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
39
+ const app = oke({ name: "maint-off" }).plug(maintenanceMode({ enabled: false }));
40
+
41
+ const res = await app.fetch(new Request("http://localhost/x"));
42
+
43
+ expect(res.status).toBe(200);
44
+ });
45
+
46
+ test("allowPaths prefixes keep serving", async () => {
47
+ on(http.get("/health"), flow({ name: "health.get", do: () => ({ up: true }) }));
48
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
49
+ const app = oke({ name: "maint-allow" }).plug(maintenanceMode({ allowPaths: ["/health"] }));
50
+
51
+ const health = await app.fetch(new Request("http://localhost/health"));
52
+ expect(health.status).toBe(200);
53
+
54
+ const x = await app.fetch(new Request("http://localhost/x"));
55
+ expect(x.status).toBe(503);
56
+ });
57
+
58
+ test("bypass header lets operators through (any non-empty value)", async () => {
59
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
60
+ const app = oke({ name: "maint-bypass" }).plug(
61
+ maintenanceMode({ bypassHeader: "x-ops-token" }),
62
+ );
63
+
64
+ const blocked = await app.fetch(new Request("http://localhost/x"));
65
+ expect(blocked.status).toBe(503);
66
+
67
+ const allowed = await app.fetch(
68
+ new Request("http://localhost/x", { headers: { "x-ops-token": "let-me-in" } }),
69
+ );
70
+ expect(allowed.status).toBe(200);
71
+ });
72
+
73
+ test("the 503 still flows through onResponse (other plugins stamp it)", async () => {
74
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
75
+ const app = oke({ name: "maint-stamp" }).plug(maintenanceMode());
76
+ app.hook("onResponse", (ctx) => {
77
+ if (!ctx.response) return;
78
+ const headers = new Headers(ctx.response.headers);
79
+ headers.set("x-stamped", "yes");
80
+ ctx.response = new Response(ctx.response.body, {
81
+ status: ctx.response.status,
82
+ headers,
83
+ });
84
+ });
85
+
86
+ const res = await app.fetch(new Request("http://localhost/x"));
87
+
88
+ expect(res.status).toBe(503);
89
+ expect(res.headers.get("x-stamped")).toBe("yes");
90
+ });
91
+ });