okengine 0.1.4
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 +91 -0
- package/README.md +327 -0
- package/docs/spec/complete-example.md +1188 -0
- package/docs/spec/console.md +739 -0
- package/docs/spec/four-applications.md +1188 -0
- package/docs/spec/unified-theory.md +489 -0
- package/package.json +152 -0
- package/spec/manifest.v1.schema.json +624 -0
- package/src/auth/api-keys.ts +168 -0
- package/src/auth/attenuation.ts +116 -0
- package/src/auth/auth.test.ts +273 -0
- package/src/auth/cross-plane.ts +98 -0
- package/src/auth/index.ts +112 -0
- package/src/auth/invites.ts +66 -0
- package/src/auth/invoke-as.ts +49 -0
- package/src/auth/operator.ts +178 -0
- package/src/auth/planes.ts +102 -0
- package/src/auth/plugin.ts +44 -0
- package/src/auth/roles.ts +84 -0
- package/src/auth/sessions.ts +418 -0
- package/src/auth/tables.ts +131 -0
- package/src/cli/args.ts +50 -0
- package/src/cli/branch.ts +75 -0
- package/src/cli/build.ts +86 -0
- package/src/cli/client-add.test.ts +140 -0
- package/src/cli/client-add.ts +164 -0
- package/src/cli/completion.test.ts +58 -0
- package/src/cli/completion.ts +296 -0
- package/src/cli/dev-app-runner.ts +77 -0
- package/src/cli/dev.test.ts +435 -0
- package/src/cli/dev.ts +594 -0
- package/src/cli/doc-drift.test.ts +146 -0
- package/src/cli/doc-drift.ts +397 -0
- package/src/cli/docker-cli.test.ts +221 -0
- package/src/cli/docker.ts +127 -0
- package/src/cli/doctor-diff-examples.ts +92 -0
- package/src/cli/doctor-diff.test.ts +91 -0
- package/src/cli/doctor-diff.ts +274 -0
- package/src/cli/doctor-pii.test.ts +98 -0
- package/src/cli/doctor-pii.ts +151 -0
- package/src/cli/doctor.test.ts +150 -0
- package/src/cli/doctor.ts +223 -0
- package/src/cli/eval.ts +122 -0
- package/src/cli/exit.ts +19 -0
- package/src/cli/gates-list.test.ts +47 -0
- package/src/cli/gates-list.ts +84 -0
- package/src/cli/images.ts +277 -0
- package/src/cli/index.ts +99 -0
- package/src/cli/json-out.test.ts +129 -0
- package/src/cli/load-config.ts +71 -0
- package/src/cli/mcp-from-console.ts +50 -0
- package/src/cli/privacy-erase.ts +73 -0
- package/src/cli/registry.ts +455 -0
- package/src/cli/safe-defaults.test.ts +92 -0
- package/src/cli/schema.ts +196 -0
- package/src/cli/stack.ts +82 -0
- package/src/cli/start.ts +106 -0
- package/src/cli/upgrade.ts +105 -0
- package/src/cli/vault-cmd.ts +140 -0
- package/src/client/budget-entry.ts +34 -0
- package/src/client/budget.test.ts +17 -0
- package/src/client/create.test.ts +159 -0
- package/src/client/create.ts +152 -0
- package/src/client/errors.ts +60 -0
- package/src/client/index.ts +35 -0
- package/src/client/notes-contract.test.ts +289 -0
- package/src/client/transport.test.ts +123 -0
- package/src/client/transport.ts +254 -0
- package/src/client/types.ts +282 -0
- package/src/compiler/aot.test.ts +248 -0
- package/src/compiler/aot.ts +182 -0
- package/src/compiler/differential.test.ts +241 -0
- package/src/compiler/dynamic.ts +54 -0
- package/src/compiler/effects-infer.ts +541 -0
- package/src/compiler/emit.ts +40 -0
- package/src/compiler/extract.test.ts +177 -0
- package/src/compiler/extract.ts +1368 -0
- package/src/compiler/fixtures/raw/raw-unannotated.ts +28 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +24 -0
- package/src/compiler/fixtures/skyport/src/ai.ts +15 -0
- package/src/compiler/fixtures/skyport/src/app.ts +10 -0
- package/src/compiler/fixtures/skyport/src/channels.ts +6 -0
- package/src/compiler/fixtures/skyport/src/core.ts +3 -0
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +57 -0
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +11 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +14 -0
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +20 -0
- package/src/compiler/fixtures/skyport/src/gates.ts +14 -0
- package/src/compiler/fixtures/skyport/src/journeys.ts +9 -0
- package/src/compiler/fixtures/skyport/src/schema.ts +3 -0
- package/src/compiler/fixtures/skyport/src/vault.ts +6 -0
- package/src/compiler/fixtures/skyport.expected.json +241 -0
- package/src/compiler/fixtures/triggers/five-triggers.ts +69 -0
- package/src/compiler/http-parse.ts +214 -0
- package/src/compiler/index.ts +67 -0
- package/src/compiler/response.ts +89 -0
- package/src/compiler/sucrose.ts +238 -0
- package/src/config/index.ts +167 -0
- package/src/console/budget.test.ts +73 -0
- package/src/console/index.ts +30 -0
- package/src/console/server/access.test.ts +370 -0
- package/src/console/server/access.ts +777 -0
- package/src/console/server/ai.test.ts +172 -0
- package/src/console/server/ai.ts +632 -0
- package/src/console/server/app.ts +294 -0
- package/src/console/server/auth-rate.test.ts +81 -0
- package/src/console/server/auth-rate.ts +72 -0
- package/src/console/server/bind.ts +25 -0
- package/src/console/server/channels.test.ts +179 -0
- package/src/console/server/channels.ts +469 -0
- package/src/console/server/claim.test.ts +66 -0
- package/src/console/server/claim.ts +134 -0
- package/src/console/server/clock.test.ts +239 -0
- package/src/console/server/clock.ts +390 -0
- package/src/console/server/console.test.ts +338 -0
- package/src/console/server/diff.test.ts +299 -0
- package/src/console/server/diff.ts +352 -0
- package/src/console/server/dry-run.audit.test.ts +118 -0
- package/src/console/server/flows-invoke.test.ts +148 -0
- package/src/console/server/flows.ts +3633 -0
- package/src/console/server/gates.test.ts +360 -0
- package/src/console/server/gates.ts +869 -0
- package/src/console/server/index.ts +141 -0
- package/src/console/server/live.test.ts +30 -0
- package/src/console/server/live.ts +109 -0
- package/src/console/server/plugin.ts +50 -0
- package/src/console/server/plugins.test.ts +131 -0
- package/src/console/server/plugins.ts +429 -0
- package/src/console/server/runs-pii.ts +98 -0
- package/src/console/server/security-headers.ts +97 -0
- package/src/console/server/security.gate.test.ts +286 -0
- package/src/console/server/serve.ts +319 -0
- package/src/console/server/signals.test.ts +88 -0
- package/src/console/server/signals.ts +282 -0
- package/src/console/server/state.ts +982 -0
- package/src/console/server/store.test.ts +233 -0
- package/src/console/server/store.ts +861 -0
- package/src/console/server/structural.ts +95 -0
- package/src/console/server/vault.test.ts +251 -0
- package/src/console/server/vault.ts +430 -0
- package/src/console/ui/access/AccessA11yView.tsx +237 -0
- package/src/console/ui/access/a11y.test.tsx +122 -0
- package/src/console/ui/access/acknowledgement.test.ts +13 -0
- package/src/console/ui/access/acknowledgement.ts +35 -0
- package/src/console/ui/access/blast-radius.test.ts +26 -0
- package/src/console/ui/access/blast-radius.ts +40 -0
- package/src/console/ui/access/confirmation.test.ts +38 -0
- package/src/console/ui/access/confirmation.ts +36 -0
- package/src/console/ui/access/fixture.ts +203 -0
- package/src/console/ui/access/grantable.test.ts +17 -0
- package/src/console/ui/access/grantable.ts +36 -0
- package/src/console/ui/access/hygiene.test.ts +12 -0
- package/src/console/ui/access/hygiene.ts +39 -0
- package/src/console/ui/access/index.ts +54 -0
- package/src/console/ui/access/provenance.test.ts +12 -0
- package/src/console/ui/access/provenance.ts +29 -0
- package/src/console/ui/access/search.test.ts +17 -0
- package/src/console/ui/access/search.ts +72 -0
- package/src/console/ui/access/types.ts +111 -0
- package/src/console/ui/ai/AiA11yView.tsx +265 -0
- package/src/console/ui/ai/a11y.test.tsx +99 -0
- package/src/console/ui/ai/findings.ts +41 -0
- package/src/console/ui/ai/fixture.ts +182 -0
- package/src/console/ui/ai/format.ts +90 -0
- package/src/console/ui/ai/group.test.ts +37 -0
- package/src/console/ui/ai/group.ts +88 -0
- package/src/console/ui/ai/index.ts +66 -0
- package/src/console/ui/ai/promotion.test.ts +77 -0
- package/src/console/ui/ai/promotion.ts +150 -0
- package/src/console/ui/ai/search.test.ts +40 -0
- package/src/console/ui/ai/search.ts +89 -0
- package/src/console/ui/ai/types.ts +139 -0
- package/src/console/ui/architecture/ArchitectureA11yView.tsx +163 -0
- package/src/console/ui/architecture/a11y.test.tsx +97 -0
- package/src/console/ui/architecture/boundary.ts +42 -0
- package/src/console/ui/architecture/declared.ts +165 -0
- package/src/console/ui/architecture/fixture.ts +74 -0
- package/src/console/ui/architecture/index.ts +84 -0
- package/src/console/ui/architecture/layers.ts +64 -0
- package/src/console/ui/architecture/layout.ts +68 -0
- package/src/console/ui/architecture/pathologies.ts +260 -0
- package/src/console/ui/architecture/search.ts +145 -0
- package/src/console/ui/architecture/traffic.ts +102 -0
- package/src/console/ui/architecture/types.ts +144 -0
- package/src/console/ui/architecture/view.test.ts +165 -0
- package/src/console/ui/architecture/view.ts +516 -0
- package/src/console/ui/channels/ChannelsA11yView.tsx +180 -0
- package/src/console/ui/channels/a11y.test.tsx +96 -0
- package/src/console/ui/channels/confirmation.test.ts +37 -0
- package/src/console/ui/channels/confirmation.ts +27 -0
- package/src/console/ui/channels/fallback.test.ts +15 -0
- package/src/console/ui/channels/fallback.ts +20 -0
- package/src/console/ui/channels/findings.ts +38 -0
- package/src/console/ui/channels/fixture.ts +134 -0
- package/src/console/ui/channels/group.ts +25 -0
- package/src/console/ui/channels/index.ts +57 -0
- package/src/console/ui/channels/locale.test.ts +17 -0
- package/src/console/ui/channels/locale.ts +31 -0
- package/src/console/ui/channels/mask.test.ts +14 -0
- package/src/console/ui/channels/mask.ts +12 -0
- package/src/console/ui/channels/search.ts +58 -0
- package/src/console/ui/channels/taxonomy.test.ts +17 -0
- package/src/console/ui/channels/taxonomy.ts +49 -0
- package/src/console/ui/channels/types.ts +117 -0
- package/src/console/ui/clock/ClockA11yView.tsx +182 -0
- package/src/console/ui/clock/a11y.test.tsx +99 -0
- package/src/console/ui/clock/confirmation.test.ts +32 -0
- package/src/console/ui/clock/confirmation.ts +34 -0
- package/src/console/ui/clock/findings.ts +40 -0
- package/src/console/ui/clock/fixture.ts +111 -0
- package/src/console/ui/clock/health.test.ts +25 -0
- package/src/console/ui/clock/health.ts +61 -0
- package/src/console/ui/clock/index.ts +49 -0
- package/src/console/ui/clock/search.test.ts +39 -0
- package/src/console/ui/clock/search.ts +76 -0
- package/src/console/ui/clock/timeline.test.ts +44 -0
- package/src/console/ui/clock/timeline.ts +41 -0
- package/src/console/ui/clock/types.ts +78 -0
- package/src/console/ui/clock/waiting-on.test.ts +34 -0
- package/src/console/ui/clock/waiting-on.ts +83 -0
- package/src/console/ui/confirmation.unify.test.ts +92 -0
- package/src/console/ui/diff/DiffA11yView.tsx +85 -0
- package/src/console/ui/diff/a11y.test.tsx +99 -0
- package/src/console/ui/diff/fixture.ts +85 -0
- package/src/console/ui/diff/group.test.ts +58 -0
- package/src/console/ui/diff/group.ts +97 -0
- package/src/console/ui/diff/index.ts +38 -0
- package/src/console/ui/diff/search.test.ts +32 -0
- package/src/console/ui/diff/search.ts +75 -0
- package/src/console/ui/diff/summary.ts +74 -0
- package/src/console/ui/diff/types.ts +48 -0
- package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
- package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
- package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
- package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
- package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
- package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
- package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
- package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
- package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
- package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
- package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
- package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
- package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
- package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
- package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
- package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
- package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
- package/src/console/ui/dist/index.html +14 -0
- package/src/console/ui/flows/FlowsA11yView.tsx +155 -0
- package/src/console/ui/flows/a11y.test.tsx +105 -0
- package/src/console/ui/flows/buffer.test.ts +91 -0
- package/src/console/ui/flows/buffer.ts +118 -0
- package/src/console/ui/flows/confirmation.ts +131 -0
- package/src/console/ui/flows/contract.test.ts +89 -0
- package/src/console/ui/flows/contract.ts +353 -0
- package/src/console/ui/flows/fixture.ts +81 -0
- package/src/console/ui/flows/graph.test.ts +101 -0
- package/src/console/ui/flows/graph.ts +549 -0
- package/src/console/ui/flows/index.ts +87 -0
- package/src/console/ui/flows/save-as-test.test.ts +47 -0
- package/src/console/ui/flows/save-as-test.ts +99 -0
- package/src/console/ui/flows/search.ts +255 -0
- package/src/console/ui/flows/tiers.ts +140 -0
- package/src/console/ui/gates/GatesA11yView.tsx +192 -0
- package/src/console/ui/gates/a11y.test.tsx +116 -0
- package/src/console/ui/gates/audit.test.ts +40 -0
- package/src/console/ui/gates/audit.ts +63 -0
- package/src/console/ui/gates/denial.test.ts +36 -0
- package/src/console/ui/gates/denial.ts +51 -0
- package/src/console/ui/gates/findings.ts +19 -0
- package/src/console/ui/gates/fixture.ts +189 -0
- package/src/console/ui/gates/group.test.ts +30 -0
- package/src/console/ui/gates/group.ts +102 -0
- package/src/console/ui/gates/index.ts +47 -0
- package/src/console/ui/gates/search.test.ts +49 -0
- package/src/console/ui/gates/search.ts +115 -0
- package/src/console/ui/gates/types.ts +121 -0
- package/src/console/ui/overview/OverviewA11yView.tsx +152 -0
- package/src/console/ui/overview/a11y.test.tsx +113 -0
- package/src/console/ui/overview/busiest.ts +26 -0
- package/src/console/ui/overview/compose.ts +133 -0
- package/src/console/ui/overview/cost.test.ts +39 -0
- package/src/console/ui/overview/cost.ts +138 -0
- package/src/console/ui/overview/fixture.ts +161 -0
- package/src/console/ui/overview/golden.ts +57 -0
- package/src/console/ui/overview/index.ts +59 -0
- package/src/console/ui/overview/rank.test.ts +136 -0
- package/src/console/ui/overview/rank.ts +210 -0
- package/src/console/ui/overview/slo.test.ts +73 -0
- package/src/console/ui/overview/slo.ts +229 -0
- package/src/console/ui/overview/types.ts +121 -0
- package/src/console/ui/overview/verdict.ts +131 -0
- package/src/console/ui/plugins/PluginsA11yView.tsx +227 -0
- package/src/console/ui/plugins/a11y.test.tsx +97 -0
- package/src/console/ui/plugins/command.test.ts +22 -0
- package/src/console/ui/plugins/command.ts +27 -0
- package/src/console/ui/plugins/findings.ts +39 -0
- package/src/console/ui/plugins/fixture.ts +233 -0
- package/src/console/ui/plugins/group.test.ts +38 -0
- package/src/console/ui/plugins/group.ts +67 -0
- package/src/console/ui/plugins/index.ts +40 -0
- package/src/console/ui/plugins/search.ts +67 -0
- package/src/console/ui/plugins/types.ts +115 -0
- package/src/console/ui/runs/RunsA11yView.tsx +267 -0
- package/src/console/ui/runs/a11y.test.tsx +99 -0
- package/src/console/ui/runs/explain.test.ts +23 -0
- package/src/console/ui/runs/explain.ts +32 -0
- package/src/console/ui/runs/fixture.ts +189 -0
- package/src/console/ui/runs/group.test.ts +24 -0
- package/src/console/ui/runs/group.ts +85 -0
- package/src/console/ui/runs/histogram.test.ts +31 -0
- package/src/console/ui/runs/histogram.ts +85 -0
- package/src/console/ui/runs/index.ts +75 -0
- package/src/console/ui/runs/project.ts +122 -0
- package/src/console/ui/runs/query.test.ts +50 -0
- package/src/console/ui/runs/query.ts +283 -0
- package/src/console/ui/runs/search.test.ts +45 -0
- package/src/console/ui/runs/search.ts +141 -0
- package/src/console/ui/runs/trace-link.test.ts +30 -0
- package/src/console/ui/runs/trace-link.ts +99 -0
- package/src/console/ui/runs/types.ts +137 -0
- package/src/console/ui/shell/App.tsx +56 -0
- package/src/console/ui/shell/client.ts +1715 -0
- package/src/console/ui/shell/components/ui.tsx +110 -0
- package/src/console/ui/shell/index.html +12 -0
- package/src/console/ui/shell/layout/Shell.tsx +95 -0
- package/src/console/ui/shell/main.tsx +281 -0
- package/src/console/ui/shell/panels/Access.tsx +5 -0
- package/src/console/ui/shell/panels/Ai.tsx +5 -0
- package/src/console/ui/shell/panels/Architecture.tsx +5 -0
- package/src/console/ui/shell/panels/Channels.tsx +5 -0
- package/src/console/ui/shell/panels/Clock.tsx +5 -0
- package/src/console/ui/shell/panels/Diff.tsx +5 -0
- package/src/console/ui/shell/panels/Flows.tsx +5 -0
- package/src/console/ui/shell/panels/Gates.tsx +5 -0
- package/src/console/ui/shell/panels/Overview.tsx +6 -0
- package/src/console/ui/shell/panels/Plugins.tsx +5 -0
- package/src/console/ui/shell/panels/Runs.tsx +5 -0
- package/src/console/ui/shell/panels/Signals.tsx +5 -0
- package/src/console/ui/shell/panels/Store.tsx +5 -0
- package/src/console/ui/shell/panels/Traces.tsx +5 -0
- package/src/console/ui/shell/panels/Vault.tsx +5 -0
- package/src/console/ui/shell/panels/access/AccessPanel.tsx +813 -0
- package/src/console/ui/shell/panels/ai/AiPanel.tsx +563 -0
- package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +377 -0
- package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +454 -0
- package/src/console/ui/shell/panels/clock/ClockPanel.tsx +479 -0
- package/src/console/ui/shell/panels/diff/DiffPanel.tsx +275 -0
- package/src/console/ui/shell/panels/flows/ContractEditor.tsx +301 -0
- package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +350 -0
- package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +502 -0
- package/src/console/ui/shell/panels/gates/GatesPanel.tsx +482 -0
- package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +428 -0
- package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +413 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +649 -0
- package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +840 -0
- package/src/console/ui/shell/panels/store/StorePanel.tsx +787 -0
- package/src/console/ui/shell/panels/traces/TracesPanel.tsx +539 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +449 -0
- package/src/console/ui/shell/plugin/Sandbox.tsx +29 -0
- package/src/console/ui/shell/setup/Wizard.tsx +189 -0
- package/src/console/ui/shell/styles.css +75 -0
- package/src/console/ui/signals/SignalsA11yView.tsx +276 -0
- package/src/console/ui/signals/a11y.test.tsx +99 -0
- package/src/console/ui/signals/confirmation.test.ts +39 -0
- package/src/console/ui/signals/confirmation.ts +63 -0
- package/src/console/ui/signals/dry-run.test.ts +18 -0
- package/src/console/ui/signals/dry-run.ts +36 -0
- package/src/console/ui/signals/durable.test.ts +22 -0
- package/src/console/ui/signals/durable.ts +41 -0
- package/src/console/ui/signals/findings.ts +33 -0
- package/src/console/ui/signals/fixture.ts +242 -0
- package/src/console/ui/signals/group.test.ts +32 -0
- package/src/console/ui/signals/group.ts +49 -0
- package/src/console/ui/signals/index.ts +52 -0
- package/src/console/ui/signals/monitor.test.ts +30 -0
- package/src/console/ui/signals/monitor.ts +79 -0
- package/src/console/ui/signals/schema-form.test.ts +34 -0
- package/src/console/ui/signals/schema-form.ts +112 -0
- package/src/console/ui/signals/search.ts +102 -0
- package/src/console/ui/signals/types.ts +71 -0
- package/src/console/ui/store/StoreA11yView.tsx +199 -0
- package/src/console/ui/store/a11y.test.tsx +99 -0
- package/src/console/ui/store/cache-view.test.ts +13 -0
- package/src/console/ui/store/cache-view.ts +36 -0
- package/src/console/ui/store/confirmation.test.ts +38 -0
- package/src/console/ui/store/confirmation.ts +55 -0
- package/src/console/ui/store/dry-run.test.ts +15 -0
- package/src/console/ui/store/dry-run.ts +29 -0
- package/src/console/ui/store/fixture.ts +163 -0
- package/src/console/ui/store/group.test.ts +21 -0
- package/src/console/ui/store/group.ts +41 -0
- package/src/console/ui/store/index.ts +40 -0
- package/src/console/ui/store/search.ts +78 -0
- package/src/console/ui/store/types.ts +65 -0
- package/src/console/ui/store/will-not-fire.test.ts +19 -0
- package/src/console/ui/store/will-not-fire.ts +40 -0
- package/src/console/ui/traces/TracesA11yView.tsx +200 -0
- package/src/console/ui/traces/a11y.test.tsx +97 -0
- package/src/console/ui/traces/chain.test.ts +61 -0
- package/src/console/ui/traces/chain.ts +144 -0
- package/src/console/ui/traces/critical-path.test.ts +24 -0
- package/src/console/ui/traces/critical-path.ts +59 -0
- package/src/console/ui/traces/filter.test.ts +78 -0
- package/src/console/ui/traces/filter.ts +112 -0
- package/src/console/ui/traces/fixture.ts +126 -0
- package/src/console/ui/traces/fold.test.ts +67 -0
- package/src/console/ui/traces/fold.ts +170 -0
- package/src/console/ui/traces/index.ts +64 -0
- package/src/console/ui/traces/mini.ts +65 -0
- package/src/console/ui/traces/replay.test.ts +24 -0
- package/src/console/ui/traces/replay.ts +35 -0
- package/src/console/ui/traces/sampling.test.ts +26 -0
- package/src/console/ui/traces/sampling.ts +66 -0
- package/src/console/ui/traces/search.ts +148 -0
- package/src/console/ui/traces/tier.ts +72 -0
- package/src/console/ui/traces/types.ts +125 -0
- package/src/console/ui/tsconfig.json +20 -0
- package/src/console/ui/vault/VaultA11yView.tsx +177 -0
- package/src/console/ui/vault/a11y.test.tsx +97 -0
- package/src/console/ui/vault/blast-radius.test.ts +35 -0
- package/src/console/ui/vault/blast-radius.ts +55 -0
- package/src/console/ui/vault/confirmation.test.ts +41 -0
- package/src/console/ui/vault/confirmation.ts +39 -0
- package/src/console/ui/vault/dormant.test.ts +37 -0
- package/src/console/ui/vault/dormant.ts +28 -0
- package/src/console/ui/vault/export-safe.test.ts +30 -0
- package/src/console/ui/vault/export-safe.ts +65 -0
- package/src/console/ui/vault/fixture.ts +75 -0
- package/src/console/ui/vault/group.test.ts +25 -0
- package/src/console/ui/vault/group.ts +50 -0
- package/src/console/ui/vault/index.ts +45 -0
- package/src/console/ui/vault/search.ts +62 -0
- package/src/console/ui/vault/types.ts +57 -0
- package/src/console/ui/vite.config.ts +84 -0
- package/src/console/xss.gate.test.ts +118 -0
- package/src/docker/compose.ts +226 -0
- package/src/docker/credentials.ts +25 -0
- package/src/docker/derive.ts +83 -0
- package/src/docker/docker.test.ts +185 -0
- package/src/docker/dockerfile.integration.test.ts +170 -0
- package/src/docker/dockerfile.ts +39 -0
- package/src/docker/helpers.ts +103 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/pin.ts +90 -0
- package/src/docker/recipes/index.ts +33 -0
- package/src/docker/recipes/postgres.ts +18 -0
- package/src/docker/recipes/redis.ts +18 -0
- package/src/docker/stack.integration.test.ts +140 -0
- package/src/docker/stack.ts +62 -0
- package/src/docker/types.ts +134 -0
- package/src/drivers/ai-anthropic.ts +128 -0
- package/src/drivers/ai-mock.ts +82 -0
- package/src/drivers/ai-openai-compatible.ts +139 -0
- package/src/drivers/ai-providers.test.ts +163 -0
- package/src/drivers/ai-types.ts +83 -0
- package/src/drivers/bun-native-completeness.test.ts +155 -0
- package/src/drivers/channel-console.ts +118 -0
- package/src/drivers/channel-fcm.ts +96 -0
- package/src/drivers/channel-resend.ts +27 -0
- package/src/drivers/channel-smtp.ts +33 -0
- package/src/drivers/channel-types.ts +160 -0
- package/src/drivers/channel-unifonic.ts +79 -0
- package/src/drivers/channel-wa-cloud.ts +84 -0
- package/src/drivers/channel-webpush.ts +317 -0
- package/src/drivers/conformance.test.ts +89 -0
- package/src/drivers/conformance.ts +162 -0
- package/src/drivers/fs.ts +73 -0
- package/src/drivers/index.ts +194 -0
- package/src/drivers/kv-lua.ts +222 -0
- package/src/drivers/memory.ts +472 -0
- package/src/drivers/pgvector.ts +130 -0
- package/src/drivers/postgres.ts +221 -0
- package/src/drivers/redis.ts +254 -0
- package/src/drivers/s3.ts +129 -0
- package/src/drivers/signal-engine.ts +690 -0
- package/src/drivers/signal-memory.ts +27 -0
- package/src/drivers/signal-nats.ts +131 -0
- package/src/drivers/signal-postgres.ts +1089 -0
- package/src/drivers/signal-redis.ts +275 -0
- package/src/drivers/signal-types.ts +328 -0
- package/src/drivers/sqlite.ts +53 -0
- package/src/drivers/types.ts +334 -0
- package/src/drivers/vault-dotenv-parse.ts +56 -0
- package/src/drivers/vault-env.ts +101 -0
- package/src/drivers/vault-infisical.ts +61 -0
- package/src/drivers/vault-managed.ts +42 -0
- package/src/drivers/vault-memory.ts +38 -0
- package/src/drivers/vault-openbao.ts +80 -0
- package/src/drivers/vault-sops.ts +252 -0
- package/src/drivers/vault-types.ts +84 -0
- package/src/elements/ai/declare.ts +206 -0
- package/src/elements/ai/eval.ts +113 -0
- package/src/elements/ai/pii.ts +108 -0
- package/src/elements/ai/runtime.ts +540 -0
- package/src/elements/ai/schema.ts +194 -0
- package/src/elements/ai.test.ts +393 -0
- package/src/elements/ai.ts +56 -0
- package/src/elements/channel/consent.ts +81 -0
- package/src/elements/channel/costs.test.ts +62 -0
- package/src/elements/channel/costs.ts +130 -0
- package/src/elements/channel/declare.ts +141 -0
- package/src/elements/channel/email-auth.test.ts +47 -0
- package/src/elements/channel/email-auth.ts +109 -0
- package/src/elements/channel/locale.ts +97 -0
- package/src/elements/channel/mask.test.ts +19 -0
- package/src/elements/channel/mask.ts +60 -0
- package/src/elements/channel/mime.ts +23 -0
- package/src/elements/channel/outcomes.test.ts +40 -0
- package/src/elements/channel/outcomes.ts +126 -0
- package/src/elements/channel/receipts.ts +121 -0
- package/src/elements/channel/runtime.ts +500 -0
- package/src/elements/channel/suppression.ts +136 -0
- package/src/elements/channel.test.ts +232 -0
- package/src/elements/channel.ts +109 -0
- package/src/elements/clock/actions.ts +170 -0
- package/src/elements/clock/declare.ts +58 -0
- package/src/elements/clock/dst.ts +197 -0
- package/src/elements/clock/durable.ts +140 -0
- package/src/elements/clock/duration.ts +29 -0
- package/src/elements/clock/health.test.ts +103 -0
- package/src/elements/clock/health.ts +112 -0
- package/src/elements/clock/leader.ts +127 -0
- package/src/elements/clock/reconcile.ts +241 -0
- package/src/elements/clock/runtime.ts +243 -0
- package/src/elements/clock/schedule.ts +262 -0
- package/src/elements/clock/time-travel.ts +53 -0
- package/src/elements/clock.test.ts +365 -0
- package/src/elements/clock.ts +80 -0
- package/src/elements/gate/declare.ts +134 -0
- package/src/elements/gate/permissions.ts +104 -0
- package/src/elements/gate/runtime.ts +177 -0
- package/src/elements/gate/strategies.ts +388 -0
- package/src/elements/gate.test.ts +145 -0
- package/src/elements/gate.ts +41 -0
- package/src/elements/index.ts +169 -0
- package/src/elements/signal/chaos-child.ts +47 -0
- package/src/elements/signal/declare.ts +77 -0
- package/src/elements/signal/dry-run-replay.test.ts +95 -0
- package/src/elements/signal/dry-run-write-isolation.test.ts +94 -0
- package/src/elements/signal/reconcile.test.ts +23 -0
- package/src/elements/signal/reconcile.ts +122 -0
- package/src/elements/signal/runtime.ts +94 -0
- package/src/elements/signal.test.ts +457 -0
- package/src/elements/signal.ts +26 -0
- package/src/elements/store/cache.ts +197 -0
- package/src/elements/store/classify.ts +125 -0
- package/src/elements/store/declare.ts +200 -0
- package/src/elements/store/files-policy.test.ts +31 -0
- package/src/elements/store/files-policy.ts +70 -0
- package/src/elements/store/replica.ts +68 -0
- package/src/elements/store/runtime.ts +393 -0
- package/src/elements/store/sql-condition.ts +205 -0
- package/src/elements/store/sql-session.ts +579 -0
- package/src/elements/store/table.ts +282 -0
- package/src/elements/store.test.ts +370 -0
- package/src/elements/store.ts +99 -0
- package/src/elements/vault/declare.ts +143 -0
- package/src/elements/vault/fingerprint.ts +30 -0
- package/src/elements/vault/redact.ts +52 -0
- package/src/elements/vault/runtime.ts +461 -0
- package/src/elements/vault.test.ts +296 -0
- package/src/elements/vault.ts +44 -0
- package/src/index.ts +183 -0
- package/src/kernel/adopt-routes.ts +194 -0
- package/src/kernel/app.ts +1068 -0
- package/src/kernel/auth-resolve.ts +93 -0
- package/src/kernel/boot.test.ts +173 -0
- package/src/kernel/boot.ts +439 -0
- package/src/kernel/budget-entry.ts +55 -0
- package/src/kernel/budget.test.ts +19 -0
- package/src/kernel/call.test.ts +83 -0
- package/src/kernel/capability.ts +113 -0
- package/src/kernel/dry-run.test.ts +64 -0
- package/src/kernel/dry-run.ts +171 -0
- package/src/kernel/effects.test.ts +89 -0
- package/src/kernel/effects.ts +140 -0
- package/src/kernel/errors.registry.test.ts +39 -0
- package/src/kernel/errors.ts +225 -0
- package/src/kernel/flow.test.ts +154 -0
- package/src/kernel/flow.ts +290 -0
- package/src/kernel/fx.test.ts +245 -0
- package/src/kernel/fx.ts +996 -0
- package/src/kernel/hook-timing.test.ts +72 -0
- package/src/kernel/hook-timing.ts +190 -0
- package/src/kernel/hooks.test.ts +181 -0
- package/src/kernel/hooks.ts +275 -0
- package/src/kernel/index.ts +278 -0
- package/src/kernel/journal.ts +405 -0
- package/src/kernel/journey.ts +75 -0
- package/src/kernel/on.ts +74 -0
- package/src/kernel/pipeline.test.ts +349 -0
- package/src/kernel/pipeline.ts +245 -0
- package/src/kernel/plug.ts +82 -0
- package/src/kernel/plugin/capabilities.test.ts +84 -0
- package/src/kernel/plugin/conflicts.test.ts +106 -0
- package/src/kernel/plugin/decorate.test.ts +111 -0
- package/src/kernel/plugin/scoping.test.ts +138 -0
- package/src/kernel/plugin.ts +485 -0
- package/src/kernel/rate-limit.ts +25 -0
- package/src/kernel/registry.ts +398 -0
- package/src/kernel/router.test.ts +114 -0
- package/src/kernel/router.ts +556 -0
- package/src/kernel/routing-budget.test.ts +19 -0
- package/src/kernel/run-telemetry.ts +70 -0
- package/src/kernel/triggers.ts +324 -0
- package/src/kernel/unit.ts +88 -0
- package/src/manifest/diff.test.ts +866 -0
- package/src/manifest/diff.ts +1670 -0
- package/src/manifest/fixtures/base.manifest.json +112 -0
- package/src/manifest/fixtures/skyport.excerpt.json +51 -0
- package/src/manifest/fixtures/skyport.manifest.json +153 -0
- package/src/manifest/index.ts +27 -0
- package/src/manifest/types.ts +358 -0
- package/src/manifest/undeclared.test.ts +82 -0
- package/src/manifest/undeclared.ts +65 -0
- package/src/manifest/validate.test.ts +105 -0
- package/src/manifest/validate.ts +174 -0
- package/src/mcp/authorization.ts +340 -0
- package/src/mcp/confirmation.ts +174 -0
- package/src/mcp/data.ts +90 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/injection.gate.test.ts +176 -0
- package/src/mcp/mcp.test.ts +348 -0
- package/src/mcp/protocol.ts +172 -0
- package/src/mcp/server.ts +283 -0
- package/src/mcp/session.ts +139 -0
- package/src/mcp/tools.ts +391 -0
- package/src/plugins/catalogue.test.ts +48 -0
- package/src/plugins/catalogue.ts +113 -0
- package/src/plugins/node-import-scan.test.ts +28 -0
- package/src/plugins/node-import-scan.ts +98 -0
- package/src/plugins/supply-chain.test.ts +48 -0
- package/src/plugins/supply-chain.ts +460 -0
- package/src/release/index.ts +30 -0
- package/src/release/limits.ts +20 -0
- package/src/release/measure.ts +443 -0
- package/src/release/publish.ts +56 -0
- package/src/runs/bench.test.ts +93 -0
- package/src/runs/collect.ts +144 -0
- package/src/runs/drivers/clickhouse.ts +108 -0
- package/src/runs/drivers/files.ts +201 -0
- package/src/runs/drivers/memory.ts +94 -0
- package/src/runs/drivers/postgres.ts +110 -0
- package/src/runs/duckdb.ts +99 -0
- package/src/runs/index.ts +82 -0
- package/src/runs/outlier.ts +180 -0
- package/src/runs/parquet.ts +210 -0
- package/src/runs/privacy.ts +49 -0
- package/src/runs/runs.test.ts +324 -0
- package/src/runs/runtime.ts +229 -0
- package/src/runs/shred.ts +243 -0
- package/src/runs/telemetry.ts +10 -0
- package/src/runs/types.ts +179 -0
- package/src/runtime/bun.ts +140 -0
- package/src/runtime/cold-start.bench.ts +20 -0
- package/src/runtime/index.ts +46 -0
- package/src/runtime/primitives.ts +242 -0
- package/src/runtime/security.test.ts +170 -0
- package/src/runtime/security.ts +168 -0
- package/src/runtime/serve.test.ts +223 -0
- package/src/runtime/types.ts +217 -0
- package/src/runtime/web-standard.ts +77 -0
- package/src/test/create-test-app.test.ts +161 -0
- package/src/test/create-test-app.ts +533 -0
- package/src/test/index.ts +21 -0
- package/src/test/provisions.integration.test.ts +174 -0
- package/src/upgrade/codemods.test.ts +58 -0
- package/src/upgrade/codemods.ts +83 -0
- package/src/validation/index.ts +23 -0
- package/src/validation/standard-schema.test.ts +96 -0
- package/src/validation/standard-schema.ts +316 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console Channel projection — Manifest + ChannelRuntime (console §9.9).
|
|
3
|
+
*
|
|
4
|
+
* The UI must not recompute delivery state from raw provider webhooks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as dns } from "node:dns";
|
|
8
|
+
import { openConsoleChannel } from "../../drivers/channel-console.ts";
|
|
9
|
+
import {
|
|
10
|
+
createChannelInbox,
|
|
11
|
+
type ChannelInbox,
|
|
12
|
+
type ChannelInboxEntry,
|
|
13
|
+
} from "../../drivers/channel-types.ts";
|
|
14
|
+
import {
|
|
15
|
+
buildOutcomeRows,
|
|
16
|
+
channel as declareChannel,
|
|
17
|
+
createChannelRuntime,
|
|
18
|
+
fallbackWeeklyCostDelta,
|
|
19
|
+
formatAttemptChain,
|
|
20
|
+
isDeliveryOutcomeState,
|
|
21
|
+
isRtlLocale,
|
|
22
|
+
maskRecipient,
|
|
23
|
+
resolveLocale,
|
|
24
|
+
verifyEmailAuth,
|
|
25
|
+
type ChannelRuntime,
|
|
26
|
+
type DeliveryOutcomeState,
|
|
27
|
+
type DeliveryReceipt,
|
|
28
|
+
type DeliveryVerdict,
|
|
29
|
+
type EmailAuthResult,
|
|
30
|
+
type OutcomeRow,
|
|
31
|
+
type SuppressionEntry,
|
|
32
|
+
type TemplateCatalog,
|
|
33
|
+
} from "../../elements/channel.ts";
|
|
34
|
+
import type { Manifest } from "../../manifest/types.ts";
|
|
35
|
+
|
|
36
|
+
export { createChannelInbox, openConsoleChannel };
|
|
37
|
+
|
|
38
|
+
/** Panel face — inbox in dev, deliverability in prod. */
|
|
39
|
+
export type ChannelsFace = "inbox" | "deliverability";
|
|
40
|
+
|
|
41
|
+
/** One Manifest template row. */
|
|
42
|
+
export interface ConsoleChannelTemplate {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly medium: string;
|
|
45
|
+
readonly locales: readonly string[];
|
|
46
|
+
readonly from: string | null;
|
|
47
|
+
readonly schema: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Masked inbox entry for the dev face. */
|
|
51
|
+
export interface ConsoleInboxRow {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly medium: string;
|
|
54
|
+
readonly toMasked: string;
|
|
55
|
+
readonly subject: string | null;
|
|
56
|
+
readonly text: string | null;
|
|
57
|
+
readonly html: string | null;
|
|
58
|
+
readonly template: string | null;
|
|
59
|
+
readonly locale: string | null;
|
|
60
|
+
readonly at: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Masked receipt row. */
|
|
64
|
+
export interface ConsoleReceiptRow {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly template: string;
|
|
67
|
+
readonly toMasked: string;
|
|
68
|
+
readonly medium: string;
|
|
69
|
+
readonly locale: string | null;
|
|
70
|
+
readonly localeChain: readonly string[];
|
|
71
|
+
readonly status: string;
|
|
72
|
+
readonly chain: string;
|
|
73
|
+
readonly messageId: string | null;
|
|
74
|
+
readonly at: number;
|
|
75
|
+
readonly error: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Masked suppression row. */
|
|
79
|
+
export interface ConsoleSuppressionRow {
|
|
80
|
+
readonly subjectMasked: string;
|
|
81
|
+
readonly medium: string;
|
|
82
|
+
readonly reason: "opted-out" | "prior-bounce";
|
|
83
|
+
readonly at: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fallback financial metric for one template (or all). */
|
|
87
|
+
export interface ConsoleFallbackMetric {
|
|
88
|
+
readonly template: string | null;
|
|
89
|
+
readonly chainExample: string;
|
|
90
|
+
readonly fallbackRate: number;
|
|
91
|
+
readonly fallbackCount: number;
|
|
92
|
+
readonly totalCount: number;
|
|
93
|
+
readonly weeklyDeltaUsd: number;
|
|
94
|
+
readonly primaryMedium: string;
|
|
95
|
+
readonly fallbackMedium: string;
|
|
96
|
+
readonly summary: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Full Channels panel projection. */
|
|
100
|
+
export interface ConsoleChannelsList {
|
|
101
|
+
readonly face: ChannelsFace;
|
|
102
|
+
readonly production: boolean;
|
|
103
|
+
readonly templates: readonly ConsoleChannelTemplate[];
|
|
104
|
+
readonly outcomes: readonly OutcomeRow[];
|
|
105
|
+
readonly fallback: ConsoleFallbackMetric;
|
|
106
|
+
readonly inbox: readonly ConsoleInboxRow[];
|
|
107
|
+
readonly receipts: readonly ConsoleReceiptRow[];
|
|
108
|
+
readonly suppression: readonly ConsoleSuppressionRow[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Preview of a template body for a locale. */
|
|
112
|
+
export interface ConsoleChannelPreview {
|
|
113
|
+
readonly template: string;
|
|
114
|
+
readonly locale: string;
|
|
115
|
+
readonly localeChain: readonly string[];
|
|
116
|
+
readonly dir: "ltr" | "rtl";
|
|
117
|
+
readonly subject: string | null;
|
|
118
|
+
readonly text: string | null;
|
|
119
|
+
readonly html: string | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Options for projecting the channels list. */
|
|
123
|
+
export interface ProjectChannelsOptions {
|
|
124
|
+
readonly manifest: Manifest | null;
|
|
125
|
+
readonly runtime: ChannelRuntime | null;
|
|
126
|
+
readonly inbox: ChannelInbox | null;
|
|
127
|
+
readonly production: boolean;
|
|
128
|
+
readonly now: () => number;
|
|
129
|
+
readonly catalog?: TemplateCatalog;
|
|
130
|
+
readonly revealPii?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Project operator-plane Channels panel data from Manifest + runtime.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Manifest, runtime, inbox, env
|
|
137
|
+
*/
|
|
138
|
+
export function projectChannelsList(
|
|
139
|
+
options: ProjectChannelsOptions,
|
|
140
|
+
): ConsoleChannelsList {
|
|
141
|
+
const face: ChannelsFace = options.production ? "deliverability" : "inbox";
|
|
142
|
+
const templates = projectTemplates(options.manifest);
|
|
143
|
+
const receipts = options.runtime?.receipts.all() ?? [];
|
|
144
|
+
const counts: Partial<Record<DeliveryOutcomeState, number>> = {};
|
|
145
|
+
for (const r of receipts) {
|
|
146
|
+
if (isDeliveryOutcomeState(r.status)) {
|
|
147
|
+
counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const outcomes = buildOutcomeRows(counts);
|
|
151
|
+
const weekStart = options.now() - 7 * 86_400_000;
|
|
152
|
+
const delta = fallbackWeeklyCostDelta(receipts, {
|
|
153
|
+
weekStartMs: weekStart,
|
|
154
|
+
now: options.now(),
|
|
155
|
+
costs: options.runtime?.costs,
|
|
156
|
+
});
|
|
157
|
+
const chainExample =
|
|
158
|
+
receipts
|
|
159
|
+
.filter((r) => r.status === "fallback" && r.attempts.length > 0)
|
|
160
|
+
.map((r) => formatAttemptChain(r.attempts))
|
|
161
|
+
.find(Boolean) ??
|
|
162
|
+
(delta.fallbackCount > 0
|
|
163
|
+
? `${delta.primaryMedium} failed → ${delta.fallbackMedium} succeeded`
|
|
164
|
+
: "");
|
|
165
|
+
|
|
166
|
+
const fallback: ConsoleFallbackMetric = {
|
|
167
|
+
template: null,
|
|
168
|
+
chainExample,
|
|
169
|
+
fallbackRate: delta.fallbackRate,
|
|
170
|
+
fallbackCount: delta.fallbackCount,
|
|
171
|
+
totalCount: delta.totalCount,
|
|
172
|
+
weeklyDeltaUsd: delta.weeklyDeltaUsd,
|
|
173
|
+
primaryMedium: delta.primaryMedium,
|
|
174
|
+
fallbackMedium: delta.fallbackMedium,
|
|
175
|
+
summary: formatFallbackSummary(delta),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const reveal = options.revealPii === true;
|
|
179
|
+
return {
|
|
180
|
+
face,
|
|
181
|
+
production: options.production,
|
|
182
|
+
templates,
|
|
183
|
+
outcomes,
|
|
184
|
+
fallback,
|
|
185
|
+
inbox: projectInbox(options.inbox?.entries ?? [], reveal),
|
|
186
|
+
receipts: projectReceipts(receipts, reveal),
|
|
187
|
+
suppression: projectSuppression(
|
|
188
|
+
options.runtime?.suppression.list() ?? [],
|
|
189
|
+
reveal,
|
|
190
|
+
),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format the weekly fallback cost line for the panel.
|
|
196
|
+
*
|
|
197
|
+
* @param delta - Weekly cost projection
|
|
198
|
+
*/
|
|
199
|
+
export function formatFallbackSummary(delta: {
|
|
200
|
+
readonly fallbackRate: number;
|
|
201
|
+
readonly weeklyDeltaUsd: number;
|
|
202
|
+
readonly primaryMedium: string;
|
|
203
|
+
}): string {
|
|
204
|
+
const pct = Math.round(delta.fallbackRate * 100);
|
|
205
|
+
const dollars = Math.abs(delta.weeklyDeltaUsd);
|
|
206
|
+
const signed =
|
|
207
|
+
delta.weeklyDeltaUsd >= 0
|
|
208
|
+
? `$${dollars.toFixed(dollars >= 1 ? 0 : 2)}`
|
|
209
|
+
: `-$${dollars.toFixed(dollars >= 1 ? 0 : 2)}`;
|
|
210
|
+
return `${pct}% fell back · ${signed} / week above ${delta.primaryMedium}-only`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Project Manifest channel templates.
|
|
215
|
+
*
|
|
216
|
+
* @param manifest - Manifest snapshot
|
|
217
|
+
*/
|
|
218
|
+
export function projectTemplates(
|
|
219
|
+
manifest: Manifest | null,
|
|
220
|
+
): readonly ConsoleChannelTemplate[] {
|
|
221
|
+
return Object.entries(manifest?.channels ?? {})
|
|
222
|
+
.map(([name, c]) => ({
|
|
223
|
+
name,
|
|
224
|
+
medium: c.medium ?? "email",
|
|
225
|
+
locales: c.locales ?? [],
|
|
226
|
+
from: c.from ?? null,
|
|
227
|
+
schema: c.schema ?? null,
|
|
228
|
+
}))
|
|
229
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Preview a template body with locale chain + RTL dir.
|
|
234
|
+
*
|
|
235
|
+
* @param options - Runtime + template + locale inputs
|
|
236
|
+
*/
|
|
237
|
+
export function previewChannelTemplate(options: {
|
|
238
|
+
readonly runtime: ChannelRuntime | null;
|
|
239
|
+
readonly manifest: Manifest | null;
|
|
240
|
+
readonly catalog?: TemplateCatalog;
|
|
241
|
+
readonly template: string;
|
|
242
|
+
readonly locale?: string;
|
|
243
|
+
readonly profileLocale?: string;
|
|
244
|
+
readonly acceptLanguage?: string;
|
|
245
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
246
|
+
readonly defaultLocale?: string;
|
|
247
|
+
}): ConsoleChannelPreview {
|
|
248
|
+
const defaultLocale = options.defaultLocale ?? "en";
|
|
249
|
+
const resolved = resolveLocale({
|
|
250
|
+
locale: options.locale,
|
|
251
|
+
profileLocale: options.profileLocale,
|
|
252
|
+
acceptLanguage: options.acceptLanguage,
|
|
253
|
+
defaultLocale,
|
|
254
|
+
});
|
|
255
|
+
const catalog = options.catalog ?? {};
|
|
256
|
+
const byLocale = catalog[options.template];
|
|
257
|
+
const entry =
|
|
258
|
+
byLocale?.[resolved.locale] ??
|
|
259
|
+
byLocale?.[defaultLocale] ??
|
|
260
|
+
byLocale?.en;
|
|
261
|
+
const data = options.data ?? sampleDataFromSchema(
|
|
262
|
+
options.manifest?.channels?.[options.template]?.schema,
|
|
263
|
+
);
|
|
264
|
+
const interpolate = (s: string | undefined): string | null => {
|
|
265
|
+
if (s === undefined) return null;
|
|
266
|
+
return s.replace(/\{\{(\w+)\}\}/g, (_, key: string) =>
|
|
267
|
+
String(data[key] ?? `{{${key}}}`),
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
template: options.template,
|
|
272
|
+
locale: resolved.locale,
|
|
273
|
+
localeChain: [...resolved.chain],
|
|
274
|
+
dir: isRtlLocale(resolved.locale) ? "rtl" : "ltr",
|
|
275
|
+
subject: interpolate(entry?.subject),
|
|
276
|
+
text: interpolate(entry?.text) ?? (entry ? null : JSON.stringify(data)),
|
|
277
|
+
html: interpolate(entry?.html),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Verify SPF/DKIM/DMARC for a template's From domain.
|
|
283
|
+
*
|
|
284
|
+
* @param fromOrDomain - From address or domain
|
|
285
|
+
* @param lookup - Optional DNS TXT lookup (defaults to system DNS)
|
|
286
|
+
*/
|
|
287
|
+
export async function verifyChannelAuth(
|
|
288
|
+
fromOrDomain: string,
|
|
289
|
+
lookup?: (name: string) => Promise<readonly string[]>,
|
|
290
|
+
): Promise<EmailAuthResult> {
|
|
291
|
+
return verifyEmailAuth(fromOrDomain, {
|
|
292
|
+
lookup:
|
|
293
|
+
lookup ??
|
|
294
|
+
(async (name) => {
|
|
295
|
+
try {
|
|
296
|
+
const records = await dns.resolveTxt(name);
|
|
297
|
+
return records.map((parts) => parts.join(""));
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Reveal a cleartext recipient from a receipt / inbox id (audited upstream).
|
|
307
|
+
*
|
|
308
|
+
* @param options - Runtime + inbox + id
|
|
309
|
+
*/
|
|
310
|
+
export function revealChannelRecipient(options: {
|
|
311
|
+
readonly runtime: ChannelRuntime | null;
|
|
312
|
+
readonly inbox: ChannelInbox | null;
|
|
313
|
+
readonly id: string;
|
|
314
|
+
}): { readonly id: string; readonly to: string } | null {
|
|
315
|
+
const receipt = options.runtime?.receipts.all().find((r) => r.id === options.id);
|
|
316
|
+
if (receipt) return { id: receipt.id, to: receipt.to };
|
|
317
|
+
const entry = options.inbox?.entries.find((e) => e.id === options.id);
|
|
318
|
+
if (entry) return { id: entry.id, to: entry.to };
|
|
319
|
+
const suppressed = options.runtime?.suppression
|
|
320
|
+
.list()
|
|
321
|
+
.find((s) => maskRecipient(s.subject) === options.id || s.subject === options.id);
|
|
322
|
+
if (suppressed) return { id: options.id, to: suppressed.subject };
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Send a real test message through the Channel runtime (never dry-run).
|
|
328
|
+
*
|
|
329
|
+
* @param runtime - Channel runtime
|
|
330
|
+
* @param input - Template + recipient
|
|
331
|
+
*/
|
|
332
|
+
export async function sendChannelTest(
|
|
333
|
+
runtime: ChannelRuntime,
|
|
334
|
+
input: {
|
|
335
|
+
readonly template: string;
|
|
336
|
+
readonly to: string;
|
|
337
|
+
readonly locale?: string;
|
|
338
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
339
|
+
},
|
|
340
|
+
): Promise<{
|
|
341
|
+
readonly ok: boolean;
|
|
342
|
+
readonly messageId: string;
|
|
343
|
+
readonly status: string;
|
|
344
|
+
readonly chain: string;
|
|
345
|
+
}> {
|
|
346
|
+
const result = await runtime.send(input.template, {
|
|
347
|
+
to: input.to,
|
|
348
|
+
locale: input.locale,
|
|
349
|
+
data: input.data,
|
|
350
|
+
});
|
|
351
|
+
const receipt =
|
|
352
|
+
runtime.receipts.byMessageId(result.messageId) ??
|
|
353
|
+
runtime.receipts.all().at(-1);
|
|
354
|
+
return {
|
|
355
|
+
ok: result.ok,
|
|
356
|
+
messageId: result.messageId,
|
|
357
|
+
status: receipt?.status ?? (result.ok ? "sent" : "provider-error"),
|
|
358
|
+
chain: formatAttemptChain(result.attempts),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Build a ChannelRuntime + shared inbox from the Manifest for Console.
|
|
364
|
+
*
|
|
365
|
+
* @param manifest - Manifest snapshot
|
|
366
|
+
* @param options - Clock + catalog + production
|
|
367
|
+
*/
|
|
368
|
+
export function createManifestChannelRuntime(
|
|
369
|
+
manifest: Manifest | null,
|
|
370
|
+
options: {
|
|
371
|
+
readonly now: () => number;
|
|
372
|
+
readonly catalog?: TemplateCatalog;
|
|
373
|
+
readonly inbox?: ChannelInbox;
|
|
374
|
+
},
|
|
375
|
+
): { readonly runtime: ChannelRuntime; readonly inbox: ChannelInbox } {
|
|
376
|
+
const inbox = options.inbox ?? createChannelInbox();
|
|
377
|
+
const templates = Object.entries(manifest?.channels ?? {}).map(([name, c]) =>
|
|
378
|
+
declareChannel.template(name, {
|
|
379
|
+
medium: c.medium,
|
|
380
|
+
locales: c.locales,
|
|
381
|
+
schema: c.schema,
|
|
382
|
+
from: c.from,
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
const runtime = createChannelRuntime({
|
|
386
|
+
templates,
|
|
387
|
+
drivers: [openConsoleChannel({ inbox })],
|
|
388
|
+
catalog: options.catalog ?? {},
|
|
389
|
+
now: options.now,
|
|
390
|
+
});
|
|
391
|
+
return { runtime, inbox };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function projectInbox(
|
|
395
|
+
entries: readonly ChannelInboxEntry[],
|
|
396
|
+
reveal: boolean,
|
|
397
|
+
): readonly ConsoleInboxRow[] {
|
|
398
|
+
return [...entries]
|
|
399
|
+
.sort((a, b) => b.at - a.at)
|
|
400
|
+
.map((e) => ({
|
|
401
|
+
id: e.id,
|
|
402
|
+
medium: e.medium,
|
|
403
|
+
toMasked: reveal ? e.to : maskRecipient(e.to),
|
|
404
|
+
subject: e.subject ?? null,
|
|
405
|
+
text: e.text ?? null,
|
|
406
|
+
html: e.html ?? null,
|
|
407
|
+
template: e.template ?? null,
|
|
408
|
+
locale: e.locale ?? null,
|
|
409
|
+
at: e.at,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function projectReceipts(
|
|
414
|
+
receipts: readonly DeliveryReceipt[],
|
|
415
|
+
reveal: boolean,
|
|
416
|
+
): readonly ConsoleReceiptRow[] {
|
|
417
|
+
return [...receipts]
|
|
418
|
+
.sort((a, b) => b.at - a.at)
|
|
419
|
+
.slice(0, 200)
|
|
420
|
+
.map((r) => ({
|
|
421
|
+
id: r.id,
|
|
422
|
+
template: r.template,
|
|
423
|
+
toMasked: reveal ? r.to : maskRecipient(r.to),
|
|
424
|
+
medium: r.medium,
|
|
425
|
+
locale: r.locale ?? null,
|
|
426
|
+
localeChain: r.localeChain ? [...r.localeChain] : [],
|
|
427
|
+
status: r.status,
|
|
428
|
+
chain: formatAttemptChain(r.attempts),
|
|
429
|
+
messageId: r.messageId ?? null,
|
|
430
|
+
at: r.at,
|
|
431
|
+
error: r.error ?? null,
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function projectSuppression(
|
|
436
|
+
rows: readonly SuppressionEntry[],
|
|
437
|
+
reveal: boolean,
|
|
438
|
+
): readonly ConsoleSuppressionRow[] {
|
|
439
|
+
return rows.map((r) => ({
|
|
440
|
+
subjectMasked: reveal ? r.subject : maskRecipient(r.subject),
|
|
441
|
+
medium: r.medium,
|
|
442
|
+
reason: r.reason,
|
|
443
|
+
at: r.at,
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function sampleDataFromSchema(schema: unknown): Record<string, unknown> {
|
|
448
|
+
if (!schema || typeof schema !== "object") return {};
|
|
449
|
+
const props = (schema as { properties?: Record<string, unknown> }).properties;
|
|
450
|
+
if (!props) return {};
|
|
451
|
+
const data: Record<string, unknown> = {};
|
|
452
|
+
for (const [key, def] of Object.entries(props)) {
|
|
453
|
+
const t = (def as { type?: string }).type;
|
|
454
|
+
data[key] =
|
|
455
|
+
t === "number" || t === "integer"
|
|
456
|
+
? 1
|
|
457
|
+
: t === "boolean"
|
|
458
|
+
? true
|
|
459
|
+
: `{{${key}}}`;
|
|
460
|
+
}
|
|
461
|
+
return data;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export type {
|
|
465
|
+
DeliveryOutcomeState,
|
|
466
|
+
DeliveryVerdict,
|
|
467
|
+
EmailAuthResult,
|
|
468
|
+
OutcomeRow,
|
|
469
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim-code acceptance — TTL, constant-time compare, rate limit, print-once.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
CLAIM_RATE_LIMIT,
|
|
8
|
+
constantTimeEqual,
|
|
9
|
+
mintClaimCode,
|
|
10
|
+
printClaimCodeOnce,
|
|
11
|
+
verifyClaimCode,
|
|
12
|
+
} from "./claim.ts";
|
|
13
|
+
|
|
14
|
+
describe("claim code", () => {
|
|
15
|
+
test("mints a hex code and verifies within TTL", () => {
|
|
16
|
+
let t = 1_000_000;
|
|
17
|
+
const state = mintClaimCode(() => t);
|
|
18
|
+
expect(state.code.length).toBe(32);
|
|
19
|
+
expect(verifyClaimCode(state, state.code, () => t)).toEqual({ ok: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("rejects after expiry", () => {
|
|
23
|
+
let t = 1_000_000;
|
|
24
|
+
const state = mintClaimCode(() => t);
|
|
25
|
+
t += 31 * 60 * 1000;
|
|
26
|
+
expect(verifyClaimCode(state, state.code, () => t)).toEqual({
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: "expired",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("rejects mismatch", () => {
|
|
33
|
+
const state = mintClaimCode(() => 1);
|
|
34
|
+
expect(verifyClaimCode(state, "nope", () => 1)).toEqual({
|
|
35
|
+
ok: false,
|
|
36
|
+
reason: "mismatch",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("rate-limits repeated attempts", () => {
|
|
41
|
+
let t = 1_000_000;
|
|
42
|
+
const state = mintClaimCode(() => t);
|
|
43
|
+
for (let i = 0; i < CLAIM_RATE_LIMIT; i++) {
|
|
44
|
+
verifyClaimCode(state, "wrong", () => t);
|
|
45
|
+
t += 1;
|
|
46
|
+
}
|
|
47
|
+
expect(verifyClaimCode(state, state.code, () => t)).toEqual({
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: "rate_limited",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("prints to boot log exactly once", () => {
|
|
54
|
+
const state = mintClaimCode(() => 1);
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
printClaimCodeOnce(state, (l) => lines.push(l));
|
|
57
|
+
printClaimCodeOnce(state, (l) => lines.push(l));
|
|
58
|
+
expect(lines.filter((l) => l.includes(state.code)).length).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("constantTimeEqual is length-safe", () => {
|
|
62
|
+
expect(constantTimeEqual("abcd", "abcd")).toBe(true);
|
|
63
|
+
expect(constantTimeEqual("abcd", "abce")).toBe(false);
|
|
64
|
+
expect(constantTimeEqual("abc", "abcd")).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-admin claim code — printed once to the boot log, never persisted.
|
|
3
|
+
*
|
|
4
|
+
* Expires in 30 minutes, regenerates on restart. Compared in constant time
|
|
5
|
+
* and rate-limited (console §2.5 · §10.4).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AUTH_RATE_LIMIT,
|
|
10
|
+
AUTH_RATE_WINDOW_MS,
|
|
11
|
+
touchRateLimit,
|
|
12
|
+
} from "./auth-rate.ts";
|
|
13
|
+
|
|
14
|
+
/** Claim-code lifetime (30 minutes). */
|
|
15
|
+
export const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
/** Max verification attempts per window (same strategy as operator login). */
|
|
18
|
+
export const CLAIM_RATE_LIMIT = AUTH_RATE_LIMIT;
|
|
19
|
+
|
|
20
|
+
/** Rate-limit window (same strategy as operator login). */
|
|
21
|
+
export const CLAIM_RATE_WINDOW_MS = AUTH_RATE_WINDOW_MS;
|
|
22
|
+
|
|
23
|
+
/** In-memory claim-code state for one Console boot. */
|
|
24
|
+
export interface ClaimCodeState {
|
|
25
|
+
/** Raw code (never written to disk). */
|
|
26
|
+
readonly code: string;
|
|
27
|
+
/** Epoch-ms when the code expires. */
|
|
28
|
+
readonly expiresAt: number;
|
|
29
|
+
/** Epoch-ms of mint. */
|
|
30
|
+
readonly mintedAt: number;
|
|
31
|
+
/** Whether the code was printed to the boot log. */
|
|
32
|
+
printed: boolean;
|
|
33
|
+
/** Sliding attempt timestamps for rate limiting. */
|
|
34
|
+
readonly attempts: number[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mint a cryptographically random claim code.
|
|
39
|
+
*
|
|
40
|
+
* @param now - Clock (injectable for tests)
|
|
41
|
+
*/
|
|
42
|
+
export function mintClaimCode(now: () => number = Date.now): ClaimCodeState {
|
|
43
|
+
const bytes = new Uint8Array(16);
|
|
44
|
+
crypto.getRandomValues(bytes);
|
|
45
|
+
const code = [...bytes]
|
|
46
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
47
|
+
.join("");
|
|
48
|
+
const mintedAt = now();
|
|
49
|
+
return {
|
|
50
|
+
code,
|
|
51
|
+
mintedAt,
|
|
52
|
+
expiresAt: mintedAt + CLAIM_TTL_MS,
|
|
53
|
+
printed: false,
|
|
54
|
+
attempts: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Print the claim code once to the boot log. Subsequent calls are no-ops.
|
|
60
|
+
*
|
|
61
|
+
* @param state - Claim state
|
|
62
|
+
* @param write - Writer (defaults to stdout)
|
|
63
|
+
*/
|
|
64
|
+
export function printClaimCodeOnce(
|
|
65
|
+
state: ClaimCodeState,
|
|
66
|
+
write: (line: string) => void = (line) => console.log(line),
|
|
67
|
+
): void {
|
|
68
|
+
if (state.printed) return;
|
|
69
|
+
state.printed = true;
|
|
70
|
+
write("");
|
|
71
|
+
write("┌─────────────────────────────────────────────────────────┐");
|
|
72
|
+
write("│ oke Console — first-admin claim code (expires 30 min) │");
|
|
73
|
+
write(`│ ${state.code} │`);
|
|
74
|
+
write("│ Whoever can read this log already owns the server. │");
|
|
75
|
+
write("└─────────────────────────────────────────────────────────┘");
|
|
76
|
+
write("");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Result of {@link verifyClaimCode}. */
|
|
80
|
+
export type ClaimVerifyResult =
|
|
81
|
+
| { readonly ok: true }
|
|
82
|
+
| {
|
|
83
|
+
readonly ok: false;
|
|
84
|
+
readonly reason: "expired" | "mismatch" | "rate_limited" | "missing";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Constant-time claim-code verification with rate limiting.
|
|
89
|
+
*
|
|
90
|
+
* @param state - Claim state (mutated for attempt tracking)
|
|
91
|
+
* @param candidate - Submitted code
|
|
92
|
+
* @param now - Clock
|
|
93
|
+
*/
|
|
94
|
+
export function verifyClaimCode(
|
|
95
|
+
state: ClaimCodeState | null,
|
|
96
|
+
candidate: string | undefined,
|
|
97
|
+
now: () => number = Date.now,
|
|
98
|
+
): ClaimVerifyResult {
|
|
99
|
+
if (!state) return { ok: false, reason: "missing" };
|
|
100
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
101
|
+
return { ok: false, reason: "mismatch" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const t = now();
|
|
105
|
+
if (touchRateLimit(state.attempts, t) === "rate_limited") {
|
|
106
|
+
return { ok: false, reason: "rate_limited" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (t >= state.expiresAt) {
|
|
110
|
+
return { ok: false, reason: "expired" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!constantTimeEqual(state.code, candidate)) {
|
|
114
|
+
return { ok: false, reason: "mismatch" };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Constant-time string equality (UTF-8 code units).
|
|
121
|
+
*
|
|
122
|
+
* @param a - Expected
|
|
123
|
+
* @param b - Candidate
|
|
124
|
+
*/
|
|
125
|
+
export function constantTimeEqual(a: string, b: string): boolean {
|
|
126
|
+
const len = Math.max(a.length, b.length);
|
|
127
|
+
let mismatch = a.length === b.length ? 0 : 1;
|
|
128
|
+
for (let i = 0; i < len; i++) {
|
|
129
|
+
const ac = a.charCodeAt(i) || 0;
|
|
130
|
+
const bc = b.charCodeAt(i) || 0;
|
|
131
|
+
mismatch |= ac ^ bc;
|
|
132
|
+
}
|
|
133
|
+
return mismatch === 0;
|
|
134
|
+
}
|