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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent / opt-out — Channel will not send without consent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ChannelMedium } from "../../manifest/types.ts";
|
|
6
|
+
|
|
7
|
+
/** Opt-out record. */
|
|
8
|
+
export interface OptOut {
|
|
9
|
+
readonly subject: string;
|
|
10
|
+
readonly medium: ChannelMedium | "all";
|
|
11
|
+
readonly at: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Consent store surface. */
|
|
15
|
+
export interface ConsentStore {
|
|
16
|
+
/**
|
|
17
|
+
* Whether the subject has opted out of this medium.
|
|
18
|
+
*
|
|
19
|
+
* @param subject - User / address id
|
|
20
|
+
* @param medium - Channel medium
|
|
21
|
+
*/
|
|
22
|
+
isOptedOut(subject: string, medium: ChannelMedium): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Record an opt-out.
|
|
25
|
+
*
|
|
26
|
+
* @param subject - User / address id
|
|
27
|
+
* @param medium - Medium or `all`
|
|
28
|
+
*/
|
|
29
|
+
optOut(subject: string, medium: ChannelMedium | "all"): void;
|
|
30
|
+
/**
|
|
31
|
+
* Clear an opt-out (re-consent).
|
|
32
|
+
*
|
|
33
|
+
* @param subject - User / address id
|
|
34
|
+
* @param medium - Medium or `all`
|
|
35
|
+
*/
|
|
36
|
+
optIn(subject: string, medium: ChannelMedium | "all"): void;
|
|
37
|
+
/** Snapshot of all opt-outs. */
|
|
38
|
+
list(): readonly OptOut[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create an in-memory consent store.
|
|
43
|
+
*/
|
|
44
|
+
export function createConsentStore(): ConsentStore {
|
|
45
|
+
const rows: OptOut[] = [];
|
|
46
|
+
|
|
47
|
+
function matches(
|
|
48
|
+
subject: string,
|
|
49
|
+
medium: ChannelMedium,
|
|
50
|
+
): boolean {
|
|
51
|
+
return rows.some(
|
|
52
|
+
(r) =>
|
|
53
|
+
r.subject === subject &&
|
|
54
|
+
(r.medium === "all" || r.medium === medium),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
isOptedOut(subject, medium) {
|
|
60
|
+
return matches(subject, medium);
|
|
61
|
+
},
|
|
62
|
+
optOut(subject, medium) {
|
|
63
|
+
if (
|
|
64
|
+
!rows.some((r) => r.subject === subject && r.medium === medium)
|
|
65
|
+
) {
|
|
66
|
+
rows.push({ subject, medium, at: Date.now() });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
optIn(subject, medium) {
|
|
70
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
71
|
+
const r = rows[i]!;
|
|
72
|
+
if (r.subject === subject && r.medium === medium) {
|
|
73
|
+
rows.splice(i, 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
list() {
|
|
78
|
+
return [...rows];
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback weekly cost delta (console §9.9 · §9.12).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { fallbackWeeklyCostDelta } from "./costs.ts";
|
|
7
|
+
import type { DeliveryReceipt } from "./receipts.ts";
|
|
8
|
+
|
|
9
|
+
describe("fallbackWeeklyCostDelta", () => {
|
|
10
|
+
test("projects fallback rate into a weekly USD delta", () => {
|
|
11
|
+
const weekStart = Date.UTC(2026, 6, 20);
|
|
12
|
+
const receipts: DeliveryReceipt[] = [
|
|
13
|
+
{
|
|
14
|
+
id: "1",
|
|
15
|
+
template: "otp-code",
|
|
16
|
+
to: "a",
|
|
17
|
+
medium: "any",
|
|
18
|
+
status: "fallback",
|
|
19
|
+
attempts: [
|
|
20
|
+
{ driverId: "wa-cloud", ok: false, at: weekStart + 1 },
|
|
21
|
+
{ driverId: "unifonic", ok: true, at: weekStart + 2 },
|
|
22
|
+
],
|
|
23
|
+
at: weekStart + 3,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "2",
|
|
27
|
+
template: "otp-code",
|
|
28
|
+
to: "b",
|
|
29
|
+
medium: "whatsapp",
|
|
30
|
+
status: "sent",
|
|
31
|
+
attempts: [{ driverId: "wa-cloud", ok: true, at: weekStart + 4 }],
|
|
32
|
+
at: weekStart + 5,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "3",
|
|
36
|
+
template: "otp-code",
|
|
37
|
+
to: "c",
|
|
38
|
+
medium: "any",
|
|
39
|
+
status: "fallback",
|
|
40
|
+
attempts: [
|
|
41
|
+
{ driverId: "wa-cloud", ok: false, at: weekStart + 6 },
|
|
42
|
+
{ driverId: "unifonic", ok: true, at: weekStart + 7 },
|
|
43
|
+
],
|
|
44
|
+
at: weekStart + 8,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const delta = fallbackWeeklyCostDelta(receipts, {
|
|
49
|
+
weekStartMs: weekStart,
|
|
50
|
+
weekEndMs: weekStart + 86_400_000 * 7,
|
|
51
|
+
costs: { whatsapp: 0.005, sms: 0.0075 },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(delta.totalCount).toBe(3);
|
|
55
|
+
expect(delta.fallbackCount).toBe(2);
|
|
56
|
+
expect(delta.fallbackRate).toBeCloseTo(2 / 3, 5);
|
|
57
|
+
// Each fallback: sms 0.0075 − whatsapp 0.005 = 0.0025
|
|
58
|
+
expect(delta.weeklyDeltaUsd).toBeCloseTo(0.005, 5);
|
|
59
|
+
expect(delta.primaryMedium).toBe("whatsapp");
|
|
60
|
+
expect(delta.fallbackMedium).toBe("sms");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel medium unit costs — fallback rate as a weekly bill (console §9.9 · §9.12).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ChannelMedium } from "../../manifest/types.ts";
|
|
6
|
+
import type { DeliveryReceipt } from "./receipts.ts";
|
|
7
|
+
|
|
8
|
+
/** USD per message by medium. */
|
|
9
|
+
export type MediumCosts = Readonly<Partial<Record<ChannelMedium, number>>>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default unit costs (USD / message). Overridable on the runtime.
|
|
13
|
+
* Rough public-cloud ballparks — the unit that matters is the weekly delta.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_MEDIUM_COSTS: MediumCosts = {
|
|
16
|
+
email: 0.0001,
|
|
17
|
+
sms: 0.0075,
|
|
18
|
+
whatsapp: 0.005,
|
|
19
|
+
push: 0.00005,
|
|
20
|
+
any: 0.005,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Weekly fallback cost projection. */
|
|
24
|
+
export interface FallbackWeeklyCostDelta {
|
|
25
|
+
/** Fraction of sends that used a fallback (0–1). */
|
|
26
|
+
readonly fallbackRate: number;
|
|
27
|
+
/** Count of fallback receipts in the window. */
|
|
28
|
+
readonly fallbackCount: number;
|
|
29
|
+
/** Total sends in the window. */
|
|
30
|
+
readonly totalCount: number;
|
|
31
|
+
/** Extra USD this week vs primary-medium-only. */
|
|
32
|
+
readonly weeklyDeltaUsd: number;
|
|
33
|
+
/** Primary (first attempted) medium for the sample. */
|
|
34
|
+
readonly primaryMedium: string;
|
|
35
|
+
/** Winning fallback medium (last success) when available. */
|
|
36
|
+
readonly fallbackMedium: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cost of one medium.
|
|
41
|
+
*
|
|
42
|
+
* @param medium - Channel medium
|
|
43
|
+
* @param costs - Unit cost table
|
|
44
|
+
*/
|
|
45
|
+
export function costOf(
|
|
46
|
+
medium: string,
|
|
47
|
+
costs: MediumCosts = DEFAULT_MEDIUM_COSTS,
|
|
48
|
+
): number {
|
|
49
|
+
return costs[medium as ChannelMedium] ?? costs.any ?? 0.005;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Project fallback rate into a weekly cost delta vs primary-medium-only.
|
|
54
|
+
*
|
|
55
|
+
* For each fallback receipt: delta = cost(winner) − cost(primary attempt).
|
|
56
|
+
* Summed over the week window (not per-call — §9.12).
|
|
57
|
+
*
|
|
58
|
+
* @param receipts - Delivery receipts
|
|
59
|
+
* @param options - Costs + week window
|
|
60
|
+
*/
|
|
61
|
+
export function fallbackWeeklyCostDelta(
|
|
62
|
+
receipts: readonly DeliveryReceipt[],
|
|
63
|
+
options: {
|
|
64
|
+
readonly costs?: MediumCosts;
|
|
65
|
+
readonly weekStartMs: number;
|
|
66
|
+
readonly weekEndMs?: number;
|
|
67
|
+
readonly now?: number;
|
|
68
|
+
},
|
|
69
|
+
): FallbackWeeklyCostDelta {
|
|
70
|
+
const costs = options.costs ?? DEFAULT_MEDIUM_COSTS;
|
|
71
|
+
const end = options.weekEndMs ?? options.now ?? Date.now();
|
|
72
|
+
const inWeek = receipts.filter(
|
|
73
|
+
(r) => r.at >= options.weekStartMs && r.at <= end,
|
|
74
|
+
);
|
|
75
|
+
const fallbacks = inWeek.filter((r) => r.status === "fallback");
|
|
76
|
+
|
|
77
|
+
let weeklyDeltaUsd = 0;
|
|
78
|
+
let primaryMedium = "whatsapp";
|
|
79
|
+
let fallbackMedium = "sms";
|
|
80
|
+
|
|
81
|
+
for (const r of fallbacks) {
|
|
82
|
+
const first = r.attempts[0];
|
|
83
|
+
const winner =
|
|
84
|
+
[...r.attempts].reverse().find((a) => a.ok) ?? r.attempts.at(-1);
|
|
85
|
+
if (!first || !winner) continue;
|
|
86
|
+
const primary = mediumFromDriver(first.driverId, r.medium);
|
|
87
|
+
const fb = mediumFromDriver(winner.driverId, r.medium);
|
|
88
|
+
primaryMedium = primary;
|
|
89
|
+
fallbackMedium = fb;
|
|
90
|
+
weeklyDeltaUsd += costOf(fb, costs) - costOf(primary, costs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If winner is cheaper, delta can be negative — still report it.
|
|
94
|
+
const totalCount = inWeek.length;
|
|
95
|
+
const fallbackCount = fallbacks.length;
|
|
96
|
+
return {
|
|
97
|
+
fallbackRate: totalCount === 0 ? 0 : fallbackCount / totalCount,
|
|
98
|
+
fallbackCount,
|
|
99
|
+
totalCount,
|
|
100
|
+
weeklyDeltaUsd,
|
|
101
|
+
primaryMedium,
|
|
102
|
+
fallbackMedium,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort map of driver id → medium for cost accounting.
|
|
108
|
+
*
|
|
109
|
+
* @param driverId - Driver / provider id
|
|
110
|
+
* @param receiptMedium - Receipt medium fallback
|
|
111
|
+
*/
|
|
112
|
+
function mediumFromDriver(driverId: string, receiptMedium: string): string {
|
|
113
|
+
const id = driverId.toLowerCase();
|
|
114
|
+
if (id.includes("wa") || id.includes("whatsapp")) return "whatsapp";
|
|
115
|
+
if (id.includes("sms") || id.includes("unifonic") || id.includes("twilio")) {
|
|
116
|
+
return "sms";
|
|
117
|
+
}
|
|
118
|
+
if (id.includes("push") || id.includes("fcm") || id.includes("webpush")) {
|
|
119
|
+
return "push";
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
id.includes("smtp") ||
|
|
123
|
+
id.includes("resend") ||
|
|
124
|
+
id.includes("ses") ||
|
|
125
|
+
id.includes("email")
|
|
126
|
+
) {
|
|
127
|
+
return "email";
|
|
128
|
+
}
|
|
129
|
+
return receiptMedium === "any" ? "email" : receiptMedium;
|
|
130
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel declaration — templates with i18n, medium helpers.
|
|
3
|
+
*
|
|
4
|
+
* Physics: email · SMS · WhatsApp · push.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ChannelMedium } from "../../manifest/types.ts";
|
|
8
|
+
|
|
9
|
+
/** Options for a medium binder (`channel.email`, …). */
|
|
10
|
+
export interface ChannelMediumOptions {
|
|
11
|
+
readonly from?: string;
|
|
12
|
+
readonly sender?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Options for {@link channel.template} / medium `.template`. */
|
|
16
|
+
export interface ChannelTemplateOptions {
|
|
17
|
+
readonly medium?: ChannelMedium;
|
|
18
|
+
readonly locales?: string[];
|
|
19
|
+
readonly schema?: unknown;
|
|
20
|
+
readonly from?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Declared channel template handle. */
|
|
24
|
+
export interface ChannelTemplateDecl {
|
|
25
|
+
readonly kind: "template";
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly medium: ChannelMedium;
|
|
28
|
+
readonly locales?: string[];
|
|
29
|
+
readonly schema?: unknown;
|
|
30
|
+
readonly from?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Medium binder that can declare templates. */
|
|
34
|
+
export interface ChannelMediumBinder {
|
|
35
|
+
readonly medium: ChannelMedium;
|
|
36
|
+
readonly from?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Declare a template on this medium.
|
|
39
|
+
*
|
|
40
|
+
* @param name - Template id
|
|
41
|
+
* @param options - Schema / locales
|
|
42
|
+
*/
|
|
43
|
+
template(
|
|
44
|
+
name: string,
|
|
45
|
+
options?: Omit<ChannelTemplateOptions, "medium" | "from">,
|
|
46
|
+
): ChannelTemplateDecl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a medium binder.
|
|
51
|
+
*
|
|
52
|
+
* @param medium - Medium id
|
|
53
|
+
* @param options - Default from / sender
|
|
54
|
+
*/
|
|
55
|
+
function mediumBinder(
|
|
56
|
+
medium: ChannelMedium,
|
|
57
|
+
options: ChannelMediumOptions = {},
|
|
58
|
+
): ChannelMediumBinder {
|
|
59
|
+
const from = options.from ?? options.sender;
|
|
60
|
+
return {
|
|
61
|
+
medium,
|
|
62
|
+
...(from !== undefined ? { from } : {}),
|
|
63
|
+
template(name, opts = {}) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "template",
|
|
66
|
+
name,
|
|
67
|
+
medium,
|
|
68
|
+
...(opts.locales ? { locales: opts.locales } : {}),
|
|
69
|
+
...(opts.schema !== undefined ? { schema: opts.schema } : {}),
|
|
70
|
+
...(from !== undefined ? { from } : {}),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Shape of the {@link channel} element namespace.
|
|
78
|
+
*/
|
|
79
|
+
export interface ChannelNamespace {
|
|
80
|
+
/**
|
|
81
|
+
* Medium-agnostic template.
|
|
82
|
+
*
|
|
83
|
+
* @param name - Template id
|
|
84
|
+
* @param options - Medium / locales / schema
|
|
85
|
+
*/
|
|
86
|
+
template(name: string, options?: ChannelTemplateOptions): ChannelTemplateDecl;
|
|
87
|
+
/** Email medium binder. */
|
|
88
|
+
email(options?: ChannelMediumOptions): ChannelMediumBinder;
|
|
89
|
+
/** SMS medium binder. */
|
|
90
|
+
sms(options?: ChannelMediumOptions): ChannelMediumBinder;
|
|
91
|
+
/** WhatsApp medium binder. */
|
|
92
|
+
whatsapp(options?: ChannelMediumOptions): ChannelMediumBinder;
|
|
93
|
+
/** Push medium binder. */
|
|
94
|
+
push(options?: ChannelMediumOptions): ChannelMediumBinder;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Channel element namespace.
|
|
99
|
+
*/
|
|
100
|
+
export const channel: ChannelNamespace = {
|
|
101
|
+
/**
|
|
102
|
+
* Medium-agnostic template.
|
|
103
|
+
*
|
|
104
|
+
* @param name - Template id
|
|
105
|
+
* @param options - Medium / locales / schema
|
|
106
|
+
*/
|
|
107
|
+
template(
|
|
108
|
+
name: string,
|
|
109
|
+
options: ChannelTemplateOptions = {},
|
|
110
|
+
): ChannelTemplateDecl {
|
|
111
|
+
if (!name) throw new TypeError("channel.template: name is required");
|
|
112
|
+
return {
|
|
113
|
+
kind: "template",
|
|
114
|
+
name,
|
|
115
|
+
medium: options.medium ?? "email",
|
|
116
|
+
...(options.locales ? { locales: options.locales } : {}),
|
|
117
|
+
...(options.schema !== undefined ? { schema: options.schema } : {}),
|
|
118
|
+
...(options.from !== undefined ? { from: options.from } : {}),
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/** Email medium binder. */
|
|
123
|
+
email(options?: ChannelMediumOptions): ChannelMediumBinder {
|
|
124
|
+
return mediumBinder("email", options);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/** SMS medium binder. */
|
|
128
|
+
sms(options?: ChannelMediumOptions): ChannelMediumBinder {
|
|
129
|
+
return mediumBinder("sms", options);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** WhatsApp medium binder. */
|
|
133
|
+
whatsapp(options?: ChannelMediumOptions): ChannelMediumBinder {
|
|
134
|
+
return mediumBinder("whatsapp", options);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/** Push medium binder. */
|
|
138
|
+
push(options?: ChannelMediumOptions): ChannelMediumBinder {
|
|
139
|
+
return mediumBinder("push", options);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPF / DKIM / DMARC verification with injectable DNS.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { domainFromFrom, verifyEmailAuth } from "./email-auth.ts";
|
|
7
|
+
|
|
8
|
+
describe("verifyEmailAuth", () => {
|
|
9
|
+
test("extracts domain from From address", () => {
|
|
10
|
+
expect(domainFromFrom("noreply@skyport.dev")).toBe("skyport.dev");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("classifies SPF DKIM DMARC from TXT records", async () => {
|
|
14
|
+
const result = await verifyEmailAuth("skyport.dev", {
|
|
15
|
+
dkimSelector: "oke",
|
|
16
|
+
now: () => 1,
|
|
17
|
+
async lookup(name) {
|
|
18
|
+
if (name === "skyport.dev") return ["v=spf1 include:_spf.google.com ~all"];
|
|
19
|
+
if (name === "oke._domainkey.skyport.dev") {
|
|
20
|
+
return ["v=DKIM1; k=rsa; p=MIIB"];
|
|
21
|
+
}
|
|
22
|
+
if (name === "_dmarc.skyport.dev") {
|
|
23
|
+
return ["v=DMARC1; p=quarantine; rua=mailto:d@skyport.dev"];
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
domain: "skyport.dev",
|
|
30
|
+
spf: "pass",
|
|
31
|
+
dkim: "pass",
|
|
32
|
+
dmarc: "pass",
|
|
33
|
+
checkedAt: 1,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("missing records are missing, not fail", async () => {
|
|
38
|
+
const result = await verifyEmailAuth("empty.test", {
|
|
39
|
+
async lookup() {
|
|
40
|
+
return [];
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(result.spf).toBe("missing");
|
|
44
|
+
expect(result.dkim).toBe("missing");
|
|
45
|
+
expect(result.dmarc).toBe("missing");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPF / DKIM / DMARC verification for sending domains (console §9.9).
|
|
3
|
+
*
|
|
4
|
+
* DNS is injected — the element never opens a side-channel.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Auth check result for one mechanism. */
|
|
8
|
+
export type AuthCheckStatus = "pass" | "fail" | "missing";
|
|
9
|
+
|
|
10
|
+
/** SPF / DKIM / DMARC verification result. */
|
|
11
|
+
export interface EmailAuthResult {
|
|
12
|
+
readonly domain: string;
|
|
13
|
+
readonly spf: AuthCheckStatus;
|
|
14
|
+
readonly dkim: AuthCheckStatus;
|
|
15
|
+
readonly dmarc: AuthCheckStatus;
|
|
16
|
+
readonly checkedAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Injectable DNS TXT lookup. */
|
|
20
|
+
export type DnsTxtLookup = (name: string) => Promise<readonly string[]>;
|
|
21
|
+
|
|
22
|
+
/** Options for {@link verifyEmailAuth}. */
|
|
23
|
+
export interface VerifyEmailAuthOptions {
|
|
24
|
+
readonly lookup: DnsTxtLookup;
|
|
25
|
+
readonly dkimSelector?: string;
|
|
26
|
+
readonly now?: () => number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract the domain from a From address / domain string.
|
|
31
|
+
*
|
|
32
|
+
* @param from - `noreply@example.com` or `example.com`
|
|
33
|
+
*/
|
|
34
|
+
export function domainFromFrom(from: string): string {
|
|
35
|
+
const trimmed = from.trim();
|
|
36
|
+
if (trimmed.includes("@")) {
|
|
37
|
+
return trimmed.split("@")[1]?.toLowerCase() ?? trimmed.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
// Strip display-name angle brackets: Name <a@b.c>
|
|
40
|
+
const angle = trimmed.match(/<([^>]+)>/);
|
|
41
|
+
if (angle?.[1]?.includes("@")) {
|
|
42
|
+
return angle[1].split("@")[1]!.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
return trimmed.toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Verify SPF, DKIM (selector), and DMARC TXT records for a domain.
|
|
49
|
+
*
|
|
50
|
+
* @param domainOrFrom - Domain or From address
|
|
51
|
+
* @param options - DNS lookup + optional DKIM selector
|
|
52
|
+
*/
|
|
53
|
+
export async function verifyEmailAuth(
|
|
54
|
+
domainOrFrom: string,
|
|
55
|
+
options: VerifyEmailAuthOptions,
|
|
56
|
+
): Promise<EmailAuthResult> {
|
|
57
|
+
const domain = domainFromFrom(domainOrFrom);
|
|
58
|
+
const selector = options.dkimSelector ?? "oke";
|
|
59
|
+
const now = options.now ?? (() => Date.now());
|
|
60
|
+
|
|
61
|
+
const [spfRecords, dkimRecords, dmarcRecords] = await Promise.all([
|
|
62
|
+
safeLookup(options.lookup, domain),
|
|
63
|
+
safeLookup(options.lookup, `${selector}._domainkey.${domain}`),
|
|
64
|
+
safeLookup(options.lookup, `_dmarc.${domain}`),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
domain,
|
|
69
|
+
spf: classifySpf(spfRecords),
|
|
70
|
+
dkim: classifyDkim(dkimRecords),
|
|
71
|
+
dmarc: classifyDmarc(dmarcRecords),
|
|
72
|
+
checkedAt: now(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function safeLookup(
|
|
77
|
+
lookup: DnsTxtLookup,
|
|
78
|
+
name: string,
|
|
79
|
+
): Promise<readonly string[]> {
|
|
80
|
+
try {
|
|
81
|
+
return await lookup(name);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function classifySpf(records: readonly string[]): AuthCheckStatus {
|
|
88
|
+
const joined = records.join(" ");
|
|
89
|
+
if (!joined) return "missing";
|
|
90
|
+
if (/v=spf1/i.test(joined)) {
|
|
91
|
+
return /[-~?]all|all/i.test(joined) ? "pass" : "fail";
|
|
92
|
+
}
|
|
93
|
+
return "missing";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function classifyDkim(records: readonly string[]): AuthCheckStatus {
|
|
97
|
+
const joined = records.join(" ");
|
|
98
|
+
if (!joined) return "missing";
|
|
99
|
+
return /v=DKIM1/i.test(joined) || /p=/i.test(joined) ? "pass" : "fail";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function classifyDmarc(records: readonly string[]): AuthCheckStatus {
|
|
103
|
+
const joined = records.join(" ");
|
|
104
|
+
if (!joined) return "missing";
|
|
105
|
+
if (/v=DMARC1/i.test(joined)) {
|
|
106
|
+
return /p=(none|quarantine|reject)/i.test(joined) ? "pass" : "fail";
|
|
107
|
+
}
|
|
108
|
+
return "missing";
|
|
109
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale resolution chain — profile → Accept-Language → default (console §9.9).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** One step in the locale resolution chain. */
|
|
6
|
+
export type LocaleChainStep =
|
|
7
|
+
| `profile:${string}`
|
|
8
|
+
| `accept-language:${string}`
|
|
9
|
+
| `default:${string}`;
|
|
10
|
+
|
|
11
|
+
/** Result of {@link resolveLocale}. */
|
|
12
|
+
export interface LocaleResolution {
|
|
13
|
+
readonly locale: string;
|
|
14
|
+
readonly chain: readonly LocaleChainStep[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Inputs for locale resolution. */
|
|
18
|
+
export interface ResolveLocaleOptions {
|
|
19
|
+
readonly profileLocale?: string;
|
|
20
|
+
readonly acceptLanguage?: string;
|
|
21
|
+
readonly defaultLocale?: string;
|
|
22
|
+
/** Explicit override (wins; still recorded in the chain when provided). */
|
|
23
|
+
readonly locale?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a locale is written right-to-left.
|
|
28
|
+
*
|
|
29
|
+
* @param locale - BCP 47 locale tag
|
|
30
|
+
*/
|
|
31
|
+
export function isRtlLocale(locale: string): boolean {
|
|
32
|
+
const base = locale.toLowerCase().split("-")[0] ?? locale;
|
|
33
|
+
return (
|
|
34
|
+
base === "ar" ||
|
|
35
|
+
base === "he" ||
|
|
36
|
+
base === "fa" ||
|
|
37
|
+
base === "ur" ||
|
|
38
|
+
base === "yi"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse the first language tag from an Accept-Language header.
|
|
44
|
+
*
|
|
45
|
+
* @param header - Raw Accept-Language value
|
|
46
|
+
*/
|
|
47
|
+
export function parseAcceptLanguage(header: string | undefined): string | undefined {
|
|
48
|
+
if (!header?.trim()) return undefined;
|
|
49
|
+
const first = header.split(",")[0]?.trim();
|
|
50
|
+
if (!first) return undefined;
|
|
51
|
+
const tag = first.split(";")[0]?.trim();
|
|
52
|
+
return tag || undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve locale with a full chain for the Console to display.
|
|
57
|
+
*
|
|
58
|
+
* Precedence: explicit `locale` → profile → Accept-Language → default.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Resolution inputs
|
|
61
|
+
*/
|
|
62
|
+
export function resolveLocale(
|
|
63
|
+
options: ResolveLocaleOptions = {},
|
|
64
|
+
): LocaleResolution {
|
|
65
|
+
const defaultLocale = options.defaultLocale ?? "en";
|
|
66
|
+
const chain: LocaleChainStep[] = [];
|
|
67
|
+
|
|
68
|
+
if (options.locale?.trim()) {
|
|
69
|
+
const locale = options.locale.trim();
|
|
70
|
+
chain.push(`profile:${locale}`);
|
|
71
|
+
return { locale, chain };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.profileLocale?.trim()) {
|
|
75
|
+
const locale = options.profileLocale.trim();
|
|
76
|
+
chain.push(`profile:${locale}`);
|
|
77
|
+
return { locale, chain };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fromHeader = parseAcceptLanguage(options.acceptLanguage);
|
|
81
|
+
if (fromHeader) {
|
|
82
|
+
chain.push(`accept-language:${fromHeader}`);
|
|
83
|
+
return { locale: fromHeader, chain };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
chain.push(`default:${defaultLocale}`);
|
|
87
|
+
return { locale: defaultLocale, chain };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format a locale chain for display.
|
|
92
|
+
*
|
|
93
|
+
* @param chain - Resolution steps
|
|
94
|
+
*/
|
|
95
|
+
export function formatLocaleChain(chain: readonly LocaleChainStep[]): string {
|
|
96
|
+
return chain.join(" → ");
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipient PII masking.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { maskRecipient } from "./mask.ts";
|
|
7
|
+
|
|
8
|
+
describe("maskRecipient", () => {
|
|
9
|
+
test("masks email local + domain", () => {
|
|
10
|
+
expect(maskRecipient("alice@example.com")).toBe("a***@e***.com");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("masks phone keeping prefix and last digits", () => {
|
|
14
|
+
const masked = maskRecipient("+966501234567");
|
|
15
|
+
expect(masked.startsWith("+966")).toBe(true);
|
|
16
|
+
expect(masked.endsWith("567")).toBe(true);
|
|
17
|
+
expect(masked).toContain("***");
|
|
18
|
+
});
|
|
19
|
+
});
|