approval-md 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +940 -4
- package/SPEC.md +476 -34
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +1035 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +77 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/payload-view.js +43 -0
- package/dist/src/channels/payload-view.js.map +1 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/render-queue.js +564 -0
- package/dist/src/channels/render-queue.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +288 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit-card.js +201 -0
- package/dist/src/cli/audit-card.js.map +1 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-telegram.js +2063 -0
- package/dist/src/cli/channel-telegram.js.map +1 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel-web.js +357 -0
- package/dist/src/cli/channel-web.js.map +1 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +2762 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/exit-codes.js +82 -0
- package/dist/src/cli/exit-codes.js.map +1 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate-window.js +294 -0
- package/dist/src/cli/gate-window.js.map +1 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/git-scope.js +295 -0
- package/dist/src/cli/git-scope.js.map +1 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-attach.js +107 -0
- package/dist/src/cli/gloss-attach.js.map +1 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex-child.js +149 -0
- package/dist/src/cli/gloss-codex-child.js.map +1 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-codex.js +255 -0
- package/dist/src/cli/gloss-codex.js.map +1 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss-options.js +79 -0
- package/dist/src/cli/gloss-options.js.map +1 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +2339 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +2849 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +840 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-anchor.js +387 -0
- package/dist/src/cli/log-anchor.js.map +1 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-checkpoint.js +128 -0
- package/dist/src/cli/log-checkpoint.js.map +1 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-sync.js +849 -0
- package/dist/src/cli/log-sync.js.map +1 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +360 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/long-help.js +148 -0
- package/dist/src/cli/long-help.js.map +1 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +1206 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +1175 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +509 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-channel.js +635 -0
- package/dist/src/cli/setup-channel.js.map +1 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-checkpoint.js +196 -0
- package/dist/src/cli/setup-checkpoint.js.map +1 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-common.js +376 -0
- package/dist/src/cli/setup-common.js.map +1 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-flow.js +476 -0
- package/dist/src/cli/setup-flow.js.map +1 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup-service.js +308 -0
- package/dist/src/cli/setup-service.js.map +1 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +849 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +2341 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/advance-cycle.js +200 -0
- package/dist/src/core/advance-cycle.js.map +1 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +589 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/child-env.js +86 -0
- package/dist/src/core/child-env.js.map +1 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +2356 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/adapter.js +71 -0
- package/dist/src/core/coverage-sources/adapter.js.map +1 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/gh.js +136 -0
- package/dist/src/core/coverage-sources/gh.js.map +1 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage-sources/git.js +269 -0
- package/dist/src/core/coverage-sources/git.js.map +1 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/credential-spec.js +23 -0
- package/dist/src/core/credential-spec.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/dark-session.js +714 -0
- package/dist/src/core/dark-session.js.map +1 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/decision-refusal.js +265 -0
- package/dist/src/core/decision-refusal.js.map +1 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/env-file.js +837 -0
- package/dist/src/core/env-file.js.map +1 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +1271 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate-window.js +506 -0
- package/dist/src/core/gate-window.js.map +1 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +3002 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/git-run.js +93 -0
- package/dist/src/core/git-run.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +211 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/harness-wait.js +58 -0
- package/dist/src/core/harness-wait.js.map +1 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/head-retry.js +121 -0
- package/dist/src/core/head-retry.js.map +1 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/intake-limits.js +350 -0
- package/dist/src/core/intake-limits.js.map +1 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/live-draw.js +703 -0
- package/dist/src/core/live-draw.js.map +1 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-reconcile.js +136 -0
- package/dist/src/core/log-reconcile.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +487 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/md-fence.js +74 -0
- package/dist/src/core/md-fence.js.map +1 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-census.js +146 -0
- package/dist/src/core/payload-census.js.map +1 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload-store.js +340 -0
- package/dist/src/core/payload-store.js.map +1 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +588 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-expectations.js +394 -0
- package/dist/src/core/policy-expectations.js.map +1 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +258 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +536 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +478 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/policy-proposal.js +458 -0
- package/dist/src/core/policy-proposal.js.map +1 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/prompt-layout.js +422 -0
- package/dist/src/core/prompt-layout.js.map +1 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +1566 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/task-file.js +464 -0
- package/dist/src/core/task-file.js.map +1 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/telegram-config.js +114 -0
- package/dist/src/core/telegram-config.js.map +1 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +561 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verified-snapshot.js +506 -0
- package/dist/src/core/verified-snapshot.js.map +1 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance-child.js +78 -0
- package/dist/src/daemon/advance-child.js.map +1 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/dark-session.js +119 -0
- package/dist/src/daemon/dark-session.js.map +1 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw-child.js +132 -0
- package/dist/src/daemon/draw-child.js.map +1 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +345 -0
- package/dist/src/daemon/git-evidence.js.map +1 -0
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +602 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +5716 -0
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +53 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/codex-instance.schema.json +82 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1811 -0
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
- package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
- package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
- package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
- package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
- package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
- package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
- package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
- package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
- package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
- package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
- package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
- package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
- package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
- package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
- package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
- package/schema/fixtures/envelope/valid/canonical.json +25 -0
- package/schema/fixtures/envelope/valid/minimal.json +7 -0
- package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
- package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
- package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
- package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
- package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
- package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
- package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
- package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
- package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
- package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
- package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
- package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
- package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
- package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
- package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
- package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
- package/schema/fixtures/event/invalid/missing-alg.json +14 -0
- package/schema/fixtures/event/invalid/missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
- package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
- package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
- package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
- package/schema/fixtures/event/invalid/short-hash.json +15 -0
- package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
- package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
- package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
- package/schema/fixtures/event/valid/approval-expired.json +15 -0
- package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
- package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
- package/schema/fixtures/event/valid/approval-granted.json +15 -0
- package/schema/fixtures/event/valid/approval-rejected.json +15 -0
- package/schema/fixtures/event/valid/approval-requested.json +19 -0
- package/schema/fixtures/event/valid/approval-revoked.json +15 -0
- package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
- package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
- package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
- package/schema/fixtures/event/valid/audit-sampled.json +14 -0
- package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
- package/schema/fixtures/event/valid/envelope-drift.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
- package/schema/fixtures/event/valid/execution-completed.json +15 -0
- package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-failed.json +15 -0
- package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
- package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
- package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
- package/schema/fixtures/event/valid/execution-started.json +14 -0
- package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
- package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
- package/schema/fixtures/event/valid/gate-closed.json +14 -0
- package/schema/fixtures/event/valid/gate-opened.json +16 -0
- package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
- package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
- package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
- package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
- package/schema/fixtures/event/valid/payload-pruned.json +17 -0
- package/schema/fixtures/event/valid/policy-declined.json +16 -0
- package/schema/fixtures/event/valid/policy-proposed.json +35 -0
- package/schema/fixtures/event/valid/policy-updated.json +14 -0
- package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
- package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
- package/schema/fixtures/event/valid/route-accepted.json +15 -0
- package/schema/fixtures/event/valid/route-proposed.json +16 -0
- package/schema/fixtures/event/valid/spec-example.json +15 -0
- package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
- package/schema/fixtures/event/valid/task-registered.json +14 -0
- package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
- package/schema/fixtures/hash/known-answer.json +74 -0
- package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
- package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
- package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
- package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
- package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/missing-version.json +8 -0
- package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
- package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
- package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
- package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
- package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
- package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
- package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
- package/schema/fixtures/policy/valid/canonical.json +47 -0
- package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
- package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
- package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
- package/schema/fixtures/policy/valid/global-budgets.json +19 -0
- package/schema/fixtures/policy/valid/human-only.json +9 -0
- package/schema/fixtures/policy/valid/minimal.json +6 -0
- package/schema/fixtures/policy/valid/protected-paths.json +10 -0
- package/schema/fixtures/policy/valid/record-namespace.json +13 -0
- package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
- package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
- package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
- package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
- package/schema/fixtures/policy/valid/wildcards.json +15 -0
- package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
- package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
- package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
- package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
- package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
- package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
- package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/valid/canonical.md +50 -0
- package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
- package/schema/fixtures/policy-md/valid/minimal.md +3 -0
- package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
- package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
- package/schema/fixtures/policy-md/valid/with-values.md +79 -0
- package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
- package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
- package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
- package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
- package/schema/fixtures/sample-record/valid/minimal.json +4 -0
- package/schema/fixtures/sample-record/valid/with-note.json +5 -0
- package/schema/fixtures/values/invalid/class-shaped.json +9 -0
- package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
- package/schema/fixtures/values/invalid/non-string-item.json +4 -0
- package/schema/fixtures/values/invalid/over-cap.json +26 -0
- package/schema/fixtures/values/invalid/unknown-key.json +5 -0
- package/schema/fixtures/values/invalid/version-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +7 -0
- package/schema/fixtures/values/valid/full.json +20 -0
- package/schema/fixtures/values/valid/minimal.json +1 -0
- package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
- package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
- package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
- package/schema/fixtures/values-md/valid/absent.md +50 -0
- package/schema/fixtures/values-md/valid/with-values.md +79 -0
- package/schema/policy.schema.json +501 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The email adapter (SPEC.md §6.1, §6.2, §7, §10.4; APRV-69).
|
|
3
|
+
*
|
|
4
|
+
* SPEC.md §6.1 makes the point in its second sentence: "the canonical example is
|
|
5
|
+
* an email, deliberately. This spec governs agent actions in the world." So this
|
|
6
|
+
* is the first adapter, and the class it serves —
|
|
7
|
+
* `communicate.email.external` — is the one the canonical task envelope
|
|
8
|
+
* declares. It implements exactly one method, {@link Adapter.act}, and the
|
|
9
|
+
* contract in `adapters/contract.ts` owns everything around it: the hash
|
|
10
|
+
* recomputation, the token spend, `execution.started`, the credential window,
|
|
11
|
+
* the outcome event, and the redaction sweep. **Nothing in this file touches a
|
|
12
|
+
* token or the log**, and nothing in it can, because it is never handed either.
|
|
13
|
+
*
|
|
14
|
+
* ## The payload, and what the grant therefore approved
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* {
|
|
18
|
+
* from: string, // one addr-spec
|
|
19
|
+
* to: string[], // at least one
|
|
20
|
+
* cc?: string[],
|
|
21
|
+
* bcc?: string[], // RCPT TO only; never a header
|
|
22
|
+
* subject: string,
|
|
23
|
+
* body: string,
|
|
24
|
+
* content_type?: "text/plain" | "text/html" // default "text/plain"
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* §6.2 says a message send's `payload_hash` covers "the full body and
|
|
29
|
+
* recipients", and this shape is that sentence made concrete. `bcc` is inside
|
|
30
|
+
* the hash even though it appears in no header: a blind recipient is still a
|
|
31
|
+
* recipient, and an approval that did not cover them would be an approval of a
|
|
32
|
+
* different act than the one performed.
|
|
33
|
+
*
|
|
34
|
+
* {@link validateEmailPayload} re-checks the shape inside `act` and refuses
|
|
35
|
+
* `email-payload-invalid`. This is defence in depth and is *not* the binding —
|
|
36
|
+
* the hash already bound the bytes before `act` was called. What it catches is a
|
|
37
|
+
* grant over bytes that are well-formed JSON and not a well-formed email, which
|
|
38
|
+
* is a human approving a mistake rather than an attacker substituting one.
|
|
39
|
+
* Unknown keys are refused too: a payload carrying `attachments` that this
|
|
40
|
+
* adapter silently dropped would send something other than what a human read.
|
|
41
|
+
*
|
|
42
|
+
* ## The two fields the grant does NOT bind
|
|
43
|
+
*
|
|
44
|
+
* A wire message needs a `Date` and a `Message-ID`, and neither can live in the
|
|
45
|
+
* payload without making the hash depend on when the send happened.
|
|
46
|
+
*
|
|
47
|
+
* - **`Date`** is stamped by the runtime from {@link EmailAdapterOptions.clock}
|
|
48
|
+
* (default: the wall clock) at the moment of the send. The grant binds the
|
|
49
|
+
* *message content*; the runtime records *when it went*. A human who approved
|
|
50
|
+
* a chaser on Tuesday and watched it execute on Wednesday approved the words,
|
|
51
|
+
* not the timestamp, and a `Date` inside the payload would additionally make
|
|
52
|
+
* every grant expire into a `payload-mismatch` the moment the clock moved.
|
|
53
|
+
* - **`Message-ID`** is derived, not random: SHA-256 over the action key and the
|
|
54
|
+
* payload hash, at the From address's domain. It is therefore reproducible
|
|
55
|
+
* from the log alone — an operator holding `action_key` and the grant's
|
|
56
|
+
* `payload_hash` can recompute the exact Message-ID the far side saw, which is
|
|
57
|
+
* what makes a delivery receipt or a bounce traceable back to an approval. A
|
|
58
|
+
* random one would have to be logged to be useful, and the log is not where an
|
|
59
|
+
* adapter writes.
|
|
60
|
+
*
|
|
61
|
+
* Both are stated in the module header rather than only in code because they are
|
|
62
|
+
* the two places this adapter puts bytes on the wire that no human approved.
|
|
63
|
+
*
|
|
64
|
+
* ## Encoding
|
|
65
|
+
*
|
|
66
|
+
* Quoted-printable, implemented here (RFC 2045 §6.7), rather than refusing
|
|
67
|
+
* non-ASCII. Refusing would mean an adapter whose canonical example — a chaser
|
|
68
|
+
* to a British letting agency about a £1,200 deposit — cannot be sent, and
|
|
69
|
+
* "your approval was fine, the runtime cannot spell your currency" is not a
|
|
70
|
+
* refusal any operator would accept. An all-ASCII body is sent `8bit`
|
|
71
|
+
* unchanged, so the common case is byte-for-byte what the human read.
|
|
72
|
+
*
|
|
73
|
+
* A non-ASCII `subject` is RFC 2047 `B` encoded-words. Addresses must be ASCII:
|
|
74
|
+
* an internationalized address needs SMTPUTF8 negotiation (RFC 6531), this
|
|
75
|
+
* client does not implement it, and encoding one anyway would hand the server an
|
|
76
|
+
* address it may or may not interpret as intended. That is
|
|
77
|
+
* `email-payload-invalid` with the rule quoted.
|
|
78
|
+
*
|
|
79
|
+
* ## Configuration comes from the vault
|
|
80
|
+
*
|
|
81
|
+
* All five settings — host, port, security, user, password — are read through
|
|
82
|
+
* {@link ActInput.credentials} inside `act`, under the names in
|
|
83
|
+
* {@link DEFAULT_CREDENTIAL_NAMES}. Host and port are not secrets, and reading
|
|
84
|
+
* them from the vault anyway buys one thing worth having: there is exactly one
|
|
85
|
+
* place an adapter reads deployment configuration from, and it is not the
|
|
86
|
+
* environment, not a config file an agent can write, and not the policy an agent
|
|
87
|
+
* can read. A future task may let policy supply host/port (they are the sort of
|
|
88
|
+
* thing an operator would like to see in `APPROVAL.md`), at which point the
|
|
89
|
+
* vault stays the source for the two that are actually secret; that is a SPEC
|
|
90
|
+
* §5.2 change and therefore its own task, not a flag here.
|
|
91
|
+
*
|
|
92
|
+
* The same five are also DECLARED, as {@link EMAIL_CREDENTIAL_SPECS}, so that
|
|
93
|
+
* `approval setup adapter email` can ask for them without knowing what SMTP is
|
|
94
|
+
* (APRV-78). The manifest is derived from {@link DEFAULT_CREDENTIAL_NAMES} and
|
|
95
|
+
* its validation borrows `act`'s own refusal sentences, so a value setup accepts
|
|
96
|
+
* is a value `act` accepts, and the words are the same on both paths.
|
|
97
|
+
*
|
|
98
|
+
* Deterministic given its clock: no randomness anywhere, no environment reads,
|
|
99
|
+
* and `act` never throws (every path returns an {@link ActOutcome}).
|
|
100
|
+
*/
|
|
101
|
+
import { createHash } from "node:crypto";
|
|
102
|
+
import { payloadHash } from "../core/payload.js";
|
|
103
|
+
import { CREDENTIAL_REFUSAL_CODES, redactSecrets, } from "./contract.js";
|
|
104
|
+
import { DEFAULT_SMTP_TIMEOUT_MS, SMTP_TRANSPORT_FAILURE_CODES, isSmtpSecurity, sendMail, } from "./smtp.js";
|
|
105
|
+
/** Every key the payload may carry. Anything else is refused. */
|
|
106
|
+
const PAYLOAD_KEYS = ["from", "to", "cc", "bcc", "subject", "body", "content_type"];
|
|
107
|
+
/**
|
|
108
|
+
* A deliberately conservative addr-spec: `local@domain`, ASCII, no display
|
|
109
|
+
* name, no angle brackets, no comments, no quoted local part.
|
|
110
|
+
*
|
|
111
|
+
* RFC 5322's real grammar admits far more than this, and every extra form is
|
|
112
|
+
* another way for a string to mean one thing to a reader and another to a
|
|
113
|
+
* server. `"alice@example.com" <attacker@evil.example>` is a valid mailbox
|
|
114
|
+
* whose display name is an address, and a human skimming a rendered approval
|
|
115
|
+
* would read the wrong one. So the adapter accepts the boring form only, and an
|
|
116
|
+
* operator who needs a display name gets a refusal explaining why rather than a
|
|
117
|
+
* message whose recipient nobody can be sure of.
|
|
118
|
+
*/
|
|
119
|
+
const ADDR_SPEC = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/u;
|
|
120
|
+
/** ASCII only, and specifically no CR or LF: header injection's whole trick. */
|
|
121
|
+
function isCleanAscii(value) {
|
|
122
|
+
return /^[ -~]*$/u.test(value);
|
|
123
|
+
}
|
|
124
|
+
function badAddresses(field, values) {
|
|
125
|
+
for (const value of values) {
|
|
126
|
+
if (typeof value !== "string" || !ADDR_SPEC.test(value) || !isCleanAscii(value)) {
|
|
127
|
+
return `${field} must hold plain ASCII addresses of the form local@domain (no display name, no angle brackets, no internationalized addresses — this client does not negotiate SMTPUTF8, RFC 6531); ${JSON.stringify(String(value))} is not one`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Structural validation. Never throws; returns the reason instead.
|
|
134
|
+
*
|
|
135
|
+
* Exported so a caller can check a payload *before* requesting approval for it,
|
|
136
|
+
* which is the only place a shape error can still be fixed cheaply.
|
|
137
|
+
*/
|
|
138
|
+
export function validateEmailPayload(value) {
|
|
139
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
140
|
+
return { ok: false, message: "the payload must be a JSON object" };
|
|
141
|
+
}
|
|
142
|
+
const record = value;
|
|
143
|
+
const unknown = Object.keys(record).filter((key) => !PAYLOAD_KEYS.includes(key));
|
|
144
|
+
if (unknown.length > 0) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
message: `the payload carries ${unknown.map((key) => JSON.stringify(key)).join(", ")}, which this adapter does not implement. An unknown key is refused rather than ignored: a human approved every byte of this payload, and sending a message that silently dropped one of them would send something other than what they read. Supported keys: ${PAYLOAD_KEYS.join(", ")}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const from = record["from"];
|
|
151
|
+
if (typeof from !== "string" || from.length === 0) {
|
|
152
|
+
return { ok: false, message: "from must be a non-empty string" };
|
|
153
|
+
}
|
|
154
|
+
const fromBad = badAddresses("from", [from]);
|
|
155
|
+
if (fromBad !== null)
|
|
156
|
+
return { ok: false, message: fromBad };
|
|
157
|
+
const to = record["to"];
|
|
158
|
+
if (!Array.isArray(to) || to.length === 0) {
|
|
159
|
+
return { ok: false, message: "to must be a non-empty array of addresses" };
|
|
160
|
+
}
|
|
161
|
+
const toBad = badAddresses("to", to);
|
|
162
|
+
if (toBad !== null)
|
|
163
|
+
return { ok: false, message: toBad };
|
|
164
|
+
const lists = {};
|
|
165
|
+
for (const field of ["cc", "bcc"]) {
|
|
166
|
+
const list = record[field];
|
|
167
|
+
if (list === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
if (!Array.isArray(list)) {
|
|
170
|
+
return { ok: false, message: `${field}, when present, must be an array of addresses` };
|
|
171
|
+
}
|
|
172
|
+
const bad = badAddresses(field, list);
|
|
173
|
+
if (bad !== null)
|
|
174
|
+
return { ok: false, message: bad };
|
|
175
|
+
lists[field] = list;
|
|
176
|
+
}
|
|
177
|
+
const subject = record["subject"];
|
|
178
|
+
if (typeof subject !== "string")
|
|
179
|
+
return { ok: false, message: "subject must be a string" };
|
|
180
|
+
if (/[\r\n]/u.test(subject)) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
message: "subject must not contain CR or LF: a newline in a header is how an extra header (a second Bcc, a different From) is smuggled into a message a human approved",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const body = record["body"];
|
|
187
|
+
if (typeof body !== "string")
|
|
188
|
+
return { ok: false, message: "body must be a string" };
|
|
189
|
+
const contentType = record["content_type"];
|
|
190
|
+
if (contentType !== undefined &&
|
|
191
|
+
contentType !== "text/plain" &&
|
|
192
|
+
contentType !== "text/html") {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `content_type, when present, must be "text/plain" or "text/html", got ${JSON.stringify(contentType)}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const recipients = [...to, ...(lists.cc ?? []), ...(lists.bcc ?? [])];
|
|
199
|
+
if (recipients.length === 0) {
|
|
200
|
+
return { ok: false, message: "the payload names no recipients" };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
payload: {
|
|
205
|
+
from,
|
|
206
|
+
to: to,
|
|
207
|
+
...(lists.cc === undefined ? {} : { cc: lists.cc }),
|
|
208
|
+
...(lists.bcc === undefined ? {} : { bcc: lists.bcc }),
|
|
209
|
+
subject,
|
|
210
|
+
body,
|
|
211
|
+
...(contentType === undefined ? {} : { content_type: contentType }),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** To, then Cc, then Bcc: every address the envelope will name, in order. */
|
|
216
|
+
export function envelopeRecipients(payload) {
|
|
217
|
+
return [...payload.to, ...(payload.cc ?? []), ...(payload.bcc ?? [])];
|
|
218
|
+
}
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Rendering (RFC 5322, RFC 2045, RFC 2047)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
const CRLF = "\r\n";
|
|
223
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
224
|
+
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
225
|
+
/** RFC 5322 §3.3, in UTC. `Tue, 18 Aug 2026 09:14:02 +0000`. */
|
|
226
|
+
export function rfc5322Date(when) {
|
|
227
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
228
|
+
return [
|
|
229
|
+
`${DAYS[when.getUTCDay()] ?? "Mon"},`,
|
|
230
|
+
pad(when.getUTCDate()),
|
|
231
|
+
MONTHS[when.getUTCMonth()] ?? "Jan",
|
|
232
|
+
String(when.getUTCFullYear()),
|
|
233
|
+
`${pad(when.getUTCHours())}:${pad(when.getUTCMinutes())}:${pad(when.getUTCSeconds())}`,
|
|
234
|
+
"+0000",
|
|
235
|
+
].join(" ");
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The deterministic Message-ID: SHA-256 over a domain-separated tuple of the
|
|
239
|
+
* action key and the payload hash, at the sender's own domain.
|
|
240
|
+
*
|
|
241
|
+
* Domain separation (the literal prefix) so this digest can never collide with
|
|
242
|
+
* a payload hash computed over the same strings by something else. Truncated to
|
|
243
|
+
* 40 hex characters, which keeps the header inside a sensible width and leaves
|
|
244
|
+
* 160 bits — the value needs to be unique, not unforgeable, since forging it
|
|
245
|
+
* accomplishes nothing an attacker who can already send mail could not do.
|
|
246
|
+
*/
|
|
247
|
+
export function deterministicMessageId(actionKey, hash, from) {
|
|
248
|
+
const digest = createHash("sha256")
|
|
249
|
+
.update(`approval.md/message-id\n${actionKey}\n${hash}`, "utf8")
|
|
250
|
+
.digest("hex")
|
|
251
|
+
.slice(0, 40);
|
|
252
|
+
const domain = from.slice(from.lastIndexOf("@") + 1);
|
|
253
|
+
return `<${digest}@${domain}>`;
|
|
254
|
+
}
|
|
255
|
+
/** Does `text` need an encoding that is not plain ASCII? */
|
|
256
|
+
function isAsciiOnly(text) {
|
|
257
|
+
for (const character of text) {
|
|
258
|
+
if (character.codePointAt(0) !== undefined && character.codePointAt(0) > 0x7f) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
function hex(byte) {
|
|
265
|
+
return `=${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Quoted-printable (RFC 2045 §6.7).
|
|
269
|
+
*
|
|
270
|
+
* Line endings are normalized to CRLF hard breaks first, and each resulting
|
|
271
|
+
* line is encoded and then soft-wrapped at 76 characters with a trailing `=`.
|
|
272
|
+
* Trailing space and tab are always encoded, because a mail server that strips
|
|
273
|
+
* trailing whitespace would otherwise silently alter bytes a human approved.
|
|
274
|
+
*/
|
|
275
|
+
export function quotedPrintable(text) {
|
|
276
|
+
const lines = text.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").split("\n");
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const bytes = Buffer.from(line, "utf8");
|
|
280
|
+
let encoded = "";
|
|
281
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
282
|
+
const byte = bytes[index];
|
|
283
|
+
const last = index === bytes.length - 1;
|
|
284
|
+
if (byte === 0x20 || byte === 0x09)
|
|
285
|
+
encoded += last ? hex(byte) : String.fromCharCode(byte);
|
|
286
|
+
else if (byte === 0x3d)
|
|
287
|
+
encoded += "=3D";
|
|
288
|
+
else if (byte >= 0x21 && byte <= 0x7e)
|
|
289
|
+
encoded += String.fromCharCode(byte);
|
|
290
|
+
else
|
|
291
|
+
encoded += hex(byte);
|
|
292
|
+
}
|
|
293
|
+
out.push(...softWrap(encoded));
|
|
294
|
+
}
|
|
295
|
+
return out.join(CRLF);
|
|
296
|
+
}
|
|
297
|
+
/** Break an encoded line at 76 characters, never splitting an `=XX` triplet. */
|
|
298
|
+
function softWrap(encoded) {
|
|
299
|
+
if (encoded.length <= 76)
|
|
300
|
+
return [encoded];
|
|
301
|
+
const pieces = [];
|
|
302
|
+
let rest = encoded;
|
|
303
|
+
while (rest.length > 76) {
|
|
304
|
+
let cut = 75;
|
|
305
|
+
// An `=` within the last two characters would be the head of a triplet.
|
|
306
|
+
while (cut > 1 && rest.slice(Math.max(0, cut - 2), cut).includes("="))
|
|
307
|
+
cut -= 1;
|
|
308
|
+
pieces.push(`${rest.slice(0, cut)}=`);
|
|
309
|
+
rest = rest.slice(cut);
|
|
310
|
+
}
|
|
311
|
+
pieces.push(rest);
|
|
312
|
+
return pieces;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* RFC 2047 `B` encoded-words for a non-ASCII header value, chunked so no
|
|
316
|
+
* encoded-word exceeds 75 characters and no chunk splits a code point.
|
|
317
|
+
*/
|
|
318
|
+
export function encodeHeaderValue(value) {
|
|
319
|
+
if (isAsciiOnly(value))
|
|
320
|
+
return value;
|
|
321
|
+
const prefix = "=?UTF-8?B?";
|
|
322
|
+
const suffix = "?=";
|
|
323
|
+
// 75 total, minus the delimiters, rounded down to a whole base64 quantum
|
|
324
|
+
// (4 characters, which is 3 source bytes).
|
|
325
|
+
const budget = 75 - prefix.length - suffix.length;
|
|
326
|
+
const perChunk = Math.floor(budget / 4) * 3;
|
|
327
|
+
const words = [];
|
|
328
|
+
let chunk = Buffer.alloc(0);
|
|
329
|
+
for (const character of value) {
|
|
330
|
+
const bytes = Buffer.from(character, "utf8");
|
|
331
|
+
if (chunk.length + bytes.length > perChunk) {
|
|
332
|
+
words.push(`${prefix}${chunk.toString("base64")}${suffix}`);
|
|
333
|
+
chunk = Buffer.alloc(0);
|
|
334
|
+
}
|
|
335
|
+
chunk = Buffer.concat([chunk, bytes]);
|
|
336
|
+
}
|
|
337
|
+
if (chunk.length > 0)
|
|
338
|
+
words.push(`${prefix}${chunk.toString("base64")}${suffix}`);
|
|
339
|
+
// A folded continuation: CRLF plus one space, which RFC 5322 §2.2.3 requires
|
|
340
|
+
// and RFC 2047 §2 relies on to keep adjacent encoded-words concatenated.
|
|
341
|
+
return words.join(`${CRLF} `);
|
|
342
|
+
}
|
|
343
|
+
/** `Name: value`, folded at commas when the line would run past 78. */
|
|
344
|
+
function header(name, value) {
|
|
345
|
+
const line = `${name}: ${value}`;
|
|
346
|
+
if (line.length <= 78 || !value.includes(", "))
|
|
347
|
+
return line;
|
|
348
|
+
const parts = value.split(", ");
|
|
349
|
+
const folded = [];
|
|
350
|
+
let current = `${name}:`;
|
|
351
|
+
for (const [index, part] of parts.entries()) {
|
|
352
|
+
const piece = index === parts.length - 1 ? part : `${part},`;
|
|
353
|
+
if (`${current} ${piece}`.length > 78) {
|
|
354
|
+
folded.push(current);
|
|
355
|
+
current = ` ${piece}`;
|
|
356
|
+
}
|
|
357
|
+
else
|
|
358
|
+
current = `${current} ${piece}`;
|
|
359
|
+
}
|
|
360
|
+
folded.push(current);
|
|
361
|
+
return folded.join(CRLF);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The complete RFC 5322 message, CRLF throughout, **not** dot-stuffed: stuffing
|
|
365
|
+
* is RFC 5321 transport framing and belongs to the SMTP layer, so what this
|
|
366
|
+
* function returns is exactly the message the recipient's server stores.
|
|
367
|
+
*
|
|
368
|
+
* Bcc appears nowhere in it. That is the whole meaning of blind: the address is
|
|
369
|
+
* in the envelope (`RCPT TO`) and in the payload a human approved, and in no
|
|
370
|
+
* byte the other recipients receive.
|
|
371
|
+
*/
|
|
372
|
+
export function renderEmailMessage(payload, stamp) {
|
|
373
|
+
const contentType = payload.content_type ?? "text/plain";
|
|
374
|
+
const asciiBody = isAsciiOnly(payload.body);
|
|
375
|
+
const body = asciiBody
|
|
376
|
+
? payload.body.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").split("\n").join(CRLF)
|
|
377
|
+
: quotedPrintable(payload.body);
|
|
378
|
+
const headers = [
|
|
379
|
+
header("Date", rfc5322Date(stamp.date)),
|
|
380
|
+
header("From", payload.from),
|
|
381
|
+
header("To", payload.to.join(", ")),
|
|
382
|
+
...(payload.cc === undefined || payload.cc.length === 0
|
|
383
|
+
? []
|
|
384
|
+
: [header("Cc", payload.cc.join(", "))]),
|
|
385
|
+
header("Subject", encodeHeaderValue(payload.subject)),
|
|
386
|
+
header("Message-ID", stamp.messageId),
|
|
387
|
+
header("MIME-Version", "1.0"),
|
|
388
|
+
header("Content-Type", `${contentType}; charset=utf-8`),
|
|
389
|
+
header("Content-Transfer-Encoding", asciiBody ? "8bit" : "quoted-printable"),
|
|
390
|
+
];
|
|
391
|
+
return `${headers.join(CRLF)}${CRLF}${CRLF}${body}${CRLF}`;
|
|
392
|
+
}
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// The adapter
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
/** The class SPEC.md §6.1's canonical envelope declares. */
|
|
397
|
+
export const EMAIL_CLASS = "communicate.email.external";
|
|
398
|
+
/** The vault names this adapter reads, all of them inside `act`. */
|
|
399
|
+
export const DEFAULT_CREDENTIAL_NAMES = {
|
|
400
|
+
host: "smtp.host",
|
|
401
|
+
port: "smtp.port",
|
|
402
|
+
user: "smtp.user",
|
|
403
|
+
password: "smtp.password",
|
|
404
|
+
security: "smtp.security",
|
|
405
|
+
};
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// The credential manifest (APRV-78)
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
/**
|
|
410
|
+
* The refusal sentences, written once and used twice.
|
|
411
|
+
*
|
|
412
|
+
* `act` refuses a bad port or a bad security setting at send time; `approval
|
|
413
|
+
* setup adapter email` refuses the same values at collection time. They must
|
|
414
|
+
* say the same thing in the same words, or an operator learns at 2am that the
|
|
415
|
+
* port they were allowed to type six weeks earlier was never a port. Each
|
|
416
|
+
* sentence takes the NAME because `credentialNames` may rename any of them.
|
|
417
|
+
*/
|
|
418
|
+
function portSentence(name) {
|
|
419
|
+
return `the vault's ${name} is not a TCP port number (1-65535)`;
|
|
420
|
+
}
|
|
421
|
+
function securitySentence(name) {
|
|
422
|
+
return `the vault's ${name} must be "implicit", "starttls" or "none"; it is none of those, and this adapter will not guess a transport security setting`;
|
|
423
|
+
}
|
|
424
|
+
/** The both-or-neither sentence. `held` is the one present; `missing` is not. */
|
|
425
|
+
function pairSentence(held, missing) {
|
|
426
|
+
return `the vault holds ${held} but not ${missing}. An SMTP login needs both; sending unauthenticated because half the credential is missing would put the message on a path nobody configured`;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* What this adapter reads from the vault, declared rather than discovered.
|
|
430
|
+
*
|
|
431
|
+
* DERIVED from {@link DEFAULT_CREDENTIAL_NAMES} rather than restating the five
|
|
432
|
+
* strings: a manifest that could drift from the names `act` asks for would be a
|
|
433
|
+
* setup wizard that fills a vault the adapter then cannot read, and
|
|
434
|
+
* `tests/adapter-email.test.ts` pins the two key for key.
|
|
435
|
+
*
|
|
436
|
+
* Order is the order an operator is asked, and it is not alphabetical: the
|
|
437
|
+
* password is last, so that everything a mistyped host or port can waste is
|
|
438
|
+
* answered before the one value that is unpleasant to re-enter.
|
|
439
|
+
*/
|
|
440
|
+
export const EMAIL_CREDENTIAL_SPECS = [
|
|
441
|
+
{
|
|
442
|
+
name: DEFAULT_CREDENTIAL_NAMES.host,
|
|
443
|
+
kind: "config",
|
|
444
|
+
label: "SMTP host",
|
|
445
|
+
describe: "the submission server this runtime connects to",
|
|
446
|
+
required: true,
|
|
447
|
+
validate(value) {
|
|
448
|
+
const trimmed = value.trim();
|
|
449
|
+
if (trimmed.length === 0)
|
|
450
|
+
return { ok: false, message: "the host is empty" };
|
|
451
|
+
if (/\s/u.test(trimmed)) {
|
|
452
|
+
return { ok: false, message: "a host name contains no whitespace" };
|
|
453
|
+
}
|
|
454
|
+
return { ok: true };
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: DEFAULT_CREDENTIAL_NAMES.port,
|
|
459
|
+
kind: "config",
|
|
460
|
+
label: "SMTP port",
|
|
461
|
+
describe: "the TCP port: 587 for STARTTLS submission, 465 for implicit TLS",
|
|
462
|
+
required: true,
|
|
463
|
+
default: "587",
|
|
464
|
+
validate(value) {
|
|
465
|
+
const port = Number(value.trim());
|
|
466
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
467
|
+
return { ok: false, message: portSentence(DEFAULT_CREDENTIAL_NAMES.port) };
|
|
468
|
+
}
|
|
469
|
+
return { ok: true };
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: DEFAULT_CREDENTIAL_NAMES.security,
|
|
474
|
+
kind: "choice",
|
|
475
|
+
label: "transport security",
|
|
476
|
+
describe: "how the connection is protected; this adapter never guesses it",
|
|
477
|
+
required: true,
|
|
478
|
+
default: "starttls",
|
|
479
|
+
choices: [
|
|
480
|
+
{ value: "implicit", describe: "TLS from the first byte (the submissions port, 465)" },
|
|
481
|
+
{ value: "starttls", describe: "plaintext, then a mandatory STARTTLS upgrade (port 587)" },
|
|
482
|
+
{ value: "none", describe: "plaintext throughout; the adapter refuses to AUTH over it" },
|
|
483
|
+
],
|
|
484
|
+
validate(value) {
|
|
485
|
+
if (!isSmtpSecurity(value.trim())) {
|
|
486
|
+
return { ok: false, message: securitySentence(DEFAULT_CREDENTIAL_NAMES.security) };
|
|
487
|
+
}
|
|
488
|
+
return { ok: true };
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: DEFAULT_CREDENTIAL_NAMES.user,
|
|
493
|
+
kind: "config",
|
|
494
|
+
label: "SMTP username",
|
|
495
|
+
describe: "the login name, when the relay wants one; leave empty for a relay that does not",
|
|
496
|
+
required: false,
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: DEFAULT_CREDENTIAL_NAMES.password,
|
|
500
|
+
kind: "secret",
|
|
501
|
+
label: "SMTP password",
|
|
502
|
+
describe: "the login secret, required exactly when a username is given",
|
|
503
|
+
required: false,
|
|
504
|
+
},
|
|
505
|
+
];
|
|
506
|
+
/**
|
|
507
|
+
* The names this adapter cannot act without, for the contract's pre-token
|
|
508
|
+
* resolution (APRV-169).
|
|
509
|
+
*
|
|
510
|
+
* DERIVED from {@link EMAIL_CREDENTIAL_SPECS}, which already states which values
|
|
511
|
+
* are required, and mapped through `names` so a deployment that renamed one gets
|
|
512
|
+
* the name it actually stored. The login pair is deliberately absent: a relay
|
|
513
|
+
* that wants no login is a supported configuration, and listing `smtp.user`
|
|
514
|
+
* here would turn an optional value into a precondition and refuse a
|
|
515
|
+
* deployment that works today.
|
|
516
|
+
*/
|
|
517
|
+
export function requiredEmailCredentials(names = DEFAULT_CREDENTIAL_NAMES) {
|
|
518
|
+
const keyOfDefault = new Map(Object.entries(DEFAULT_CREDENTIAL_NAMES).map(([key, value]) => [
|
|
519
|
+
value,
|
|
520
|
+
key,
|
|
521
|
+
]));
|
|
522
|
+
return EMAIL_CREDENTIAL_SPECS.filter((spec) => spec.required === true).map((spec) => {
|
|
523
|
+
const key = keyOfDefault.get(spec.name);
|
|
524
|
+
return key === undefined ? spec.name : names[key];
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The cross-field rule: a username and a password are both-or-neither.
|
|
529
|
+
*
|
|
530
|
+
* Returns the refusal sentence, or `null` when the set is coherent. Exported so
|
|
531
|
+
* that `approval setup adapter email` refuses the half-configured pair at the
|
|
532
|
+
* moment the operator could still fix it, saying exactly what `act` would have
|
|
533
|
+
* said at send time — and `act` itself calls this, so there is one sentence and
|
|
534
|
+
* not two that drift.
|
|
535
|
+
*
|
|
536
|
+
* An absent value and an empty one are the same thing here: `setCredential`
|
|
537
|
+
* refuses an empty credential outright, so "" can only ever mean "not given".
|
|
538
|
+
*/
|
|
539
|
+
export function checkEmailCredentialSet(values, names = DEFAULT_CREDENTIAL_NAMES, kept = []) {
|
|
540
|
+
// A name the operator kept from an earlier run is present in the vault even
|
|
541
|
+
// though this run never saw its value (APRV-98). Presence is all the pair
|
|
542
|
+
// rule asks about.
|
|
543
|
+
const has = (name) => {
|
|
544
|
+
if (kept.includes(name))
|
|
545
|
+
return true;
|
|
546
|
+
const value = values[name];
|
|
547
|
+
return typeof value === "string" && value.length > 0;
|
|
548
|
+
};
|
|
549
|
+
const user = has(names.user);
|
|
550
|
+
const password = has(names.password);
|
|
551
|
+
if (user === password)
|
|
552
|
+
return null;
|
|
553
|
+
return user ? pairSentence(names.user, names.password) : pairSentence(names.password, names.user);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Everything this adapter can report. Frozen union, additive only
|
|
557
|
+
* (SPEC.md §11.1(6)), and note the three families it is a union OF:
|
|
558
|
+
*
|
|
559
|
+
* - the adapter's own two (`email-payload-invalid`, `email-config-invalid`);
|
|
560
|
+
* - the credential refusals, passed through verbatim from the provider, because
|
|
561
|
+
* "the vault is locked" and "nobody stored an SMTP password" are two different
|
|
562
|
+
* repairs and collapsing them into one adapter code would hide which; and
|
|
563
|
+
* - the SMTP transport codes.
|
|
564
|
+
*
|
|
565
|
+
* A server's own refusal produces `smtp-<NNN>`, which is a family rather than a
|
|
566
|
+
* member (see `smtp.ts`); {@link isEmailFailureCode} accepts both.
|
|
567
|
+
*/
|
|
568
|
+
export const EMAIL_FAILURE_CODES = [
|
|
569
|
+
/** The payload is not a well-formed email. Nothing was connected to. */
|
|
570
|
+
"email-payload-invalid",
|
|
571
|
+
/** The vault answered, and what it holds is not usable configuration. */
|
|
572
|
+
"email-config-invalid",
|
|
573
|
+
...CREDENTIAL_REFUSAL_CODES,
|
|
574
|
+
...SMTP_TRANSPORT_FAILURE_CODES,
|
|
575
|
+
];
|
|
576
|
+
export function isEmailFailureCode(value) {
|
|
577
|
+
return (EMAIL_FAILURE_CODES.includes(value) || /^smtp-[1-5]\d\d$/u.test(value));
|
|
578
|
+
}
|
|
579
|
+
function readCredential(credentials, name) {
|
|
580
|
+
const got = credentials.get(name);
|
|
581
|
+
if (got.ok)
|
|
582
|
+
return { ok: true, value: got.value };
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
code: got.code,
|
|
586
|
+
message: `the email adapter needs the credential ${JSON.stringify(name)}: ${got.message}`,
|
|
587
|
+
absent: got.code === "credential-unavailable",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Read this adapter's whole configuration from a credential provider.
|
|
592
|
+
*
|
|
593
|
+
* Extracted from `act` (APRV-99) so that there is exactly ONE piece of code in
|
|
594
|
+
* the repository that turns a {@link CredentialProvider} into SMTP settings: the
|
|
595
|
+
* names it asks for, the order it asks in, the port and security validation,
|
|
596
|
+
* and the both-or-neither pair rule. `act` calls it inside the verified-token
|
|
597
|
+
* window, and `setup adapter email` calls it to probe a configuration it only
|
|
598
|
+
* partly typed, over a provider built the same way. Neither one re-derives the
|
|
599
|
+
* rules, and a second reader would be a second opinion about what "configured"
|
|
600
|
+
* means.
|
|
601
|
+
*
|
|
602
|
+
* Values never leave the process on either path: what comes back is the
|
|
603
|
+
* configuration a session is opened with, and every string this function
|
|
604
|
+
* RETURNS as a message has been through the scrub built from what it read.
|
|
605
|
+
*/
|
|
606
|
+
export function readEmailSmtpConfig(credentials, names = DEFAULT_CREDENTIAL_NAMES) {
|
|
607
|
+
const secrets = [];
|
|
608
|
+
const scrub = (text) => redactSecrets(text, secrets).text;
|
|
609
|
+
const read = (name) => {
|
|
610
|
+
const got = readCredential(credentials, name);
|
|
611
|
+
if (got.ok && got.value.length > 0)
|
|
612
|
+
secrets.push(got.value);
|
|
613
|
+
return got;
|
|
614
|
+
};
|
|
615
|
+
const fail = (code, message) => ({
|
|
616
|
+
ok: false,
|
|
617
|
+
code,
|
|
618
|
+
message,
|
|
619
|
+
secrets,
|
|
620
|
+
});
|
|
621
|
+
const host = read(names.host);
|
|
622
|
+
if (!host.ok)
|
|
623
|
+
return fail(host.code, scrub(host.message));
|
|
624
|
+
const port = read(names.port);
|
|
625
|
+
if (!port.ok)
|
|
626
|
+
return fail(port.code, scrub(port.message));
|
|
627
|
+
const security = read(names.security);
|
|
628
|
+
if (!security.ok)
|
|
629
|
+
return fail(security.code, scrub(security.message));
|
|
630
|
+
const portNumber = Number(port.value);
|
|
631
|
+
if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65_535) {
|
|
632
|
+
return fail("email-config-invalid", portSentence(names.port));
|
|
633
|
+
}
|
|
634
|
+
if (!isSmtpSecurity(security.value)) {
|
|
635
|
+
return fail("email-config-invalid", securitySentence(names.security));
|
|
636
|
+
}
|
|
637
|
+
// The credential pair. Both absent is a relay that needs no login; exactly
|
|
638
|
+
// one absent is a half-configured deployment, and sending unauthenticated
|
|
639
|
+
// because the password happens to be missing is how a message goes out over a
|
|
640
|
+
// path nobody intended.
|
|
641
|
+
const user = read(names.user);
|
|
642
|
+
const password = read(names.password);
|
|
643
|
+
if (!user.ok && !user.absent)
|
|
644
|
+
return fail(user.code, scrub(user.message));
|
|
645
|
+
if (!password.ok && !password.absent)
|
|
646
|
+
return fail(password.code, scrub(password.message));
|
|
647
|
+
const pairProblem = checkEmailCredentialSet({
|
|
648
|
+
[names.user]: user.ok ? user.value : undefined,
|
|
649
|
+
[names.password]: password.ok ? password.value : undefined,
|
|
650
|
+
}, names);
|
|
651
|
+
if (pairProblem !== null)
|
|
652
|
+
return fail("email-config-invalid", pairProblem);
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
secrets,
|
|
656
|
+
config: {
|
|
657
|
+
host: host.value,
|
|
658
|
+
port: portNumber,
|
|
659
|
+
security: security.value,
|
|
660
|
+
...(user.ok ? { user: user.value } : {}),
|
|
661
|
+
...(password.ok ? { password: password.value } : {}),
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* A fresh email adapter.
|
|
667
|
+
*
|
|
668
|
+
* Stateless and reusable: it holds no socket, no connection pool, and nothing
|
|
669
|
+
* from a previous send. Two concurrent executions through the same instance
|
|
670
|
+
* share nothing.
|
|
671
|
+
*/
|
|
672
|
+
export function emailAdapter(options = {}) {
|
|
673
|
+
const names = { ...DEFAULT_CREDENTIAL_NAMES, ...options.credentialNames };
|
|
674
|
+
const classes = [EMAIL_CLASS, ...(options.classes ?? []).filter((cls) => cls !== EMAIL_CLASS)];
|
|
675
|
+
const clock = options.clock ?? (() => new Date());
|
|
676
|
+
return {
|
|
677
|
+
name: "email",
|
|
678
|
+
classes,
|
|
679
|
+
requiredCredentials: requiredEmailCredentials(names),
|
|
680
|
+
async act(input) {
|
|
681
|
+
// (1) The payload. Refused before any credential is read and long before
|
|
682
|
+
// any socket is opened: a malformed payload is not a reason to touch
|
|
683
|
+
// the vault.
|
|
684
|
+
const validated = validateEmailPayload(input.payload);
|
|
685
|
+
if (!validated.ok) {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
code: "email-payload-invalid",
|
|
689
|
+
message: `the approved payload for ${input.actionKey} is not a well-formed email: ${validated.message}. Nothing was connected to and nothing was sent`,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const payload = validated.payload;
|
|
693
|
+
// (2) The configuration and the credential pair, from the vault, inside
|
|
694
|
+
// the window — through {@link readEmailSmtpConfig}, which is the only
|
|
695
|
+
// code in this repository that turns a provider into SMTP settings.
|
|
696
|
+
// Every value it read joins the local redaction corpus, so a
|
|
697
|
+
// diagnostic this adapter builds is scrubbed before the contract
|
|
698
|
+
// scrubs it again.
|
|
699
|
+
const configured = readEmailSmtpConfig(input.credentials, names);
|
|
700
|
+
const scrub = (text) => redactSecrets(text, configured.secrets).text;
|
|
701
|
+
if (!configured.ok) {
|
|
702
|
+
// Already scrubbed with everything that had been read when it failed.
|
|
703
|
+
return { ok: false, code: configured.code, message: configured.message };
|
|
704
|
+
}
|
|
705
|
+
const { host, port: portNumber, security: transport } = configured.config;
|
|
706
|
+
const { user, password } = configured.config;
|
|
707
|
+
// (4) The two stamped fields, and the message.
|
|
708
|
+
const hash = payloadHash(payload);
|
|
709
|
+
const stamp = {
|
|
710
|
+
date: clock(),
|
|
711
|
+
messageId: deterministicMessageId(input.actionKey, hash, payload.from),
|
|
712
|
+
};
|
|
713
|
+
const message = renderEmailMessage(payload, stamp);
|
|
714
|
+
const recipients = envelopeRecipients(payload);
|
|
715
|
+
// (5) The send.
|
|
716
|
+
const sent = await sendMail({
|
|
717
|
+
host,
|
|
718
|
+
port: portNumber,
|
|
719
|
+
security: transport,
|
|
720
|
+
...(user === undefined ? {} : { user }),
|
|
721
|
+
...(password === undefined ? {} : { password }),
|
|
722
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_SMTP_TIMEOUT_MS,
|
|
723
|
+
...(options.clientName === undefined ? {} : { clientName: options.clientName }),
|
|
724
|
+
tlsRejectUnauthorized: options.tlsRejectUnauthorized ?? true,
|
|
725
|
+
redact: scrub,
|
|
726
|
+
}, { from: payload.from, recipients }, message);
|
|
727
|
+
if (!sent.ok) {
|
|
728
|
+
return { ok: false, code: sent.code, message: scrub(sent.message) };
|
|
729
|
+
}
|
|
730
|
+
// The detail is the receipt: what went where, and nothing about how the
|
|
731
|
+
// session authenticated beyond the mechanism's name.
|
|
732
|
+
return {
|
|
733
|
+
ok: true,
|
|
734
|
+
detail: {
|
|
735
|
+
message_id: stamp.messageId,
|
|
736
|
+
payload_hash: hash,
|
|
737
|
+
date: stamp.date.toISOString(),
|
|
738
|
+
recipients: recipients.length,
|
|
739
|
+
bytes: Buffer.byteLength(message, "utf8"),
|
|
740
|
+
secure: sent.secure,
|
|
741
|
+
auth: sent.authenticated,
|
|
742
|
+
smtp_code: sent.reply.code,
|
|
743
|
+
transcript: sent.transcript,
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=email.js.map
|