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.
- package/AGENTS.md +33 -30
- package/README.md +74 -340
- package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +40 -9
- package/package.json +47 -44
- package/site/content/docs/ai/llms-txt.mdx +54 -0
- package/site/content/docs/ai/mcp.mdx +123 -0
- package/site/content/docs/ai/meta.json +5 -0
- package/site/content/docs/ai/skills.mdx +53 -0
- package/site/content/docs/console/access.mdx +29 -0
- package/site/content/docs/console/ai.mdx +35 -0
- package/site/content/docs/console/architecture.mdx +35 -0
- package/site/content/docs/console/channels.mdx +37 -0
- package/site/content/docs/console/clock.mdx +31 -0
- package/site/content/docs/console/flows.mdx +31 -0
- package/site/content/docs/console/gates.mdx +35 -0
- package/site/content/docs/console/manifest-diff.mdx +34 -0
- package/site/content/docs/console/meta.json +23 -0
- package/site/content/docs/console/overview.mdx +40 -0
- package/site/content/docs/console/plugins.mdx +41 -0
- package/site/content/docs/console/privacy.mdx +32 -0
- package/site/content/docs/console/runs.mdx +40 -0
- package/site/content/docs/console/signals.mdx +31 -0
- package/site/content/docs/console/store.mdx +32 -0
- package/site/content/docs/console/tenancy.mdx +32 -0
- package/site/content/docs/console/traces.mdx +34 -0
- package/site/content/docs/console/vault.mdx +37 -0
- package/site/content/docs/elements/ai.mdx +180 -0
- package/site/content/docs/elements/channel.mdx +167 -0
- package/site/content/docs/elements/clock.mdx +182 -0
- package/site/content/docs/elements/flow.mdx +288 -0
- package/site/content/docs/elements/gate.mdx +171 -0
- package/site/content/docs/elements/meta.json +5 -0
- package/site/content/docs/elements/signal.mdx +171 -0
- package/site/content/docs/elements/store.mdx +320 -0
- package/site/content/docs/elements/vault.mdx +263 -0
- package/site/content/docs/get-started/basic-usage.mdx +124 -0
- package/site/content/docs/get-started/comparison.mdx +65 -0
- package/site/content/docs/get-started/installation.mdx +113 -0
- package/site/content/docs/get-started/introduction.mdx +123 -0
- package/site/content/docs/get-started/meta.json +5 -0
- package/site/content/docs/index.mdx +63 -0
- package/site/content/docs/meta.json +5 -0
- package/site/content/docs/plugins/compression.mdx +60 -0
- package/site/content/docs/plugins/cors.mdx +92 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
- package/site/content/docs/plugins/meta.json +15 -0
- package/site/content/docs/plugins/security-headers.mdx +136 -0
- package/site/content/docs/reference/cli.md +101 -0
- package/site/content/docs/reference/configuration.mdx +159 -0
- package/site/content/docs/reference/environment-variables.mdx +87 -0
- package/site/content/docs/reference/errors.mdx +80 -0
- package/site/content/docs/reference/fx.mdx +117 -0
- package/site/content/docs/reference/meta.json +5 -0
- package/site/content/docs/reference/plugins.mdx +249 -0
- package/site/content/docs/reference/security.md +63 -0
- package/src/auth/api-keys.ts +2 -5
- package/src/auth/attenuation.ts +2 -6
- package/src/auth/auth.test.ts +56 -31
- package/src/auth/invoke-as.ts +1 -3
- package/src/auth/operator.ts +8 -26
- package/src/auth/planes.ts +2 -7
- package/src/auth/plugin.ts +4 -7
- package/src/auth/roles.ts +1 -5
- package/src/auth/sessions.ts +10 -39
- package/src/cli/args.ts +1 -5
- package/src/cli/ask-dev-mode.ts +40 -0
- package/src/cli/build.ts +1 -2
- package/src/cli/client-add.test.ts +8 -15
- package/src/cli/client-add.ts +5 -14
- package/src/cli/completion.test.ts +1 -5
- package/src/cli/completion.ts +4 -17
- package/src/cli/db-auto-push.test.ts +64 -0
- package/src/cli/db-auto-push.ts +72 -0
- package/src/cli/db.test.ts +189 -0
- package/src/cli/db.ts +456 -0
- package/src/cli/dev-app-runner.ts +2 -8
- package/src/cli/dev-db-push.test.ts +193 -0
- package/src/cli/dev-mode.test.ts +45 -0
- package/src/cli/dev-mode.ts +78 -0
- package/src/cli/dev-schema-sync.test.ts +66 -0
- package/src/cli/dev-schema-sync.ts +139 -0
- package/src/cli/dev.test.ts +264 -15
- package/src/cli/dev.ts +355 -139
- package/src/cli/doc-staleness.test.ts +69 -0
- package/src/cli/docker-cli.test.ts +27 -8
- package/src/cli/docker.ts +11 -3
- package/src/cli/doctor-diff.test.ts +1 -5
- package/src/cli/doctor-diff.ts +17 -46
- package/src/cli/doctor-pii.ts +2 -7
- package/src/cli/doctor.test.ts +26 -4
- package/src/cli/doctor.ts +35 -11
- package/src/cli/drizzle-env.test.ts +67 -0
- package/src/cli/drizzle-env.ts +78 -0
- package/src/cli/ensure-drizzle-config.ts +50 -0
- package/src/cli/eval.ts +4 -14
- package/src/cli/gates-list.ts +1 -4
- package/src/cli/hero-meta.test.ts +17 -19
- package/src/cli/hero-meta.ts +20 -30
- package/src/cli/images.ts +3 -8
- package/src/cli/index.ts +11 -2
- package/src/cli/load-config.images.test.ts +14 -14
- package/src/cli/load-config.ts +47 -47
- package/src/cli/mcp-from-console.ts +1 -4
- package/src/cli/mode.ts +65 -0
- package/src/cli/openbao-bootstrap.test.ts +147 -0
- package/src/cli/openbao-bootstrap.ts +280 -0
- package/src/cli/openbao-restart.integration.test.ts +136 -0
- package/src/cli/ports.test.ts +7 -14
- package/src/cli/ports.ts +7 -5
- package/src/cli/privacy-erase.ts +1 -3
- package/src/cli/registry.ts +75 -25
- package/src/cli/resolve-dev-sql-env.test.ts +48 -0
- package/src/cli/resolve-dev-sql-env.ts +42 -0
- package/src/cli/schema.ts +3 -10
- package/src/cli/stack.ts +8 -10
- package/src/cli/start.ts +4 -15
- package/src/cli/upgrade.ts +1 -4
- package/src/cli/vault-cmd.ts +63 -0
- package/src/client/budget-entry.ts +1 -6
- package/src/client/create.test.ts +6 -12
- package/src/client/create.ts +2 -8
- package/src/client/errors.ts +1 -4
- package/src/client/index.ts +1 -6
- package/src/client/notes-contract.test.ts +4 -13
- package/src/client/transport.ts +9 -30
- package/src/client/types.ts +15 -30
- package/src/compiler/aot.test.ts +2 -9
- package/src/compiler/aot.ts +1 -4
- package/src/compiler/differential.test.ts +3 -11
- package/src/compiler/dynamic.ts +1 -4
- package/src/compiler/effects-infer.ts +8 -34
- package/src/compiler/emit.ts +1 -3
- package/src/compiler/extract.test.ts +136 -30
- package/src/compiler/extract.ts +485 -157
- package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -3
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -3
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +4 -1
- package/src/compiler/fixtures/skyport.expected.json +24 -83
- package/src/compiler/fixtures/triggers/five-triggers.ts +1 -2
- package/src/compiler/http-parse.ts +2 -9
- package/src/compiler/index.ts +1 -5
- package/src/compiler/response.ts +16 -7
- package/src/compiler/sucrose.ts +11 -31
- package/src/config/define-config.test.ts +55 -34
- package/src/config/index.ts +177 -46
- package/src/config/resolve-driver.test.ts +28 -17
- package/src/console/budget.test.ts +1 -4
- package/src/console/index.ts +1 -4
- package/src/console/server/access.test.ts +15 -43
- package/src/console/server/access.ts +19 -49
- package/src/console/server/ai.test.ts +3 -11
- package/src/console/server/ai.ts +23 -46
- package/src/console/server/app.ts +16 -45
- package/src/console/server/auth-rate.test.ts +4 -12
- package/src/console/server/bind.ts +1 -5
- package/src/console/server/channels.test.ts +2 -6
- package/src/console/server/channels.ts +10 -35
- package/src/console/server/claim.ts +2 -8
- package/src/console/server/clock.test.ts +2 -6
- package/src/console/server/clock.ts +8 -26
- package/src/console/server/console.test.ts +1 -3
- package/src/console/server/diff.test.ts +5 -17
- package/src/console/server/diff.ts +10 -31
- package/src/console/server/dry-run.audit.test.ts +2 -8
- package/src/console/server/flows.ts +64 -233
- package/src/console/server/gates.test.ts +14 -50
- package/src/console/server/gates.ts +13 -48
- package/src/console/server/index.ts +2 -9
- package/src/console/server/live.ts +3 -12
- package/src/console/server/operator-db.test.ts +4 -15
- package/src/console/server/operator-db.ts +13 -32
- package/src/console/server/plugin.ts +50 -53
- package/src/console/server/plugins.test.ts +2 -6
- package/src/console/server/plugins.ts +17 -56
- package/src/console/server/runs-pii.ts +2 -7
- package/src/console/server/security.gate.test.ts +18 -40
- package/src/console/server/serve.ts +10 -25
- package/src/console/server/signals.test.ts +1 -3
- package/src/console/server/signals.ts +17 -40
- package/src/console/server/state.ts +21 -59
- package/src/console/server/store.test.ts +20 -22
- package/src/console/server/store.ts +31 -76
- package/src/console/server/vault.test.ts +12 -29
- package/src/console/server/vault.ts +44 -66
- package/src/console/ui/access/AccessA11yView.tsx +6 -21
- package/src/console/ui/access/a11y.test.tsx +3 -12
- package/src/console/ui/access/acknowledgement.test.ts +1 -4
- package/src/console/ui/access/acknowledgement.ts +1 -3
- package/src/console/ui/access/blast-radius.ts +1 -3
- package/src/console/ui/access/confirmation.test.ts +1 -5
- package/src/console/ui/access/confirmation.ts +1 -4
- package/src/console/ui/access/fixture.ts +1 -5
- package/src/console/ui/access/grantable.test.ts +1 -3
- package/src/console/ui/access/grantable.ts +2 -7
- package/src/console/ui/access/index.ts +2 -9
- package/src/console/ui/access/provenance.test.ts +1 -3
- package/src/console/ui/access/provenance.ts +2 -6
- package/src/console/ui/access/search.test.ts +1 -5
- package/src/console/ui/access/search.ts +2 -6
- package/src/console/ui/ai/AiA11yView.tsx +6 -17
- package/src/console/ui/ai/a11y.test.tsx +2 -5
- package/src/console/ui/ai/fixture.ts +2 -7
- package/src/console/ui/ai/format.ts +1 -3
- package/src/console/ui/ai/group.test.ts +1 -3
- package/src/console/ui/ai/group.ts +4 -12
- package/src/console/ui/ai/index.ts +2 -9
- package/src/console/ui/ai/promotion.test.ts +2 -7
- package/src/console/ui/ai/promotion.ts +4 -13
- package/src/console/ui/ai/search.test.ts +2 -11
- package/src/console/ui/ai/search.ts +3 -13
- package/src/console/ui/ai/types.ts +1 -8
- package/src/console/ui/architecture/ArchitectureA11yView.tsx +8 -29
- package/src/console/ui/architecture/a11y.test.tsx +1 -2
- package/src/console/ui/architecture/declared.ts +1 -5
- package/src/console/ui/architecture/index.ts +7 -35
- package/src/console/ui/architecture/layout.ts +1 -3
- package/src/console/ui/architecture/pathologies.ts +10 -19
- package/src/console/ui/architecture/search.ts +6 -15
- package/src/console/ui/architecture/traffic.ts +4 -14
- package/src/console/ui/architecture/types.ts +2 -10
- package/src/console/ui/architecture/view.test.ts +7 -19
- package/src/console/ui/architecture/view.ts +9 -36
- package/src/console/ui/channels/ChannelsA11yView.tsx +4 -13
- package/src/console/ui/channels/a11y.test.tsx +1 -2
- package/src/console/ui/channels/confirmation.test.ts +1 -4
- package/src/console/ui/channels/confirmation.ts +1 -4
- package/src/console/ui/channels/findings.ts +1 -5
- package/src/console/ui/channels/index.ts +2 -8
- package/src/console/ui/channels/locale.test.ts +1 -3
- package/src/console/ui/channels/locale.ts +2 -8
- package/src/console/ui/channels/search.ts +4 -13
- package/src/console/ui/channels/taxonomy.ts +1 -3
- package/src/console/ui/clock/ClockA11yView.tsx +9 -26
- package/src/console/ui/clock/a11y.test.tsx +2 -5
- package/src/console/ui/clock/confirmation.test.ts +1 -3
- package/src/console/ui/clock/confirmation.ts +1 -4
- package/src/console/ui/clock/findings.ts +1 -4
- package/src/console/ui/clock/health.ts +2 -7
- package/src/console/ui/clock/index.ts +2 -10
- package/src/console/ui/clock/search.test.ts +1 -3
- package/src/console/ui/clock/search.ts +2 -6
- package/src/console/ui/clock/timeline.test.ts +1 -3
- package/src/console/ui/clock/waiting-on.test.ts +1 -5
- package/src/console/ui/clock/waiting-on.ts +2 -7
- package/src/console/ui/diff/DiffA11yView.tsx +8 -17
- package/src/console/ui/diff/a11y.test.tsx +2 -5
- package/src/console/ui/diff/group.test.ts +3 -13
- package/src/console/ui/diff/group.ts +2 -8
- package/src/console/ui/diff/index.ts +2 -8
- package/src/console/ui/diff/search.test.ts +1 -5
- package/src/console/ui/diff/search.ts +4 -16
- package/src/console/ui/diff/summary.ts +1 -2
- package/src/console/ui/dist/assets/{index-Dy4jht9P.js → index-BWo8R7NR.js} +2 -2
- package/src/console/ui/dist/assets/{panel-access-Dd37LU2c.js → panel-access-C0J2D-a2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-CC7LR6-J.js → panel-ai-D_m6WQI8.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-B5b3iKCz.js → panel-architecture-CKnXFyUx.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-CeNjTKXp.js → panel-channels-BOmQ-onL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-DgFTLoHV.js → panel-clock-giAq0Ccv.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DxehccqB.js → panel-diff-cdonmH8c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-BtrVn-Eg.js → panel-flows-DlCU5zjA.js} +2 -2
- package/src/console/ui/dist/assets/{panel-gates-Z9MKRGdH.js → panel-gates-XclZxWD5.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-Dt_AeXgd.js → panel-overview-BznEOTnb.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-DWd0TowH.js → panel-plugins-CcGM1g64.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-DGstFHeq.js → panel-runs-CGWNHLR4.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-DzEa2Fnt.js → panel-signals-CNywkdak.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-BJkbNgxx.js → panel-store-KmTbFHMH.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-BaRVO3gM.js → panel-traces-DBLx2ilD.js} +1 -1
- package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +1 -0
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/flows/FlowsA11yView.tsx +7 -22
- package/src/console/ui/flows/a11y.test.tsx +2 -8
- package/src/console/ui/flows/buffer.test.ts +3 -11
- package/src/console/ui/flows/buffer.ts +4 -11
- package/src/console/ui/flows/confirmation.ts +1 -3
- package/src/console/ui/flows/contract.test.ts +3 -7
- package/src/console/ui/flows/contract.ts +12 -36
- package/src/console/ui/flows/graph.test.ts +3 -11
- package/src/console/ui/flows/graph.ts +5 -15
- package/src/console/ui/flows/index.ts +1 -6
- package/src/console/ui/flows/save-as-test.ts +1 -3
- package/src/console/ui/flows/search.ts +8 -25
- package/src/console/ui/flows/tiers.ts +2 -9
- package/src/console/ui/gates/GatesA11yView.tsx +4 -13
- package/src/console/ui/gates/a11y.test.tsx +2 -5
- package/src/console/ui/gates/audit.ts +1 -3
- package/src/console/ui/gates/denial.test.ts +1 -3
- package/src/console/ui/gates/denial.ts +4 -12
- package/src/console/ui/gates/findings.ts +1 -3
- package/src/console/ui/gates/group.test.ts +3 -3
- package/src/console/ui/gates/group.ts +3 -11
- package/src/console/ui/gates/index.ts +2 -10
- package/src/console/ui/gates/search.ts +3 -10
- package/src/console/ui/overview/OverviewA11yView.tsx +9 -27
- package/src/console/ui/overview/a11y.test.tsx +1 -2
- package/src/console/ui/overview/busiest.ts +1 -3
- package/src/console/ui/overview/compose.ts +1 -13
- package/src/console/ui/overview/cost.ts +34 -38
- package/src/console/ui/overview/fixture.ts +21 -26
- package/src/console/ui/overview/golden.ts +4 -14
- package/src/console/ui/overview/index.ts +2 -9
- package/src/console/ui/overview/rank.test.ts +3 -10
- package/src/console/ui/overview/rank.ts +2 -7
- package/src/console/ui/overview/slo.test.ts +5 -9
- package/src/console/ui/overview/slo.ts +6 -23
- package/src/console/ui/overview/verdict.ts +3 -10
- package/src/console/ui/plugins/PluginsA11yView.tsx +8 -18
- package/src/console/ui/plugins/a11y.test.tsx +1 -2
- package/src/console/ui/plugins/fixture.ts +2 -4
- package/src/console/ui/plugins/group.test.ts +1 -3
- package/src/console/ui/plugins/group.ts +1 -6
- package/src/console/ui/plugins/index.ts +3 -13
- package/src/console/ui/plugins/search.ts +7 -19
- package/src/console/ui/runs/RunsA11yView.tsx +5 -20
- package/src/console/ui/runs/a11y.test.tsx +2 -5
- package/src/console/ui/runs/explain.ts +2 -6
- package/src/console/ui/runs/group.ts +3 -12
- package/src/console/ui/runs/histogram.test.ts +2 -11
- package/src/console/ui/runs/histogram.ts +1 -4
- package/src/console/ui/runs/index.ts +4 -18
- package/src/console/ui/runs/project.ts +2 -6
- package/src/console/ui/runs/query.test.ts +1 -3
- package/src/console/ui/runs/query.ts +12 -35
- package/src/console/ui/runs/search.ts +5 -19
- package/src/console/ui/runs/trace-link.test.ts +2 -6
- package/src/console/ui/runs/trace-link.ts +4 -16
- package/src/console/ui/runs/types.ts +1 -3
- package/src/console/ui/shell/App.tsx +2 -10
- package/src/console/ui/shell/client.ts +22 -115
- package/src/console/ui/shell/components/ui.tsx +1 -4
- package/src/console/ui/shell/layout/Shell.tsx +2 -4
- package/src/console/ui/shell/main.tsx +31 -96
- package/src/console/ui/shell/panels/access/AccessPanel.tsx +34 -98
- package/src/console/ui/shell/panels/ai/AiPanel.tsx +35 -106
- package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +18 -66
- package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +20 -57
- package/src/console/ui/shell/panels/clock/ClockPanel.tsx +22 -54
- package/src/console/ui/shell/panels/diff/DiffPanel.tsx +20 -61
- package/src/console/ui/shell/panels/flows/ContractEditor.tsx +6 -23
- package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +13 -28
- package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +9 -29
- package/src/console/ui/shell/panels/gates/GatesPanel.tsx +24 -80
- package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +20 -58
- package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +16 -41
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +31 -99
- package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +38 -101
- package/src/console/ui/shell/panels/store/StorePanel.tsx +27 -66
- package/src/console/ui/shell/panels/traces/TracesPanel.tsx +28 -89
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +22 -53
- package/src/console/ui/shell/setup/Wizard.tsx +3 -5
- package/src/console/ui/shell/styles.css +1 -2
- package/src/console/ui/signals/SignalsA11yView.tsx +9 -33
- package/src/console/ui/signals/a11y.test.tsx +2 -5
- package/src/console/ui/signals/confirmation.test.ts +1 -5
- package/src/console/ui/signals/confirmation.ts +2 -6
- package/src/console/ui/signals/dry-run.ts +1 -3
- package/src/console/ui/signals/durable.ts +1 -3
- package/src/console/ui/signals/findings.ts +1 -3
- package/src/console/ui/signals/group.test.ts +2 -8
- package/src/console/ui/signals/group.ts +2 -10
- package/src/console/ui/signals/index.ts +1 -4
- package/src/console/ui/signals/monitor.ts +2 -7
- package/src/console/ui/signals/schema-form.test.ts +3 -14
- package/src/console/ui/signals/schema-form.ts +1 -4
- package/src/console/ui/signals/search.ts +4 -13
- package/src/console/ui/store/StoreA11yView.tsx +3 -9
- package/src/console/ui/store/a11y.test.tsx +2 -5
- package/src/console/ui/store/confirmation.test.ts +1 -3
- package/src/console/ui/store/confirmation.ts +1 -4
- package/src/console/ui/store/dry-run.ts +2 -5
- package/src/console/ui/store/fixture.ts +1 -2
- package/src/console/ui/store/group.test.ts +1 -6
- package/src/console/ui/store/group.ts +1 -4
- package/src/console/ui/store/index.ts +1 -5
- package/src/console/ui/store/search.ts +2 -6
- package/src/console/ui/store/will-not-fire.ts +1 -2
- package/src/console/ui/traces/TracesA11yView.tsx +10 -29
- package/src/console/ui/traces/a11y.test.tsx +1 -2
- package/src/console/ui/traces/chain.test.ts +5 -19
- package/src/console/ui/traces/chain.ts +1 -3
- package/src/console/ui/traces/critical-path.test.ts +1 -3
- package/src/console/ui/traces/critical-path.ts +2 -6
- package/src/console/ui/traces/filter.test.ts +2 -6
- package/src/console/ui/traces/filter.ts +5 -13
- package/src/console/ui/traces/fold.test.ts +4 -12
- package/src/console/ui/traces/fold.ts +1 -3
- package/src/console/ui/traces/mini.ts +1 -3
- package/src/console/ui/traces/replay.test.ts +1 -3
- package/src/console/ui/traces/replay.ts +1 -3
- package/src/console/ui/traces/sampling.ts +1 -4
- package/src/console/ui/traces/search.ts +4 -17
- package/src/console/ui/traces/tier.ts +1 -3
- package/src/console/ui/vault/VaultA11yView.tsx +5 -17
- package/src/console/ui/vault/a11y.test.tsx +1 -2
- package/src/console/ui/vault/blast-radius.ts +1 -3
- package/src/console/ui/vault/confirmation.test.ts +1 -5
- package/src/console/ui/vault/confirmation.ts +1 -4
- package/src/console/ui/vault/dormant.test.ts +1 -4
- package/src/console/ui/vault/export-safe.test.ts +1 -5
- package/src/console/ui/vault/export-safe.ts +1 -4
- package/src/console/ui/vault/fixture.ts +3 -3
- package/src/console/ui/vault/group.ts +1 -2
- package/src/console/ui/vault/index.ts +2 -10
- package/src/console/ui/vault/search.ts +2 -6
- package/src/console/ui/vault/types.ts +1 -1
- package/src/console/xss.gate.test.ts +1 -3
- package/src/docker/compose.ts +215 -27
- package/src/docker/derive.ts +6 -14
- package/src/docker/docker.test.ts +118 -15
- package/src/docker/dockerfile.integration.test.ts +12 -21
- package/src/docker/helpers.ts +5 -4
- package/src/docker/index.ts +9 -6
- package/src/docker/pin.ts +1 -9
- package/src/docker/recipes/index.ts +6 -6
- package/src/docker/recipes/mailpit.ts +22 -0
- package/src/docker/recipes/openbao.ts +47 -0
- package/src/docker/recipes/postgres.ts +17 -6
- package/src/docker/recipes/redis.ts +11 -2
- package/src/docker/recipes/rustfs.ts +32 -0
- package/src/docker/stack-id.test.ts +73 -5
- package/src/docker/stack-id.ts +124 -33
- package/src/docker/stack.integration.test.ts +6 -21
- package/src/docker/stack.ts +36 -4
- package/src/docker/types.ts +16 -4
- package/src/docs-origin.ts +4 -4
- package/src/drivers/ai-anthropic.ts +5 -14
- package/src/drivers/ai-mock.ts +2 -5
- package/src/drivers/ai-openai-compatible.ts +4 -10
- package/src/drivers/ai-providers.test.ts +6 -13
- package/src/drivers/bun-native-completeness.test.ts +6 -15
- package/src/drivers/channel-console.ts +6 -7
- package/src/drivers/channel-fcm.ts +20 -30
- package/src/drivers/channel-resend.ts +1 -3
- package/src/drivers/channel-smtp.ts +2 -6
- package/src/drivers/channel-types.ts +2 -12
- package/src/drivers/channel-unifonic.ts +2 -6
- package/src/drivers/channel-wa-cloud.ts +2 -6
- package/src/drivers/channel-webpush.ts +21 -60
- package/src/drivers/conformance.test.ts +2 -10
- package/src/drivers/conformance.ts +8 -21
- package/src/drivers/drizzle-dialect.test.ts +20 -0
- package/src/drivers/drizzle-dialect.ts +37 -0
- package/src/drivers/fs.ts +2 -8
- package/src/drivers/index.ts +12 -57
- package/src/drivers/memory.ts +287 -75
- package/src/drivers/pgvector.ts +7 -11
- package/src/drivers/postgres.ts +7 -13
- package/src/drivers/redis.ts +13 -37
- package/src/drivers/s3.ts +15 -18
- package/src/drivers/signal-engine.ts +12 -49
- package/src/drivers/signal-memory.ts +2 -8
- package/src/drivers/signal-nats.ts +4 -9
- package/src/drivers/signal-postgres.ts +62 -129
- package/src/drivers/signal-redis.ts +7 -21
- package/src/drivers/signal-types.ts +1 -4
- package/src/drivers/sqlite.ts +3 -12
- package/src/drivers/types.ts +4 -23
- package/src/drivers/vault-driver-removal.test.ts +55 -0
- package/src/drivers/vault-env.ts +2 -6
- package/src/drivers/vault-infisical.ts +1 -5
- package/src/drivers/vault-managed.ts +1 -5
- package/src/drivers/vault-memory.ts +1 -5
- package/src/drivers/vault-openbao.test.ts +97 -0
- package/src/drivers/vault-openbao.ts +103 -40
- package/src/drivers/vault-types.ts +3 -16
- package/src/elements/ai/declare.ts +4 -14
- package/src/elements/ai/eval.ts +2 -6
- package/src/elements/ai/pii.ts +1 -3
- package/src/elements/ai/runtime.ts +12 -50
- package/src/elements/ai/schema.ts +4 -15
- package/src/elements/ai.test.ts +13 -24
- package/src/elements/ai.ts +1 -5
- package/src/elements/channel/consent.ts +3 -12
- package/src/elements/channel/costs.ts +4 -15
- package/src/elements/channel/declare.ts +1 -4
- package/src/elements/channel/email-auth.ts +1 -4
- package/src/elements/channel/locale.ts +2 -10
- package/src/elements/channel/mask.ts +1 -3
- package/src/elements/channel/mime.ts +2 -12
- package/src/elements/channel/outcomes.test.ts +1 -5
- package/src/elements/channel/outcomes.ts +5 -15
- package/src/elements/channel/receipts.ts +2 -7
- package/src/elements/channel/runtime.ts +28 -74
- package/src/elements/channel/suppression.ts +8 -14
- package/src/elements/channel.test.ts +8 -31
- package/src/elements/channel.ts +3 -14
- package/src/elements/clock/actions.ts +6 -18
- package/src/elements/clock/declare.ts +1 -3
- package/src/elements/clock/dst.ts +1 -5
- package/src/elements/clock/durable.ts +4 -16
- package/src/elements/clock/health.ts +2 -7
- package/src/elements/clock/leader.ts +2 -9
- package/src/elements/clock/reconcile.ts +4 -15
- package/src/elements/clock/runtime.ts +2 -5
- package/src/elements/clock/schedule.ts +4 -20
- package/src/elements/clock.test.ts +2 -7
- package/src/elements/clock.ts +5 -23
- package/src/elements/gate/permissions.ts +1 -4
- package/src/elements/gate/runtime.ts +7 -28
- package/src/elements/gate.ts +1 -5
- package/src/elements/index.ts +9 -7
- package/src/elements/signal/declare.ts +2 -7
- package/src/elements/signal/reconcile.test.ts +1 -4
- package/src/elements/signal/runtime.ts +1 -3
- package/src/elements/signal.test.ts +3 -15
- package/src/elements/signal.ts +2 -8
- package/src/elements/store/cache.test.ts +46 -0
- package/src/elements/store/cache.ts +3 -11
- package/src/elements/store/classify.ts +5 -22
- package/src/elements/store/declare.ts +11 -17
- package/src/elements/store/domain-ddl.test.ts +86 -0
- package/src/elements/store/emit-drizzle.ts +363 -0
- package/src/elements/store/files-policy.test.ts +1 -5
- package/src/elements/store/files-policy.ts +4 -12
- package/src/elements/store/load-plugin-tables.ts +103 -0
- package/src/elements/store/missing-relation.test.ts +83 -0
- package/src/elements/store/missing-relation.ts +38 -0
- package/src/elements/store/replica.ts +1 -4
- package/src/elements/store/resource-list-docs.fixture.ts +56 -0
- package/src/elements/store/resource-list-docs.test.ts +79 -0
- package/src/elements/store/resource.test.ts +253 -0
- package/src/elements/store/resource.ts +786 -0
- package/src/elements/store/runtime.ts +25 -28
- package/src/elements/store/schema-decl.test.ts +227 -0
- package/src/elements/store/schema-decl.ts +516 -0
- package/src/elements/store/sql-condition.test.ts +132 -0
- package/src/elements/store/sql-condition.ts +293 -65
- package/src/elements/store/sql-session.test.ts +135 -0
- package/src/elements/store/sql-session.ts +241 -84
- package/src/elements/store/table.ts +69 -23
- package/src/elements/store.test.ts +17 -39
- package/src/elements/store.ts +53 -31
- package/src/elements/vault/chain.ts +34 -0
- package/src/elements/vault/declare.ts +22 -32
- package/src/elements/vault/fingerprint.ts +2 -7
- package/src/elements/vault/redact.ts +2 -6
- package/src/elements/vault/runtime.ts +19 -33
- package/src/elements/vault.test.ts +12 -55
- package/src/elements/vault.ts +9 -13
- package/src/index.ts +5 -2
- package/src/kernel/adopt-routes.ts +15 -29
- package/src/kernel/app.ts +92 -118
- package/src/kernel/auth-resolve.ts +2 -9
- package/src/kernel/boot-bind/ai.ts +1 -4
- package/src/kernel/boot-bind/channel.test.ts +60 -0
- package/src/kernel/boot-bind/channel.ts +61 -5
- package/src/kernel/boot-bind/clock.ts +2 -6
- package/src/kernel/boot-bind/gate.ts +2 -8
- package/src/kernel/boot-bind/runs.ts +1 -3
- package/src/kernel/boot-bind/signal.ts +3 -10
- package/src/kernel/boot-bind/store.test.ts +27 -18
- package/src/kernel/boot-bind/store.ts +93 -52
- package/src/kernel/boot-bind/vault.ts +2 -8
- package/src/kernel/boot.test.ts +4 -10
- package/src/kernel/boot.ts +30 -77
- package/src/kernel/budget.test.ts +1 -3
- package/src/kernel/capability.ts +3 -10
- package/src/kernel/dry-run.test.ts +1 -4
- package/src/kernel/dry-run.ts +3 -11
- package/src/kernel/edge.test.ts +68 -0
- package/src/kernel/effects.test.ts +1 -5
- package/src/kernel/effects.ts +4 -11
- package/src/kernel/errors.registry.test.ts +2 -8
- package/src/kernel/errors.ts +23 -32
- package/src/kernel/flow.test.ts +12 -25
- package/src/kernel/flow.ts +13 -25
- package/src/kernel/fx.test.ts +31 -40
- package/src/kernel/fx.ts +135 -90
- package/src/kernel/hook-timing.test.ts +3 -6
- package/src/kernel/hook-timing.ts +5 -14
- package/src/kernel/hooks.test.ts +43 -10
- package/src/kernel/hooks.ts +25 -12
- package/src/kernel/index.ts +11 -17
- package/src/kernel/journal.ts +7 -31
- package/src/kernel/on.ts +45 -9
- package/src/kernel/pipeline.test.ts +10 -19
- package/src/kernel/pipeline.ts +5 -17
- package/src/kernel/plug.ts +4 -11
- package/src/kernel/plugin/conflicts.test.ts +13 -25
- package/src/kernel/plugin/decorate.test.ts +8 -13
- package/src/kernel/plugin/scoping.test.ts +4 -11
- package/src/kernel/plugin.ts +81 -27
- package/src/kernel/registry-isolation.test.ts +74 -0
- package/src/kernel/registry.ts +102 -30
- package/src/kernel/router.test.ts +15 -13
- package/src/kernel/router.ts +9 -29
- package/src/kernel/routing-budget.test.ts +1 -3
- package/src/kernel/run-telemetry.ts +1 -3
- package/src/kernel/triggers.ts +63 -21
- package/src/manifest/diff.test.ts +26 -91
- package/src/manifest/diff.ts +95 -225
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/manifest/index.ts +3 -10
- package/src/manifest/types.ts +30 -6
- package/src/manifest/undeclared.test.ts +4 -12
- package/src/manifest/undeclared.ts +1 -2
- package/src/manifest/validate.test.ts +2 -6
- package/src/manifest/validate.ts +5 -15
- package/src/mcp/authorization.ts +8 -27
- package/src/mcp/confirmation.ts +3 -10
- package/src/mcp/data.ts +2 -6
- package/src/mcp/docs-index.ts +4 -7
- package/src/mcp/docs-mcp.test.ts +3 -12
- package/src/mcp/docs-server.ts +7 -27
- package/src/mcp/docs-tools.ts +2 -6
- package/src/mcp/index.ts +1 -6
- package/src/mcp/injection.gate.test.ts +4 -18
- package/src/mcp/mcp.test.ts +18 -49
- package/src/mcp/protocol.ts +3 -10
- package/src/mcp/server.ts +11 -52
- package/src/mcp/session.ts +4 -14
- package/src/mcp/tools.ts +14 -50
- package/src/plugins/catalogue.test.ts +3 -7
- package/src/plugins/catalogue.ts +1 -3
- package/src/plugins/compression.test.ts +127 -0
- package/src/plugins/compression.ts +94 -0
- package/src/plugins/config-source.test.ts +204 -0
- package/src/plugins/config-source.ts +209 -0
- package/src/plugins/cors.test.ts +138 -0
- package/src/plugins/cors.ts +129 -0
- package/src/plugins/csrf.test.ts +102 -0
- package/src/plugins/csrf.ts +86 -0
- package/src/plugins/headers.ts +54 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/ip-allowlist.test.ts +105 -0
- package/src/plugins/ip-allowlist.ts +76 -0
- package/src/plugins/maintenance-mode.test.ts +91 -0
- package/src/plugins/maintenance-mode.ts +85 -0
- package/src/plugins/node-import-scan.ts +2 -5
- package/src/plugins/security-headers.test.ts +243 -0
- package/src/plugins/security-headers.ts +255 -0
- package/src/plugins/supply-chain.ts +5 -17
- package/src/release/exports.ts +3 -8
- package/src/release/measure.exports.test.ts +1 -3
- package/src/release/measure.ts +16 -45
- package/src/runs/bench.test.ts +1 -3
- package/src/runs/collect.ts +8 -25
- package/src/runs/drivers/clickhouse.ts +2 -9
- package/src/runs/drivers/files.ts +9 -31
- package/src/runs/drivers/memory.ts +5 -22
- package/src/runs/drivers/postgres.ts +8 -20
- package/src/runs/duckdb.ts +2 -8
- package/src/runs/index.ts +3 -14
- package/src/runs/outlier.ts +3 -10
- package/src/runs/parquet.ts +8 -26
- package/src/runs/privacy.ts +2 -8
- package/src/runs/runs.test.ts +3 -9
- package/src/runs/runtime.ts +7 -32
- package/src/runs/shred.ts +10 -29
- package/src/runs/types.ts +1 -4
- package/src/runtime/bun.ts +4 -15
- package/src/runtime/cold-start.bench.ts +1 -3
- package/src/runtime/dev-request-log.test.ts +1 -4
- package/src/runtime/dev-request-log.ts +1 -4
- package/src/runtime/index.ts +1 -6
- package/src/runtime/primitives.ts +5 -4
- package/src/runtime/security.ts +3 -13
- package/src/runtime/serve.test.ts +4 -7
- package/src/runtime/types.ts +1 -4
- package/src/runtime/web-standard.ts +3 -15
- package/src/term.test.ts +4 -6
- package/src/term.ts +49 -98
- package/src/test/create-test-app.test.ts +4 -14
- package/src/test/create-test-app.ts +27 -33
- package/src/test/provisions.integration.test.ts +3 -11
- package/src/upgrade/codemods.test.ts +25 -6
- package/src/upgrade/codemods.ts +116 -3
- package/src/validation/standard-schema.test.ts +2 -5
- package/src/validation/standard-schema.ts +13 -33
- package/docs/spec/console.md +0 -739
- package/docs/spec/example.md +0 -1196
- package/docs/spec/four-applications.md +0 -1195
- package/docs/spec/unified-theory.md +0 -489
- package/src/cli/doc-drift.test.ts +0 -146
- package/src/cli/doc-drift.ts +0 -430
- package/src/cli/doctor-diff-examples.ts +0 -92
- package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +0 -1
- package/src/drivers/vault-sops.ts +0 -271
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official `compression` plugin — gzip response bodies for clients that
|
|
3
|
+
* accept it. Uses only the public plugin API (unified-theory §14).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
7
|
+
import {
|
|
8
|
+
isConfigSource,
|
|
9
|
+
pluginConfigSnapshot,
|
|
10
|
+
resolvePluginOptions,
|
|
11
|
+
withConfigTable,
|
|
12
|
+
type ConfigSource,
|
|
13
|
+
} from "./config-source.ts";
|
|
14
|
+
import { appendVary } from "./headers.ts";
|
|
15
|
+
|
|
16
|
+
/** Options for {@link compression}. */
|
|
17
|
+
export interface CompressionOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Minimum body size in bytes before compression is worth it.
|
|
20
|
+
* Smaller bodies can grow under gzip. Default `1024`.
|
|
21
|
+
*/
|
|
22
|
+
readonly minSize?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Which `Content-Type`s to compress. Default matches JSON (including
|
|
25
|
+
* `+json`), JavaScript, XML, and every `text/*`.
|
|
26
|
+
*/
|
|
27
|
+
readonly match?: RegExp;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Default compressible types: JSON, JS, XML, and any text/*. */
|
|
31
|
+
const DEFAULT_MATCH = /^(application\/(json|.+\+json|javascript|xml|.+\+xml)|text\/)/i;
|
|
32
|
+
|
|
33
|
+
/** True when the Accept-Encoding value allows gzip (honoring `q=0`). */
|
|
34
|
+
export function acceptsGzip(header: string | null): boolean {
|
|
35
|
+
if (!header) return false;
|
|
36
|
+
for (const part of header.split(",")) {
|
|
37
|
+
const [token, ...params] = part.trim().split(";");
|
|
38
|
+
const name = token!.trim().toLowerCase();
|
|
39
|
+
if (name !== "gzip" && name !== "*") continue;
|
|
40
|
+
const q = params.map((p) => p.trim()).find((p) => p.toLowerCase().startsWith("q="));
|
|
41
|
+
if (q !== undefined && Number.parseFloat(q.slice(2)) === 0) continue;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compress HTTP response bodies with `Bun.gzipSync` when the client sends
|
|
49
|
+
* `Accept-Encoding: gzip`. Skips: already-encoded responses, `no-transform`
|
|
50
|
+
* cache directives, event streams, non-matching content types, and bodies
|
|
51
|
+
* under `minSize`. Non-HTTP triggers no-op.
|
|
52
|
+
*
|
|
53
|
+
* Accepts static options or a {@link ConfigSource} for DB-driven thresholds.
|
|
54
|
+
*
|
|
55
|
+
* @param options - Size threshold / content-type matcher, or a config source
|
|
56
|
+
*/
|
|
57
|
+
export function compression(
|
|
58
|
+
options: CompressionOptions | ConfigSource<CompressionOptions> = {},
|
|
59
|
+
): PluginDef {
|
|
60
|
+
const def = plugin("compression", {
|
|
61
|
+
version: "0.0.2",
|
|
62
|
+
config: pluginConfigSnapshot(options),
|
|
63
|
+
}).hook("onResponse", async (ctx) => {
|
|
64
|
+
if (!ctx.response || !ctx.request) return;
|
|
65
|
+
const resolved = resolvePluginOptions(options);
|
|
66
|
+
const minSize = resolved.minSize ?? 1024;
|
|
67
|
+
const match = resolved.match ?? DEFAULT_MATCH;
|
|
68
|
+
if (!acceptsGzip(ctx.request.headers.get("accept-encoding"))) return;
|
|
69
|
+
|
|
70
|
+
const headers = new Headers(ctx.response.headers);
|
|
71
|
+
if (headers.has("content-encoding")) return;
|
|
72
|
+
const cacheControl = headers.get("cache-control");
|
|
73
|
+
if (cacheControl !== null && /\bno-transform\b/i.test(cacheControl)) return;
|
|
74
|
+
|
|
75
|
+
const contentType = headers.get("content-type") ?? "";
|
|
76
|
+
if (!match.test(contentType)) return;
|
|
77
|
+
|
|
78
|
+
const body = await ctx.response.arrayBuffer();
|
|
79
|
+
if (body.byteLength < minSize) return;
|
|
80
|
+
|
|
81
|
+
const compressed = Bun.gzipSync(new Uint8Array(body));
|
|
82
|
+
headers.set("content-encoding", "gzip");
|
|
83
|
+
headers.delete("content-length");
|
|
84
|
+
appendVary(headers, "accept-encoding");
|
|
85
|
+
|
|
86
|
+
ctx.response = new Response(compressed, {
|
|
87
|
+
status: ctx.response.status,
|
|
88
|
+
statusText: ctx.response.statusText,
|
|
89
|
+
headers,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return isConfigSource(options) ? withConfigTable(def, options) : def;
|
|
94
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `configSource` — code floor, DB overrides, automatic KV read-through,
|
|
3
|
+
* and the sync flow's declared effects. Also the plugin-side wiring:
|
|
4
|
+
* hooks follow the box, DB-backed sources contribute their config table.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { memoryKvDriver } from "../drivers/memory.ts";
|
|
9
|
+
import { sqliteDriver } from "../drivers/sqlite.ts";
|
|
10
|
+
import { store } from "../elements/store/declare.ts";
|
|
11
|
+
import { createStoreRuntime } from "../elements/store/runtime.ts";
|
|
12
|
+
import type { SqlStoreHandle } from "../elements/store/sql-session.ts";
|
|
13
|
+
import { defineTable } from "../elements/store/table.ts";
|
|
14
|
+
import { oke } from "../kernel/app.ts";
|
|
15
|
+
import { flow, resetFlowSeq } from "../kernel/flow.ts";
|
|
16
|
+
import { createFxContext, type Fx } from "../kernel/fx.ts";
|
|
17
|
+
import { on, resetBindings } from "../kernel/on.ts";
|
|
18
|
+
import { createPluginRegistry } from "../kernel/registry.ts";
|
|
19
|
+
import { http } from "../kernel/triggers.ts";
|
|
20
|
+
import { configSource, resolvePluginOptions } from "./config-source.ts";
|
|
21
|
+
import { maintenanceMode } from "./maintenance-mode.ts";
|
|
22
|
+
import { securityHeaders } from "./security-headers.ts";
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
resetBindings();
|
|
26
|
+
resetFlowSeq();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** A KV-only source against the in-memory stub. */
|
|
30
|
+
function kvFx(kvRef: string, seed: Record<string, unknown>): Fx {
|
|
31
|
+
return createFxContext({
|
|
32
|
+
flow: "x.config-sync",
|
|
33
|
+
effects: { reads: [kvRef as `kv:${string}`], writes: [kvRef as `kv:${string}`] },
|
|
34
|
+
storeData: { [kvRef]: seed },
|
|
35
|
+
}).fx;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("configSource — box semantics", () => {
|
|
39
|
+
test("current() is the code config before any sync", () => {
|
|
40
|
+
const source = configSource({ plugin: "maintenance-mode", code: { enabled: false } });
|
|
41
|
+
expect(source.current()).toEqual({ enabled: false });
|
|
42
|
+
expect(resolvePluginOptions(source)).toEqual({ enabled: false });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("sync reads the KV cache and merges it over code", async () => {
|
|
46
|
+
const cfg = store.kv("cfg");
|
|
47
|
+
const source = configSource({
|
|
48
|
+
plugin: "maintenance-mode",
|
|
49
|
+
code: { enabled: false, retryAfter: 60 },
|
|
50
|
+
kv: cfg,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const fx = kvFx("kv:cfg", { "oke:plugin-config:maintenance-mode": { enabled: true } });
|
|
54
|
+
const outcome = await source.sync().do(undefined, fx);
|
|
55
|
+
|
|
56
|
+
expect(outcome).toEqual({ source: "kv" });
|
|
57
|
+
expect(source.current()).toEqual({ enabled: true, retryAfter: 60 });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("DB is the source of truth; KV write-back makes the next tick KV-served", async () => {
|
|
61
|
+
const db = store.sql("cfgdb");
|
|
62
|
+
const cfg = store.kv("cfg");
|
|
63
|
+
const runtime = createStoreRuntime({
|
|
64
|
+
drivers: { sql: sqliteDriver, kv: memoryKvDriver },
|
|
65
|
+
sql: { cfgdb: { name: "cfgdb", primary: { url: ":memory:" } } },
|
|
66
|
+
kv: { cfg: {} },
|
|
67
|
+
});
|
|
68
|
+
runtime.register(db);
|
|
69
|
+
runtime.register(cfg);
|
|
70
|
+
|
|
71
|
+
const effects = {
|
|
72
|
+
reads: ["sql:cfgdb" as const, "kv:cfg" as const],
|
|
73
|
+
writes: ["sql:cfgdb" as const, "kv:cfg" as const],
|
|
74
|
+
};
|
|
75
|
+
const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
|
|
76
|
+
|
|
77
|
+
const table = defineTable("security_headers_config", { key: true, value: true });
|
|
78
|
+
const sql = fx.store(db) as SqlStoreHandle;
|
|
79
|
+
await sql.ensureTable(table);
|
|
80
|
+
await sql
|
|
81
|
+
.insert(table)
|
|
82
|
+
.values({ key: "config", value: JSON.stringify({ hsts: true }) })
|
|
83
|
+
.execute();
|
|
84
|
+
|
|
85
|
+
const source = configSource({
|
|
86
|
+
plugin: "security-headers",
|
|
87
|
+
code: { hsts: false },
|
|
88
|
+
db: { store: db },
|
|
89
|
+
kv: cfg,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const first = await source.sync().do(undefined, fx);
|
|
93
|
+
expect(first).toEqual({ source: "db" });
|
|
94
|
+
expect(source.current()).toEqual({ hsts: true });
|
|
95
|
+
|
|
96
|
+
// Remove the DB row — a fresh invocation must still be served from KV.
|
|
97
|
+
await sql.delete(table, "config");
|
|
98
|
+
const second = await source.sync().do(undefined, fx);
|
|
99
|
+
expect(second).toEqual({ source: "kv" });
|
|
100
|
+
expect(source.current()).toEqual({ hsts: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an empty DB reverts to the code config", async () => {
|
|
104
|
+
const db = store.sql("cfgdb");
|
|
105
|
+
const runtime = createStoreRuntime({
|
|
106
|
+
drivers: { sql: sqliteDriver },
|
|
107
|
+
sql: { cfgdb: { name: "cfgdb", primary: { url: ":memory:" } } },
|
|
108
|
+
});
|
|
109
|
+
runtime.register(db);
|
|
110
|
+
|
|
111
|
+
const effects = { reads: ["sql:cfgdb" as const], writes: ["sql:cfgdb" as const] };
|
|
112
|
+
const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
|
|
113
|
+
const table = defineTable("security_headers_config", { key: true, value: true });
|
|
114
|
+
await (fx.store(db) as SqlStoreHandle).ensureTable(table);
|
|
115
|
+
|
|
116
|
+
const source = configSource({
|
|
117
|
+
plugin: "security-headers",
|
|
118
|
+
code: { frameOptions: "DENY" as const },
|
|
119
|
+
db: { store: db },
|
|
120
|
+
});
|
|
121
|
+
expect(await source.sync().do(undefined, fx)).toEqual({ source: "code" });
|
|
122
|
+
expect(source.current()).toEqual({ frameOptions: "DENY" });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a corrupt JSON row fails the sync loudly", async () => {
|
|
126
|
+
const db = store.sql("cfgdb");
|
|
127
|
+
const runtime = createStoreRuntime({
|
|
128
|
+
drivers: { sql: sqliteDriver },
|
|
129
|
+
sql: { cfgdb: { name: "cfgdb", primary: { url: ":memory:" } } },
|
|
130
|
+
});
|
|
131
|
+
runtime.register(db);
|
|
132
|
+
|
|
133
|
+
const effects = { reads: ["sql:cfgdb" as const], writes: ["sql:cfgdb" as const] };
|
|
134
|
+
const fx = createFxContext({ flow: "x.config-sync", effects, storeRuntime: runtime }).fx;
|
|
135
|
+
const table = defineTable("security_headers_config", { key: true, value: true });
|
|
136
|
+
const sql = fx.store(db) as SqlStoreHandle;
|
|
137
|
+
await sql.ensureTable(table);
|
|
138
|
+
await sql.insert(table).values({ key: "config", value: "{not json" }).execute();
|
|
139
|
+
|
|
140
|
+
const source = configSource({
|
|
141
|
+
plugin: "security-headers",
|
|
142
|
+
code: {},
|
|
143
|
+
db: { store: db },
|
|
144
|
+
});
|
|
145
|
+
await expect(source.sync().do(undefined, fx)).rejects.toThrow(/not valid JSON/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("configSource — plugin wiring", () => {
|
|
150
|
+
test("the maintenanceMode hook follows the box after a sync", async () => {
|
|
151
|
+
const cfg = store.kv("cfg");
|
|
152
|
+
const source = configSource({
|
|
153
|
+
plugin: "maintenance-mode",
|
|
154
|
+
code: { enabled: false },
|
|
155
|
+
kv: cfg,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
159
|
+
const app = oke({ name: "cfg-live" }).plug(maintenanceMode(source));
|
|
160
|
+
|
|
161
|
+
expect((await app.fetch(new Request("http://localhost/x"))).status).toBe(200);
|
|
162
|
+
|
|
163
|
+
await source.sync().do(undefined, kvFx("kv:cfg", {}));
|
|
164
|
+
expect((await app.fetch(new Request("http://localhost/x"))).status).toBe(200);
|
|
165
|
+
|
|
166
|
+
const seeded = kvFx("kv:cfg", { "oke:plugin-config:maintenance-mode": { enabled: true } });
|
|
167
|
+
await source.sync().do(undefined, seeded);
|
|
168
|
+
expect((await app.fetch(new Request("http://localhost/x"))).status).toBe(503);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("DB-backed sources contribute their config table; static options do not", () => {
|
|
172
|
+
const db = store.sql("cfgdb");
|
|
173
|
+
const source = configSource({
|
|
174
|
+
plugin: "security-headers",
|
|
175
|
+
code: {},
|
|
176
|
+
db: { store: db },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const registry = createPluginRegistry();
|
|
180
|
+
const registration = registry.plug(securityHeaders(source), { kind: "app" });
|
|
181
|
+
expect(registration?.tables.map((t) => t.name)).toEqual(["security_headers_config"]);
|
|
182
|
+
|
|
183
|
+
const staticRegistry = createPluginRegistry();
|
|
184
|
+
const staticRegistration = staticRegistry.plug(securityHeaders({}), { kind: "app" });
|
|
185
|
+
expect(staticRegistration?.tables).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("the identity snapshot is the code config, not the live box", async () => {
|
|
189
|
+
const cfg = store.kv("cfg");
|
|
190
|
+
const source = configSource({
|
|
191
|
+
plugin: "maintenance-mode",
|
|
192
|
+
code: { enabled: false },
|
|
193
|
+
kv: cfg,
|
|
194
|
+
});
|
|
195
|
+
await source
|
|
196
|
+
.sync()
|
|
197
|
+
.do(undefined, kvFx("kv:cfg", { "oke:plugin-config:maintenance-mode": { enabled: true } }));
|
|
198
|
+
|
|
199
|
+
const registry = createPluginRegistry();
|
|
200
|
+
registry.plug(maintenanceMode(source), { kind: "app" });
|
|
201
|
+
// Double-plug with the same source is a no-op (snapshot still equals code).
|
|
202
|
+
expect(registry.plug(maintenanceMode(source), { kind: "app" })).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `configSource()` — runtime-mutable plugin configuration: code as the
|
|
3
|
+
* floor, DB as the source of truth, KV as the automatic read-through cache.
|
|
4
|
+
*
|
|
5
|
+
* Hooks cannot touch stores (fx is capability-gated per flow), so plugins
|
|
6
|
+
* resolve config synchronously from an in-memory box. A real flow — bound
|
|
7
|
+
* by the app on a clock trigger, with declared effects — refreshes that
|
|
8
|
+
* box: KV hit → box; miss → DB row → box + KV write-back. Every world
|
|
9
|
+
* access stays inside a flow, per the fx rule.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { KvStoreDecl, SqlStoreDecl } from "../elements/store/declare.ts";
|
|
13
|
+
import type { KvStoreFxHandle } from "../elements/store/runtime.ts";
|
|
14
|
+
import { field } from "../elements/store/schema-decl.ts";
|
|
15
|
+
import type { SqlStoreHandle } from "../elements/store/sql-session.ts";
|
|
16
|
+
import { flow, type AnyFlowDef } from "../kernel/flow.ts";
|
|
17
|
+
import type { PluginDef } from "../kernel/plugin.ts";
|
|
18
|
+
|
|
19
|
+
/** Resource refs a config sync flow may touch. */
|
|
20
|
+
type StoreRef = `kv:${string}` | `sql:${string}`;
|
|
21
|
+
|
|
22
|
+
const configSourceBrand: unique symbol = Symbol("oke.configSource");
|
|
23
|
+
|
|
24
|
+
/** DB side of a {@link ConfigSource}. */
|
|
25
|
+
export interface ConfigSourceDb {
|
|
26
|
+
/** `store.sql` binding owned by the app. */
|
|
27
|
+
readonly store: SqlStoreDecl;
|
|
28
|
+
/**
|
|
29
|
+
* Config table name. Default `<plugin-id>_config` (kebab → snake). The
|
|
30
|
+
* owning plugin contributes this table (`oke db push` creates it).
|
|
31
|
+
*/
|
|
32
|
+
readonly table?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Options for {@link configSource}. */
|
|
36
|
+
export interface ConfigSourceOptions<T extends object> {
|
|
37
|
+
/**
|
|
38
|
+
* Plugin id this config belongs to (e.g. `"security-headers"`) — derives
|
|
39
|
+
* the DB row key, the KV key, and the default table name.
|
|
40
|
+
*/
|
|
41
|
+
readonly plugin: string;
|
|
42
|
+
/**
|
|
43
|
+
* Base config in code — always present, always the floor. DB values are
|
|
44
|
+
* shallow-merged over it; a missing DB row means exactly this.
|
|
45
|
+
*/
|
|
46
|
+
readonly code: T;
|
|
47
|
+
/** DB source of truth (optional). Values override `code`. */
|
|
48
|
+
readonly db?: ConfigSourceDb;
|
|
49
|
+
/**
|
|
50
|
+
* KV binding for the automatic read-through cache (optional). When set,
|
|
51
|
+
* DB values are cached with `ttl` so steady-state sync ticks never touch
|
|
52
|
+
* the database.
|
|
53
|
+
*/
|
|
54
|
+
readonly kv?: KvStoreDecl;
|
|
55
|
+
/** KV TTL for cached DB values. Default `"30s"`. */
|
|
56
|
+
readonly ttl?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A live config box for one plugin. `current()` is synchronous and never
|
|
61
|
+
* throws — safe to read from any hook. `sync()` is the refresh flow the
|
|
62
|
+
* app binds once: `on(every("30s"), source.sync())`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ConfigSource<T extends object> {
|
|
65
|
+
readonly [configSourceBrand]: true;
|
|
66
|
+
/** Plugin id. */
|
|
67
|
+
readonly plugin: string;
|
|
68
|
+
/** The static `code` config — used as the plugin identity snapshot. */
|
|
69
|
+
readonly codeConfig: T;
|
|
70
|
+
/** Config table name when DB-backed (the owning plugin contributes it). */
|
|
71
|
+
readonly table?: string;
|
|
72
|
+
/** Current merged config (code ← DB overrides). */
|
|
73
|
+
current(): T;
|
|
74
|
+
/** Refresh flow — bind once on a clock trigger. */
|
|
75
|
+
sync(): AnyFlowDef;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Row key of the single config row inside a plugin's config table. */
|
|
79
|
+
const ROW_KEY = "config";
|
|
80
|
+
|
|
81
|
+
/** kebab-case plugin id → snake_case default table name. */
|
|
82
|
+
function defaultTable(plugin: string): string {
|
|
83
|
+
return `${plugin.replace(/-/g, "_")}_config`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Shallow merge — DB partial wins over code, key by key. */
|
|
87
|
+
function mergeConfig<T extends object>(code: T, partial: unknown): T {
|
|
88
|
+
if (typeof partial !== "object" || partial === null || Array.isArray(partial)) return code;
|
|
89
|
+
return { ...code, ...(partial as Partial<T>) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a code-or-DB config source for a plugin.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Plugin id, code floor, optional DB + KV
|
|
96
|
+
*/
|
|
97
|
+
export function configSource<T extends object>(options: ConfigSourceOptions<T>): ConfigSource<T> {
|
|
98
|
+
const table =
|
|
99
|
+
options.db === undefined ? undefined : (options.db.table ?? defaultTable(options.plugin));
|
|
100
|
+
const kvKey = `oke:plugin-config:${options.plugin}`;
|
|
101
|
+
const ttl = options.ttl ?? "30s";
|
|
102
|
+
let box: T = options.code;
|
|
103
|
+
|
|
104
|
+
const source: ConfigSource<T> = {
|
|
105
|
+
[configSourceBrand]: true,
|
|
106
|
+
plugin: options.plugin,
|
|
107
|
+
codeConfig: options.code,
|
|
108
|
+
...(table !== undefined ? { table } : {}),
|
|
109
|
+
current: () => box,
|
|
110
|
+
sync() {
|
|
111
|
+
const reads: StoreRef[] = [];
|
|
112
|
+
const writes: StoreRef[] = [];
|
|
113
|
+
if (options.kv !== undefined) {
|
|
114
|
+
reads.push(options.kv.ref);
|
|
115
|
+
writes.push(options.kv.ref);
|
|
116
|
+
}
|
|
117
|
+
if (options.db !== undefined) reads.push(options.db.store.ref);
|
|
118
|
+
|
|
119
|
+
return flow({
|
|
120
|
+
name: `${options.plugin}.config-sync`,
|
|
121
|
+
effects: { reads, writes },
|
|
122
|
+
do: async (_input, fx) => {
|
|
123
|
+
if (options.kv !== undefined) {
|
|
124
|
+
const kv = fx.store(options.kv) as KvStoreFxHandle;
|
|
125
|
+
const cached = await kv.get(kvKey);
|
|
126
|
+
if (cached !== undefined && cached !== null) {
|
|
127
|
+
box = mergeConfig(options.code, cached);
|
|
128
|
+
return { source: "kv" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.db !== undefined && table !== undefined) {
|
|
133
|
+
const db = fx.store(options.db.store) as SqlStoreHandle;
|
|
134
|
+
const rows = await db.raw(`SELECT "value" FROM "${table}" WHERE "key" = ?`, [ROW_KEY]);
|
|
135
|
+
const raw = rows[0]?.["value"];
|
|
136
|
+
let partial: unknown = undefined;
|
|
137
|
+
if (raw !== undefined && raw !== null) {
|
|
138
|
+
if (typeof raw !== "string") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`configSource(${options.plugin}): row "value" must be JSON text, got ${typeof raw}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
partial = JSON.parse(raw);
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`configSource(${options.plugin}): config row is not valid JSON — ${String(cause)}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
box = mergeConfig(options.code, partial);
|
|
152
|
+
if (options.kv !== undefined) {
|
|
153
|
+
const kv = fx.store(options.kv) as KvStoreFxHandle;
|
|
154
|
+
await kv.set(kvKey, partial ?? {}, ttl);
|
|
155
|
+
}
|
|
156
|
+
return { source: partial === undefined ? "code" : "db" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
box = options.code;
|
|
160
|
+
return { source: "code" };
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return source;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Type guard for {@link ConfigSource}. */
|
|
170
|
+
export function isConfigSource<T extends object>(value: unknown): value is ConfigSource<T> {
|
|
171
|
+
return typeof value === "object" && value !== null && configSourceBrand in value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve plugin options at request time — static options as-is, a
|
|
176
|
+
* ConfigSource through its current merged value.
|
|
177
|
+
*
|
|
178
|
+
* @param value - Options or source
|
|
179
|
+
*/
|
|
180
|
+
export function resolvePluginOptions<T extends object>(value: T | ConfigSource<T>): T {
|
|
181
|
+
return isConfigSource<T>(value) ? value.current() : value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Identity snapshot for `plugin(name, { config })` — always the static
|
|
186
|
+
* code config, so DB edits never trigger false conflict errors.
|
|
187
|
+
*
|
|
188
|
+
* @param value - Options or source
|
|
189
|
+
*/
|
|
190
|
+
export function pluginConfigSnapshot<T extends object>(value: T | ConfigSource<T>): T {
|
|
191
|
+
return isConfigSource<T>(value) ? value.codeConfig : value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Contribute the plugin's config table when its options are a DB-backed
|
|
196
|
+
* ConfigSource. Chain last on the builder.
|
|
197
|
+
*
|
|
198
|
+
* @param def - Plugin builder
|
|
199
|
+
* @param value - Options or source passed to the factory
|
|
200
|
+
*/
|
|
201
|
+
export function withConfigTable<D extends PluginDef>(def: D, value: unknown): D {
|
|
202
|
+
if (isConfigSource(value) && value.table !== undefined) {
|
|
203
|
+
def.table(value.table, {
|
|
204
|
+
key: field.text().primaryKey(),
|
|
205
|
+
value: field.text().notNull(),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return def;
|
|
209
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cors` plugin — preflight at the edge, CORS headers on matched
|
|
3
|
+
* responses, closed-by-default origin rule.
|
|
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 { cors, originAllowed } from "./cors.ts";
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
resetBindings();
|
|
15
|
+
resetFlowSeq();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function preflight(path = "/x", headers: Record<string, string> = {}): Request {
|
|
19
|
+
return new Request(`http://localhost${path}`, {
|
|
20
|
+
method: "OPTIONS",
|
|
21
|
+
headers: {
|
|
22
|
+
origin: "https://app.example.com",
|
|
23
|
+
"access-control-request-method": "POST",
|
|
24
|
+
...headers,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("cors plugin — preflight", () => {
|
|
30
|
+
test("answers preflight for a path bound to another method (edge)", async () => {
|
|
31
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
32
|
+
const app = oke({ name: "cors-pre" }).plug(cors({ origin: "https://app.example.com" }));
|
|
33
|
+
|
|
34
|
+
const res = await app.fetch(preflight());
|
|
35
|
+
|
|
36
|
+
expect(res.status).toBe(204);
|
|
37
|
+
expect(res.headers.get("access-control-allow-origin")).toBe("https://app.example.com");
|
|
38
|
+
expect(res.headers.get("access-control-allow-methods")).toContain("POST");
|
|
39
|
+
expect(res.headers.get("vary")).toContain("origin");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("denied origins get a 204 without CORS headers — the browser blocks quietly", async () => {
|
|
43
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
44
|
+
const app = oke({ name: "cors-deny" }).plug(cors({ origin: "https://other.example.com" }));
|
|
45
|
+
|
|
46
|
+
const res = await app.fetch(preflight());
|
|
47
|
+
|
|
48
|
+
expect(res.status).toBe(204);
|
|
49
|
+
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("no origin configured answers nothing — cross-origin closed by default", async () => {
|
|
53
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
54
|
+
const app = oke({ name: "cors-closed" }).plug(cors());
|
|
55
|
+
|
|
56
|
+
const res = await app.fetch(preflight());
|
|
57
|
+
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a plain unmatched OPTIONS without preflight headers still 404s", async () => {
|
|
61
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
62
|
+
const app = oke({ name: "cors-404" }).plug(cors({ origin: "*" }));
|
|
63
|
+
|
|
64
|
+
const res = await app.fetch(new Request("http://localhost/x", { method: "OPTIONS" }));
|
|
65
|
+
expect(res.status).toBe(404);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("reflects request headers by default, honors configured lists and maxAge", async () => {
|
|
69
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
70
|
+
const app = oke({ name: "cors-hdr" }).plug(
|
|
71
|
+
cors({ origin: "*", maxAge: 600, allowedHeaders: ["x-custom"] }),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const reflected = await app.fetch(
|
|
75
|
+
preflight("/x", { "access-control-request-headers": "authorization, content-type" }),
|
|
76
|
+
);
|
|
77
|
+
// Configured list wins over reflection.
|
|
78
|
+
expect(reflected.headers.get("access-control-allow-headers")).toBe("x-custom");
|
|
79
|
+
expect(reflected.headers.get("access-control-max-age")).toBe("600");
|
|
80
|
+
expect(reflected.headers.get("access-control-allow-origin")).toBe("*");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("credentials reflect the origin instead of stamping *", async () => {
|
|
84
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
85
|
+
const app = oke({ name: "cors-cred" }).plug(cors({ origin: "*", credentials: true }));
|
|
86
|
+
|
|
87
|
+
const res = await app.fetch(preflight());
|
|
88
|
+
expect(res.headers.get("access-control-allow-origin")).toBe("https://app.example.com");
|
|
89
|
+
expect(res.headers.get("access-control-allow-credentials")).toBe("true");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("cors plugin — actual requests", () => {
|
|
94
|
+
test("allowed origins get allow-origin (+ exposed headers) on matched responses", async () => {
|
|
95
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
96
|
+
const app = oke({ name: "cors-actual" }).plug(
|
|
97
|
+
cors({ origin: ["https://app.example.com"], exposedHeaders: ["x-total"] }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const res = await app.fetch(
|
|
101
|
+
new Request("http://localhost/x", { headers: { origin: "https://app.example.com" } }),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
expect(res.headers.get("access-control-allow-origin")).toBe("https://app.example.com");
|
|
106
|
+
expect(res.headers.get("access-control-expose-headers")).toBe("x-total");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("denied origins get no CORS headers on matched responses", async () => {
|
|
110
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
111
|
+
const app = oke({ name: "cors-actual-deny" }).plug(cors({ origin: "https://ok.example.com" }));
|
|
112
|
+
|
|
113
|
+
const res = await app.fetch(
|
|
114
|
+
new Request("http://localhost/x", { headers: { origin: "https://evil.example.com" } }),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(200);
|
|
118
|
+
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("requests without an Origin header are untouched", async () => {
|
|
122
|
+
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
123
|
+
const app = oke({ name: "cors-no-origin" }).plug(cors({ origin: "*" }));
|
|
124
|
+
|
|
125
|
+
const res = await app.fetch(new Request("http://localhost/x"));
|
|
126
|
+
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("originAllowed", () => {
|
|
131
|
+
test("undefined / * / string / list", () => {
|
|
132
|
+
expect(originAllowed("https://a.com", undefined)).toBe(false);
|
|
133
|
+
expect(originAllowed("https://a.com", "*")).toBe(true);
|
|
134
|
+
expect(originAllowed("https://a.com", "https://a.com")).toBe(true);
|
|
135
|
+
expect(originAllowed("https://b.com", "https://a.com")).toBe(false);
|
|
136
|
+
expect(originAllowed("https://b.com", ["https://a.com", "https://b.com"])).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|