@runeditrun/rer 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +154 -2
- package/bin/rer.mjs +3 -0
- package/cli/README.md +243 -0
- package/cli/adapter.mjs +10 -0
- package/cli/ai-local.mjs +65 -0
- package/cli/archive-path.mjs +52 -0
- package/cli/archive-vite.mjs +55 -0
- package/cli/archive.mjs +8 -0
- package/cli/auth-copy.mjs +80 -0
- package/cli/bounded.mjs +70 -0
- package/cli/cache-doctor.mjs +63 -0
- package/cli/container-transfer.mjs +35 -0
- package/cli/container.mjs +62 -0
- package/cli/contract.mjs +25 -0
- package/cli/dependencies.mjs +45 -0
- package/cli/dependency-approval.mjs +33 -0
- package/cli/dependency-doctor.mjs +34 -0
- package/cli/dependency-input.mjs +43 -0
- package/cli/deploy-config.mjs +118 -0
- package/cli/deploy.mjs +37 -0
- package/cli/dev-identity.mjs +80 -0
- package/cli/errors.mjs +14 -0
- package/cli/export.mjs +42 -0
- package/cli/font-cache.mjs +29 -0
- package/cli/hosted-output.mjs +110 -0
- package/cli/independent.mjs +29 -0
- package/cli/inspect.mjs +174 -0
- package/cli/jsonc.mjs +111 -0
- package/cli/lifecycle.mjs +148 -0
- package/cli/local.mjs +100 -0
- package/cli/main.mjs +57 -0
- package/cli/managed-server.mjs +143 -0
- package/cli/node-runtime-cache.mjs +68 -0
- package/cli/node-runtime-discovery.mjs +56 -0
- package/cli/node-runtime-download.mjs +59 -0
- package/cli/node-runtime.mjs +50 -0
- package/cli/output.mjs +22 -0
- package/cli/parse.mjs +77 -0
- package/cli/port.mjs +52 -0
- package/cli/portability-worker.mjs +56 -0
- package/cli/portability.mjs +66 -0
- package/cli/preview-cache.mjs +99 -0
- package/cli/project.mjs +48 -0
- package/cli/release.mjs +24 -0
- package/cli/runtime-front-door.mjs +46 -0
- package/cli/scheduled.mjs +81 -0
- package/cli/server-activity.mjs +12 -0
- package/cli/server-proxy.mjs +93 -0
- package/cli/server-registry.mjs +177 -0
- package/cli/server-resources.mjs +22 -0
- package/cli/settings.mjs +15 -0
- package/cli/setup-answers.mjs +29 -0
- package/cli/setup-input.mjs +113 -0
- package/cli/shared.mjs +7 -0
- package/cli/transfers.mjs +33 -0
- package/cli/version.mjs +36 -0
- package/composition/git-source.mjs +111 -0
- package/edition-authoring/files.mjs +81 -0
- package/edition-authoring/manifest.mjs +65 -0
- package/global/README.md +155 -0
- package/global/checkout.mjs +72 -0
- package/global/cli/command.mjs +13 -0
- package/global/cli/install.mjs +75 -0
- package/global/cli/layout.mjs +45 -0
- package/global/cli/status.mjs +54 -0
- package/global/cli/uninstall.mjs +36 -0
- package/global/delegate.mjs +13 -0
- package/global/launch/capacity.mjs +72 -0
- package/global/launch/dev.mjs +129 -0
- package/global/launch/facts.mjs +93 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +61 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +97 -0
- package/global/launch/steps.mjs +81 -0
- package/global/launch/worker.mjs +43 -0
- package/global/list.mjs +59 -0
- package/global/main.mjs +53 -0
- package/global/package-info.mjs +38 -0
- package/global/registry.mjs +24 -0
- package/global/rer.mjs +29 -0
- package/global/update/archive.mjs +89 -0
- package/global/update/command.mjs +90 -0
- package/global/update/latest.mjs +6 -0
- package/global/update/notice.mjs +58 -0
- package/global/update/registry.mjs +47 -0
- package/global/version.mjs +29 -0
- package/install.sh +144 -0
- package/modules/ai/LICENSE +21 -0
- package/modules/ai/README.md +90 -0
- package/modules/ai/client.mjs +71 -0
- package/modules/ai/config.mjs +39 -0
- package/modules/ai/dev/index.d.mts +6 -0
- package/modules/ai/dev/index.mjs +64 -0
- package/modules/ai/dev/outbox.mjs +33 -0
- package/modules/ai/errors.mjs +28 -0
- package/modules/ai/fake/index.d.mts +3 -0
- package/modules/ai/fake/index.mjs +82 -0
- package/modules/ai/index.d.mts +10 -0
- package/modules/ai/index.mjs +3 -0
- package/modules/ai/package.json +6 -0
- package/modules/container/README.md +156 -0
- package/modules/container/adapters/d1.mjs +34 -0
- package/modules/container/adapters/egress.mjs +40 -0
- package/modules/container/adapters/jobs.mjs +47 -0
- package/modules/container/adapters/objects.mjs +84 -0
- package/modules/container/adapters/retained.mjs +14 -0
- package/modules/container/adapters/sqlite-error.mjs +7 -0
- package/modules/container/adapters/sqlite-worker.mjs +26 -0
- package/modules/container/assets.mjs +14 -0
- package/modules/container/bindings.mjs +1 -0
- package/modules/container/boundary.mjs +38 -0
- package/modules/container/build.mjs +65 -0
- package/modules/container/deploy-input.mjs +17 -0
- package/modules/container/entrypoint.sh +5 -0
- package/modules/container/host.mjs +43 -0
- package/modules/container/ingress.mjs +23 -0
- package/modules/container/lock.mjs +11 -0
- package/modules/container/migrations.mjs +16 -0
- package/modules/container/node-engine.mjs +39 -0
- package/modules/container/portability-schema.mjs +14 -0
- package/modules/container/remote/deploy.py +76 -0
- package/modules/container/remote/files.py +275 -0
- package/modules/container/remote/resources.py +157 -0
- package/modules/container/runner.mjs +10 -0
- package/modules/container/ssh-plan.mjs +44 -0
- package/modules/container/ssh.mjs +63 -0
- package/modules/container/transfer.mjs +27 -0
- package/modules/container/worker-engine.mjs +32 -0
- package/modules/dependencies/README.md +211 -0
- package/modules/dependencies/approval.mjs +51 -0
- package/modules/dependencies/auth.mjs +40 -0
- package/modules/dependencies/authority.mjs +14 -0
- package/modules/dependencies/crypto.mjs +13 -0
- package/modules/dependencies/index.d.mts +23 -0
- package/modules/dependencies/index.mjs +113 -0
- package/modules/dependencies/logout.mjs +42 -0
- package/modules/dependencies/native.mjs +9 -0
- package/modules/dependencies/protocol.mjs +23 -0
- package/modules/dependencies/schema/approval-attempt.sql +3 -0
- package/modules/dependencies/schema/dependencies.sql +15 -0
- package/modules/dependencies/store.mjs +19 -0
- package/modules/deploy-kit/README.md +308 -0
- package/modules/deploy-kit/artifact.mjs +47 -0
- package/modules/deploy-kit/build-modules.mjs +21 -0
- package/modules/deploy-kit/check.mjs +13 -0
- package/modules/deploy-kit/cli.mjs +50 -0
- package/modules/deploy-kit/container-config.mjs +8 -0
- package/modules/deploy-kit/default-stage.mjs +14 -0
- package/modules/deploy-kit/deploy.mjs +80 -0
- package/modules/deploy-kit/health-request.mjs +86 -0
- package/modules/deploy-kit/health.mjs +99 -0
- package/modules/deploy-kit/index.mjs +12 -0
- package/modules/deploy-kit/local-only.mjs +10 -0
- package/modules/deploy-kit/migrations.mjs +48 -0
- package/modules/deploy-kit/owner-cli.mjs +63 -0
- package/modules/deploy-kit/owner.mjs +297 -0
- package/modules/deploy-kit/prepare.mjs +62 -0
- package/modules/deploy-kit/provider.mjs +99 -0
- package/modules/deploy-kit/queue-consumer.mjs +10 -0
- package/modules/deploy-kit/receipt.mjs +16 -0
- package/modules/deploy-kit/render.mjs +47 -0
- package/modules/deploy-kit/resources.mjs +106 -0
- package/modules/deploy-kit/service-grants.mjs +25 -0
- package/modules/deploy-kit/sql-safety.mjs +94 -0
- package/modules/deploy-kit/sql-tokens.mjs +49 -0
- package/modules/deploy-kit/teardown.mjs +100 -0
- package/modules/deploy-kit/templates.mjs +31 -0
- package/modules/deploy-kit/validation.mjs +58 -0
- package/modules/deploy-kit/worker.mjs +105 -0
- package/modules/erasure/CONTRACT.md +90 -0
- package/modules/erasure/README.md +221 -0
- package/modules/erasure/audit.mjs +23 -0
- package/modules/erasure/blobs.mjs +45 -0
- package/modules/erasure/cascade.mjs +91 -0
- package/modules/erasure/completion.mjs +62 -0
- package/modules/erasure/declaration.mjs +51 -0
- package/modules/erasure/fence.mjs +19 -0
- package/modules/erasure/index.d.mts +62 -0
- package/modules/erasure/index.mjs +85 -0
- package/modules/erasure/order.mjs +56 -0
- package/modules/erasure/plan.mjs +121 -0
- package/modules/erasure/rate.mjs +57 -0
- package/modules/erasure/receipt.mjs +32 -0
- package/modules/erasure/root.mjs +21 -0
- package/modules/erasure/schema/erasure-rates.sql +5 -0
- package/modules/erasure/schema/erasure.sql +27 -0
- package/modules/erasure/scope.mjs +29 -0
- package/modules/erasure/sql.mjs +23 -0
- package/modules/erasure/verification.mjs +69 -0
- package/modules/integrations/core/CONTRACT.md +50 -0
- package/modules/integrations/core/README.md +27 -0
- package/modules/integrations/core/crypto.ts +19 -0
- package/modules/integrations/core/database.ts +21 -0
- package/modules/integrations/core/delivery.ts +97 -0
- package/modules/integrations/core/destination.ts +13 -0
- package/modules/integrations/core/index.ts +7 -0
- package/modules/integrations/core/node-transport.ts +72 -0
- package/modules/integrations/core/public-address.ts +57 -0
- package/modules/integrations/core/repository.ts +74 -0
- package/modules/integrations/core/schema.sql +36 -0
- package/modules/integrations/core/sqlite.ts +20 -0
- package/modules/integrations/core/timestamped-body-signature.ts +38 -0
- package/modules/integrations/core/tokens.ts +71 -0
- package/modules/integrations/core/types.ts +52 -0
- package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
- package/modules/integrations/core/webhook.ts +73 -0
- package/modules/job-queue/README.md +173 -0
- package/modules/job-queue/cli.mjs +64 -0
- package/modules/job-queue/command.mjs +64 -0
- package/modules/job-queue/config.mjs +48 -0
- package/modules/job-queue/identity.mjs +48 -0
- package/modules/job-queue/index.mjs +135 -0
- package/modules/job-queue/membership.mjs +30 -0
- package/modules/job-queue/module.json +10 -0
- package/modules/job-queue/policy.mjs +29 -0
- package/modules/job-queue/processes.mjs +80 -0
- package/modules/job-queue/resources.mjs +37 -0
- package/modules/job-queue/storage.mjs +64 -0
- package/modules/job-queue/supervisor.mjs +30 -0
- package/modules/job-queue/waiting.mjs +16 -0
- package/modules/mail/LICENSE +21 -0
- package/modules/mail/README.md +336 -0
- package/modules/mail/doctor.mjs +81 -0
- package/modules/mail/index.mjs +8 -0
- package/modules/mail/live-proof/README.md +113 -0
- package/modules/mail/live-proof/run.mjs +55 -0
- package/modules/mail/package.json +19 -0
- package/modules/mail/pnpm-lock.yaml +537 -0
- package/modules/mail/src/adapters/cloudflare/README.md +84 -0
- package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
- package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
- package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
- package/modules/mail/src/adapters/messaging/README.md +92 -0
- package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
- package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
- package/modules/mail/src/adapters/messaging/index.mjs +61 -0
- package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
- package/modules/mail/src/adapters/messaging/request.mjs +52 -0
- package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
- package/modules/mail/src/adapters/resend/README.md +120 -0
- package/modules/mail/src/adapters/resend/index.mjs +22 -0
- package/modules/mail/src/adapters/resend/response.mjs +54 -0
- package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
- package/modules/mail/src/adapters/resend/size.mjs +99 -0
- package/modules/mail/src/adapters/sendgrid/README.md +75 -0
- package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
- package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/core/transport.mjs +33 -0
- package/modules/mail/src/core/validation.mjs +172 -0
- package/modules/mail/src/development/index.d.mts +37 -0
- package/modules/mail/src/development/index.mjs +35 -0
- package/modules/mail/src/development/outbox.mjs +78 -0
- package/modules/mail/src/development/schema.sql +9 -0
- package/modules/mail/src/development/viewer-html.mjs +42 -0
- package/modules/mail/src/development/viewer.mjs +65 -0
- package/modules/mail/src/doctor/README.md +107 -0
- package/modules/mail/src/doctor/common.mjs +64 -0
- package/modules/mail/src/doctor/dns.mjs +102 -0
- package/modules/mail/src/doctor/index.mjs +28 -0
- package/modules/mail/src/doctor/probe.mjs +66 -0
- package/modules/mail/src/doctor/receive.mjs +81 -0
- package/modules/mail/src/doctor/records.mjs +45 -0
- package/modules/mail/src/events/README.md +268 -0
- package/modules/mail/src/events/d1-schema.sql +25 -0
- package/modules/mail/src/events/d1-storage.mjs +76 -0
- package/modules/mail/src/events/index.mjs +3 -0
- package/modules/mail/src/events/intake.mjs +201 -0
- package/modules/mail/src/events/storage.mjs +52 -0
- package/modules/mail/src/events/verify.mjs +92 -0
- package/modules/mail/src/inbound/README.md +137 -0
- package/modules/mail/src/inbound/bytes.mjs +91 -0
- package/modules/mail/src/inbound/headers.mjs +143 -0
- package/modules/mail/src/inbound/index.mjs +29 -0
- package/modules/mail/src/inbound/mime.mjs +36 -0
- package/modules/mail/src/inbound/multipart.mjs +27 -0
- package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
- package/modules/mail/src/inbound/resend.mjs +54 -0
- package/modules/mail/src/inbound/sendgrid.mjs +60 -0
- package/modules/mail/src/sender.mjs +33 -0
- package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
- package/modules/mail/src/sendgrid-email/index.mjs +86 -0
- package/modules/mail/src/unsubscribe/index.mjs +142 -0
- package/modules/operator-auth/README.md +599 -0
- package/modules/operator-auth/accounts/adapter.mjs +41 -0
- package/modules/operator-auth/accounts/admin.mjs +159 -0
- package/modules/operator-auth/accounts/credentials.mjs +27 -0
- package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
- package/modules/operator-auth/accounts/index.d.mts +9 -0
- package/modules/operator-auth/accounts/index.mjs +9 -0
- package/modules/operator-auth/accounts/native.d.mts +38 -0
- package/modules/operator-auth/accounts/native.mjs +171 -0
- package/modules/operator-auth/accounts/oa.mjs +90 -0
- package/modules/operator-auth/accounts/provider-control.mjs +50 -0
- package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
- package/modules/operator-auth/accounts/roles.mjs +26 -0
- package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
- package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
- package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
- package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
- package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
- package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
- package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
- package/modules/operator-auth/accounts/settings-page.mjs +4 -0
- package/modules/operator-auth/accounts/store.mjs +246 -0
- package/modules/operator-auth/branding.mjs +66 -0
- package/modules/operator-auth/config.mjs +35 -0
- package/modules/operator-auth/conformance/README.md +81 -0
- package/modules/operator-auth/conformance/index.d.mts +43 -0
- package/modules/operator-auth/conformance/index.mjs +300 -0
- package/modules/operator-auth/conformance/presentation.mjs +57 -0
- package/modules/operator-auth/crypto.mjs +17 -0
- package/modules/operator-auth/dev/badge.mjs +62 -0
- package/modules/operator-auth/dev/index.d.mts +14 -0
- package/modules/operator-auth/dev/index.mjs +137 -0
- package/modules/operator-auth/dev/personas.mjs +17 -0
- package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
- package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
- package/modules/operator-auth/dev-admission.d.mts +4 -0
- package/modules/operator-auth/dev-admission.mjs +10 -0
- package/modules/operator-auth/dev-gate.mjs +86 -0
- package/modules/operator-auth/docs/README.md +78 -0
- package/modules/operator-auth/docs/native-accounts.md +313 -0
- package/modules/operator-auth/docs/tanstack-start.md +218 -0
- package/modules/operator-auth/docs/vite-spa.md +144 -0
- package/modules/operator-auth/errors.mjs +9 -0
- package/modules/operator-auth/http.mjs +42 -0
- package/modules/operator-auth/identity/admission.mjs +105 -0
- package/modules/operator-auth/identity/after-confirm.mjs +18 -0
- package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
- package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
- package/modules/operator-auth/identity/email-link/index.mjs +128 -0
- package/modules/operator-auth/identity/email-link/message.mjs +16 -0
- package/modules/operator-auth/identity/index.d.mts +7 -0
- package/modules/operator-auth/identity/index.mjs +5 -0
- package/modules/operator-auth/identity/oidc/README.md +171 -0
- package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
- package/modules/operator-auth/identity/oidc/config.mjs +102 -0
- package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
- package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
- package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
- package/modules/operator-auth/identity/oidc/index.mjs +287 -0
- package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
- package/modules/operator-auth/identity/oidc/network.mjs +93 -0
- package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
- package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
- package/modules/operator-auth/identity/oidc/store.mjs +68 -0
- package/modules/operator-auth/identity/principal.mjs +28 -0
- package/modules/operator-auth/index.d.mts +223 -0
- package/modules/operator-auth/index.mjs +11 -0
- package/modules/operator-auth/input.mjs +55 -0
- package/modules/operator-auth/local-cli.mjs +16 -0
- package/modules/operator-auth/page.mjs +34 -0
- package/modules/operator-auth/policy.mjs +20 -0
- package/modules/operator-auth/routes.mjs +91 -0
- package/modules/operator-auth/service.mjs +152 -0
- package/modules/operator-auth/signin/index.mjs +67 -0
- package/modules/owner-claim/README.md +137 -0
- package/modules/owner-claim/idp.mjs +40 -0
- package/modules/owner-claim/index.d.mts +27 -0
- package/modules/owner-claim/index.mjs +39 -0
- package/modules/owner-claim/page.mjs +11 -0
- package/modules/owner-claim/schema/idp.sql +4 -0
- package/modules/owner-claim/schema/owner-claim.sql +18 -0
- package/modules/owner-claim/transaction.mjs +87 -0
- package/modules/owner-claim/verify.mjs +80 -0
- package/modules/portability/README.md +444 -0
- package/modules/portability/bridge.mjs +102 -0
- package/modules/portability/catalogue.mjs +118 -0
- package/modules/portability/cli-options.mjs +38 -0
- package/modules/portability/cli.mjs +56 -0
- package/modules/portability/copies.mjs +68 -0
- package/modules/portability/dev-data.mjs +32 -0
- package/modules/portability/diff.mjs +33 -0
- package/modules/portability/export.mjs +129 -0
- package/modules/portability/files.mjs +65 -0
- package/modules/portability/format.mjs +122 -0
- package/modules/portability/generated.mjs +60 -0
- package/modules/portability/hash.mjs +79 -0
- package/modules/portability/http.mjs +42 -0
- package/modules/portability/import.mjs +185 -0
- package/modules/portability/index.d.mts +58 -0
- package/modules/portability/index.mjs +22 -0
- package/modules/portability/journal.mjs +49 -0
- package/modules/portability/legacy-files.mjs +43 -0
- package/modules/portability/legacy-safety.mjs +27 -0
- package/modules/portability/lines.mjs +36 -0
- package/modules/portability/migration-history.mjs +18 -0
- package/modules/portability/migration-statements.mjs +23 -0
- package/modules/portability/payloads.mjs +52 -0
- package/modules/portability/provider-data.mjs +25 -0
- package/modules/portability/restore-safety.mjs +30 -0
- package/modules/portability/schema/portability.sql +25 -0
- package/modules/portability/storage.mjs +49 -0
- package/modules/release/README.md +212 -0
- package/modules/release/catalogue.mjs +79 -0
- package/modules/release/export.mjs +51 -0
- package/modules/release/produce.mjs +136 -0
- package/modules/release/public-child-transport-state.cjs +29 -0
- package/modules/release/public-child.mjs +71 -0
- package/modules/release/public-git.mjs +21 -0
- package/modules/release/publication-scan.mjs +22 -0
- package/modules/release/publish-handoff.mjs +37 -0
- package/modules/release/source.mjs +71 -0
- package/modules/release/test-summary.mjs +50 -0
- package/onboarding/README.md +163 -0
- package/onboarding/activity.mjs +31 -0
- package/onboarding/adapter-auth.mjs +88 -0
- package/onboarding/adapter-hosting.mjs +35 -0
- package/onboarding/adapter-labels.mjs +13 -0
- package/onboarding/adapter-settings.mjs +129 -0
- package/onboarding/adapter.mjs +238 -0
- package/onboarding/agent-handoff.mjs +9 -0
- package/onboarding/app-processes.mjs +21 -0
- package/onboarding/archive-output.d.mts +3 -0
- package/onboarding/archive-output.mjs +54 -0
- package/onboarding/command-custody.mjs +24 -0
- package/onboarding/command-recovery.mjs +8 -0
- package/onboarding/commands.mjs +166 -0
- package/onboarding/dependency-approval.mjs +36 -0
- package/onboarding/dev-identity.mjs +23 -0
- package/onboarding/email.mjs +24 -0
- package/onboarding/failures.mjs +42 -0
- package/onboarding/input-limits.mjs +7 -0
- package/onboarding/installer-inputs.mjs +24 -0
- package/onboarding/launcher.mjs +34 -0
- package/onboarding/local/build-files.mjs +64 -0
- package/onboarding/local/build.mjs +49 -0
- package/onboarding/local/environment.mjs +30 -0
- package/onboarding/local/files.mjs +35 -0
- package/onboarding/local/operation.mjs +166 -0
- package/onboarding/local/owner.mjs +34 -0
- package/onboarding/local/process-table.mjs +114 -0
- package/onboarding/local/recovery.mjs +79 -0
- package/onboarding/local/state.mjs +31 -0
- package/onboarding/metadata.mjs +7 -0
- package/onboarding/operator-install.mjs +45 -0
- package/onboarding/operator-installer.mjs +39 -0
- package/onboarding/operator-prerequisites.mjs +27 -0
- package/onboarding/package.json +6 -0
- package/onboarding/prefill.mjs +16 -0
- package/onboarding/private-files.mjs +97 -0
- package/onboarding/private.mjs +35 -0
- package/onboarding/process-cleanup.mjs +134 -0
- package/onboarding/process-diagnostics.mjs +91 -0
- package/onboarding/process-scope.mjs +118 -0
- package/onboarding/process-table.mjs +118 -0
- package/onboarding/process-tree.mjs +36 -0
- package/onboarding/production-modules.mjs +22 -0
- package/onboarding/production-options.mjs +40 -0
- package/onboarding/production-plan.mjs +8 -0
- package/onboarding/production-runtime.mjs +30 -0
- package/onboarding/project-vite.mjs +14 -0
- package/onboarding/providers.mjs +289 -0
- package/onboarding/queue.mjs +53 -0
- package/onboarding/queued-entry.mjs +34 -0
- package/onboarding/readiness.mjs +59 -0
- package/onboarding/redaction-jwt.mjs +32 -0
- package/onboarding/redaction.mjs +298 -0
- package/onboarding/schema/SETUP.schema.json +514 -0
- package/onboarding/schema/dependencies.mjs +35 -0
- package/onboarding/schema/json-schema.mjs +202 -0
- package/onboarding/schema/setup-validation.mjs +325 -0
- package/onboarding/secrets.mjs +21 -0
- package/onboarding/server.mjs +188 -0
- package/onboarding/setup-manifest.mjs +116 -0
- package/onboarding/state.mjs +46 -0
- package/onboarding/theme-files.mjs +37 -0
- package/onboarding/theme.mjs +118 -0
- package/onboarding/ui/activity.js +98 -0
- package/onboarding/ui/address-client.mjs +33 -0
- package/onboarding/ui/app.js +377 -0
- package/onboarding/ui/index.html +1 -0
- package/onboarding/ui/model-core.mjs +127 -0
- package/onboarding/ui/model.mjs +5 -0
- package/onboarding/ui/style.css +63 -0
- package/onboarding/ui/theme.css +37 -0
- package/onboarding/ui/workspace-art.js +30 -0
- package/onboarding/wrangler-auth.mjs +43 -0
- package/operations/README.md +246 -0
- package/operations/operations.mjs +168 -0
- package/operations/project-operations.mjs +157 -0
- package/package.json +11 -4
- package/scripts/publish-check/README.md +72 -0
- package/scripts/publish-check/checksum_maps.py +52 -0
- package/scripts/publish-check/cli.py +34 -0
- package/scripts/publish-check/command_arguments.py +51 -0
- package/scripts/publish-check/command_shell_quotes.py +41 -0
- package/scripts/publish-check/command_source_quotes.py +120 -0
- package/scripts/publish-check/content_identities.py +250 -0
- package/scripts/publish-check/copy_identities.py +41 -0
- package/scripts/publish-check/der.py +61 -0
- package/scripts/publish-check/destination.py +117 -0
- package/scripts/publish-check/env_values.py +74 -0
- package/scripts/publish-check/font_licence.py +23 -0
- package/scripts/publish-check/git_history.py +115 -0
- package/scripts/publish-check/github_refs.py +217 -0
- package/scripts/publish-check/gitleaks.py +72 -0
- package/scripts/publish-check/gitleaks.toml +40 -0
- package/scripts/publish-check/history_scan.py +176 -0
- package/scripts/publish-check/hit_location.py +18 -0
- package/scripts/publish-check/hygiene_config.py +42 -0
- package/scripts/publish-check/hygiene_text.py +11 -0
- package/scripts/publish-check/identity-recipe.mjs +54 -0
- package/scripts/publish-check/json_identity.py +79 -0
- package/scripts/publish-check/license-exceptions.json +11 -0
- package/scripts/publish-check/licenses.py +49 -0
- package/scripts/publish-check/policy.py +18 -0
- package/scripts/publish-check/project_identity.py +89 -0
- package/scripts/publish-check/provenance.py +91 -0
- package/scripts/publish-check/provenance_pins.py +33 -0
- package/scripts/publish-check/public_base.py +63 -0
- package/scripts/publish-check/public_hygiene.py +80 -0
- package/scripts/publish-check/publication-config.json +12 -0
- package/scripts/publish-check/publication_config.py +34 -0
- package/scripts/publish-check/report.py +52 -0
- package/scripts/publish-check/repository_commands.py +75 -0
- package/scripts/publish-check/repository_intent.py +52 -0
- package/scripts/publish-check/reviewed.py +41 -0
- package/scripts/publish-check/source_hashes.mjs +6 -0
- package/scripts/publish-check/tree_scan.py +88 -0
- package/scripts/publish-check/trust_bundle.py +87 -0
- package/scripts/publish-check.mjs +58 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Portable mail
|
|
2
|
+
|
|
3
|
+
`index.mjs` is the IF-056-MAIL-v1 ESM entry, usable from Node and Workers without a build. Applications own authorization, verified sender setup, durable delivery policy, replay storage, attachment storage and inbox reconciliation. An accepted API submission is not proof of mailbox receipt.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createMailSender } from './modules/mail/index.mjs';
|
|
7
|
+
|
|
8
|
+
// The provider credential is a deployment secret, read from the environment.
|
|
9
|
+
const { RESEND_API_KEY: apiKey } = env;
|
|
10
|
+
const sender = createMailSender({ provider: 'resend', mode: 'production', apiKey });
|
|
11
|
+
const result = await sender.send({
|
|
12
|
+
deliveryId: durableCommand.id,
|
|
13
|
+
from: 'Support <support@example.com>',
|
|
14
|
+
to: ['recipient@example.net'],
|
|
15
|
+
subject: 'Your requested sign-in link',
|
|
16
|
+
text: 'Open the link you requested.',
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Adopting
|
|
21
|
+
|
|
22
|
+
A seed copies this module's runtime files byte for byte, to the same paths under its root, and records them in its source lock. Every seed copies the same one module: one SendGrid transport, and the development outbox. The same facade also supports optional signed Messaging links; unlinked sends and the local outbox keep their existing configuration.
|
|
23
|
+
|
|
24
|
+
### Mail
|
|
25
|
+
|
|
26
|
+
Every seed copies these files. `index.mjs` loads all of them, including the outbox below, so none can be left out.
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
modules/mail/LICENSE
|
|
30
|
+
modules/mail/index.mjs
|
|
31
|
+
modules/mail/src/sender.mjs
|
|
32
|
+
modules/mail/src/core/errors.mjs
|
|
33
|
+
modules/mail/src/core/transport.mjs
|
|
34
|
+
modules/mail/src/core/validation.mjs
|
|
35
|
+
modules/mail/src/adapters/cloudflare/headers.mjs
|
|
36
|
+
modules/mail/src/adapters/cloudflare/index.mjs
|
|
37
|
+
modules/mail/src/adapters/cloudflare/response.mjs
|
|
38
|
+
modules/mail/src/adapters/resend/index.mjs
|
|
39
|
+
modules/mail/src/adapters/resend/response.mjs
|
|
40
|
+
modules/mail/src/adapters/resend/serialize.mjs
|
|
41
|
+
modules/mail/src/adapters/resend/size.mjs
|
|
42
|
+
modules/mail/src/adapters/sendgrid/index.mjs
|
|
43
|
+
modules/mail/src/adapters/sendgrid/serialize.mjs
|
|
44
|
+
modules/mail/src/adapters/messaging/contract.mjs
|
|
45
|
+
modules/mail/src/adapters/messaging/event-schema.mjs
|
|
46
|
+
modules/mail/src/adapters/messaging/index.mjs
|
|
47
|
+
modules/mail/src/adapters/messaging/jws.mjs
|
|
48
|
+
modules/mail/src/adapters/messaging/request.mjs
|
|
49
|
+
modules/mail/src/adapters/messaging/serialize.mjs
|
|
50
|
+
modules/mail/src/unsubscribe/index.mjs
|
|
51
|
+
modules/mail/src/events/index.mjs
|
|
52
|
+
modules/mail/src/events/intake.mjs
|
|
53
|
+
modules/mail/src/events/storage.mjs
|
|
54
|
+
modules/mail/src/events/d1-storage.mjs
|
|
55
|
+
modules/mail/src/events/verify.mjs
|
|
56
|
+
modules/mail/src/inbound/bytes.mjs
|
|
57
|
+
modules/mail/src/inbound/headers.mjs
|
|
58
|
+
modules/mail/src/inbound/index.mjs
|
|
59
|
+
modules/mail/src/inbound/mime.mjs
|
|
60
|
+
modules/mail/src/inbound/multipart.mjs
|
|
61
|
+
modules/mail/src/inbound/resend-attachment.mjs
|
|
62
|
+
modules/mail/src/inbound/resend.mjs
|
|
63
|
+
modules/mail/src/inbound/sendgrid.mjs
|
|
64
|
+
modules/mail/src/doctor/common.mjs
|
|
65
|
+
modules/mail/src/doctor/dns.mjs
|
|
66
|
+
modules/mail/src/doctor/index.mjs
|
|
67
|
+
modules/mail/src/doctor/probe.mjs
|
|
68
|
+
modules/mail/src/doctor/receive.mjs
|
|
69
|
+
modules/mail/src/doctor/records.mjs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Local outbox
|
|
73
|
+
|
|
74
|
+
Every seed copies this whole folder, every file in it. It is what lets a seed sign in locally without a real mail account. Apply `schema.sql` to the local database during local setup (see [Adopting the local outbox](#adopting-the-local-outbox)).
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
modules/mail/src/development/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### D1 event intake
|
|
81
|
+
|
|
82
|
+
For durable delivery-event intake on D1, copy this additive schema into a new
|
|
83
|
+
application migration and apply it before using `createD1EventStore`. It is
|
|
84
|
+
separate from the JavaScript runtime files and the local outbox migration.
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
modules/mail/src/events/d1-schema.sql
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Sign-in mail
|
|
91
|
+
|
|
92
|
+
Sign-in mail is the seed's own wiring around the two things above, not a copy of module files. The reference is the Base default seed; adapt these files for the seed's own auth and keep its own database binding and migration directory:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
default-seed/src/core/platform/auth/mail.server.mjs
|
|
96
|
+
default-seed/src/core/platform/auth/outbox.server.mjs
|
|
97
|
+
default-seed/migrations/core/0004_development_outbox.sql
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`mail.server.mjs` builds the production sender from `MAIL_PROVIDER` (`resend`, `sendgrid` or `cloudflare`) and, only under explicit local configuration, the development sender. `outbox.server.mjs` serves the loopback-only viewer. The migration is the outbox table from `schema.sql`.
|
|
101
|
+
|
|
102
|
+
### Former sendgrid-email API
|
|
103
|
+
|
|
104
|
+
A seed that already calls the former sendgrid-email module copies these two files as well, and changes its import from `modules/sendgrid-email/sender` to `modules/mail/src/sendgrid-email/index.mjs`. The exports (`sendSendGrid`, `serializeSendGridMessage`, `SendGridSendError`, `SendGridMessage`) are unchanged. A seed with no such calls does not copy them; new code uses `createMailSender` with `provider: 'sendgrid'`. See [the former sendgrid-email API](#the-former-sendgrid-email-api).
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
modules/mail/src/sendgrid-email/index.mjs
|
|
108
|
+
modules/mail/src/sendgrid-email/index.d.mts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Not copied
|
|
112
|
+
|
|
113
|
+
These are for developing the module itself or documentation. A seed may keep any of them for reference:
|
|
114
|
+
|
|
115
|
+
- `modules/mail/tests/`, `modules/mail/live-proof/`, `modules/mail/doctor.mjs`
|
|
116
|
+
- `modules/mail/package.json`, `modules/mail/pnpm-lock.yaml`, `modules/mail/.gitignore`
|
|
117
|
+
- `modules/mail/README.md` and every `README.md` under `modules/mail/src/`
|
|
118
|
+
|
|
119
|
+
The provider is explicit: `sendgrid`, `resend`, `cloudflare`, or `development`.
|
|
120
|
+
`mode` is required (`production`, `development`, or `test`). Remote providers require `apiKey`; Cloudflare additionally requires a 32-hex `accountId`. A remote provider in test/development mode still makes a real provider request unless the caller explicitly supplies a test fetch. Missing credentials, validation errors, or provider failure never select another provider. Config and messages reject unknown fields so requested features cannot silently disappear.
|
|
121
|
+
|
|
122
|
+
`createMailSender(config, dependencies?)` returns `{send(message)}`. Dependencies are optional standard `fetch`, an explicit asynchronous `outbox.put(message)`, `clock: () => epochMilliseconds`, and `timeoutMilliseconds` (a positive safe integer bounding each remote attempt; the default is 15,000). Production normally uses global fetch. No configurable provider endpoint, retry, or automatic replay exists. The sender snapshots configuration at construction and the message at submission.
|
|
123
|
+
|
|
124
|
+
## Message and results
|
|
125
|
+
|
|
126
|
+
Required fields: `deliveryId` (nonempty, at most 512 characters), `from`, nonempty `to: string[]`, `subject: string` (SendGrid requires nonempty; other providers allow empty), and at least one nonempty `text` or `html`. Addresses accept a mailbox or `Display Name <mailbox>`; the conservative mailbox grammar rejects controls, multiple addresses inside one string and malformed domains. Use arrays for recipients.
|
|
127
|
+
|
|
128
|
+
Optional `cc`, `bcc`, `replyTo`, and string-valued `headers` preserve envelope and threading. `In-Reply-To` and ordered `References` can be supplied as headers. SendGrid and Resend also accept `Message-ID`; Cloudflare generates it and rejects a caller-supplied value. CR/LF/control characters, duplicate header names ignoring case, reserved envelope/MIME/security headers and invalid header names fail before a request. Provider-specific header restrictions are also enforced.
|
|
129
|
+
|
|
130
|
+
Attachments use `{filename, contentType, content: Uint8Array, disposition?: 'attachment'|'inline', contentId?}`. Content is raw bytes, never a base64 string at the public boundary. Inline attachments require a content ID. Unsupported attachment features fail explicitly. Provider size and count limits still apply.
|
|
131
|
+
|
|
132
|
+
Resend checks a conservative 40,000,000-byte total email budget before Base64 or
|
|
133
|
+
JSON encoding, including UTF-8 bodies, all recipients/headers, aggregate Base64
|
|
134
|
+
attachments and MIME overhead. Its final JSON request is capped at 39,000,000
|
|
135
|
+
UTF-8 bytes. Oversize input throws `MailValidationError` with fixed code
|
|
136
|
+
`resend_message_too_large` before any send or outbox effect. See the precise
|
|
137
|
+
[Resend size budget](src/adapters/resend/README.md#outbound-size-budget); it is a
|
|
138
|
+
local guard and may reject input below the provider's maximum.
|
|
139
|
+
|
|
140
|
+
SendGrid additionally supports `customArgs: Record<string,string>`, positive safe-integer `asmGroupId`, and boolean `subscriptionTracking`. The custom argument JSON including `rer_delivery_id` is limited to 10,000 UTF-8 bytes. Caller arguments cannot replace `rer_delivery_id`, `email`, `event`, `timestamp`, `sg_event_id`, or `sg_message_id`. Resend and Cloudflare reject these SendGrid-only settings, including empty `customArgs` and `subscriptionTracking: false`.
|
|
141
|
+
|
|
142
|
+
`send` returns `{provider,status,providerMessageId,code?,recipientResults?}`. Status is `accepted`, `rejected`, `rate_limited`, `uncertain`, or `development`. `providerMessageId` is a real provider receipt or null; local correlation never fabricates one. A validation/configuration error throws a redacted error with a fixed code. Errors/results never expose raw provider response bodies or credentials.
|
|
143
|
+
|
|
144
|
+
- SendGrid requires HTTP 202 for acceptance. Its `X-Message-Id` is nullable. `rer_delivery_id` is correlation, not duplicate suppression. Click, open, and Google Analytics tracking are disabled; subscription tracking is enabled only when requested. `serializeSendGridMessage(message)` exposes validated wire bytes for callers that own durable fingerprints.
|
|
145
|
+
- Resend uses `deliveryId` as `Idempotency-Key` and preserves the returned ID. Resend retains idempotency keys for 24 hours; a reused key with a different payload conflicts. This module does not retry or assume indefinite duplicate suppression. See [Resend idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys).
|
|
146
|
+
- Cloudflare uses the [Email Sending REST API](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/), separate from Email Routing's restricted send binding. Both `success` and recipient result data are checked. Partial recipient outcomes remain visible in `recipientResults`; API send results do not assert actual inbox receipt.
|
|
147
|
+
|
|
148
|
+
Network loss, redirects, HTTP 408 and 5xx are conservatively `uncertain`; 429 is `rate_limited`, and other definite 4xx rejection is `rejected`. Provider success statuses require their documented semantics. Reconcile uncertain attempts before deciding whether a new submission is safe. No adapter retries or follows redirects.
|
|
149
|
+
|
|
150
|
+
## Development and unsubscribe
|
|
151
|
+
|
|
152
|
+
Development requires all three: `provider:'development'`, `mode:'development'`, and explicit `dependencies.outbox.put`. It captures the prepared message with its raw attachment bytes and returns `development`, never remote acceptance. Configure storage only in your development host; no route is mounted automatically and no network replay is provided. Treat captured messages and sign-in links as sensitive local data.
|
|
153
|
+
|
|
154
|
+
### Adopting the local outbox
|
|
155
|
+
|
|
156
|
+
The typed `src/development/index.mjs` subpath exports
|
|
157
|
+
`createD1DevelopmentOutbox({db,maxMessages?,maxAgeMs?,clock?})` and
|
|
158
|
+
`createDevelopmentOutboxHandler({local,origin,outbox,path?,theme?})`. The root facade's
|
|
159
|
+
exports remain stable. `theme` is the seed's served theme stylesheet, a
|
|
160
|
+
root-relative `.css` path such as `'/rer-theme.css'` (the same grammar as
|
|
161
|
+
operator-auth `branding.theme`; any other value throws when the handler is
|
|
162
|
+
created). With it the page links that file, reads its `--rer-*` tokens with
|
|
163
|
+
readable fallbacks and a visible 3px focus ring, and its policy allows
|
|
164
|
+
same-origin styles, fonts and images (still no script). Without it the page is
|
|
165
|
+
unchanged. Apply the additive [schema](src/development/schema.sql)
|
|
166
|
+
to your local D1 database during local setup, for example:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
pnpm exec wrangler d1 execute DB --local --file=../modules/mail/src/development/schema.sql
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Use the actual database binding and source path for your host. The table and
|
|
173
|
+
index are namespaced `rer_mail_development_outbox`; the schema is repeatable and
|
|
174
|
+
does not replace application tables. Keep the local D1 persistence directory
|
|
175
|
+
between restarts. No provider credentials are needed for local capture.
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import { createMailSender } from './modules/mail/index.mjs';
|
|
179
|
+
import {
|
|
180
|
+
createD1DevelopmentOutbox, createDevelopmentOutboxHandler,
|
|
181
|
+
} from './modules/mail/src/development/index.mjs';
|
|
182
|
+
|
|
183
|
+
// These values come from trusted, explicit local host configuration.
|
|
184
|
+
const outbox = createD1DevelopmentOutbox({ db: env.DB });
|
|
185
|
+
const sender = createMailSender(
|
|
186
|
+
{ provider: 'development', mode: 'development' }, { outbox },
|
|
187
|
+
);
|
|
188
|
+
const viewOutbox = createDevelopmentOutboxHandler({
|
|
189
|
+
local: localConfiguration.enabled === true,
|
|
190
|
+
origin: localConfiguration.origin, // e.g. 'http://localhost:8787'
|
|
191
|
+
outbox,
|
|
192
|
+
});
|
|
193
|
+
// In the host's request handler, before the application catch-all:
|
|
194
|
+
const response = await viewOutbox(request);
|
|
195
|
+
if (response) return response;
|
|
196
|
+
// Continue normal application routing otherwise.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Wire the application's real sign-in mail call to `sender.send`; the regular
|
|
200
|
+
message composition and unsubscribe assembly still run. Make this selection
|
|
201
|
+
only in the explicit local configuration. Production keeps its configured
|
|
202
|
+
remote sender, and does not mount the development viewer. Missing provider
|
|
203
|
+
credentials or a delivery failure must never select development mode.
|
|
204
|
+
|
|
205
|
+
`put(preparedMessage)` resolves without a receipt; `list()` returns captures
|
|
206
|
+
newest first, and `get(id)` returns a capture or `null`. A capture is
|
|
207
|
+
`{id,createdAt,message}`, with epoch milliseconds and a unique capture ID.
|
|
208
|
+
Repeated delivery IDs remain separate captures. Attachment content returns as
|
|
209
|
+
`Uint8Array`; storage encodes those bytes losslessly as base64 inside JSON.
|
|
210
|
+
The default retention is 100 messages and seven days. Both limits must be
|
|
211
|
+
positive safe integers. Every read and write prunes age and count in the same
|
|
212
|
+
D1 batch as the operation; a message expires exactly at `createdAt + maxAgeMs`.
|
|
213
|
+
Equal timestamps retain the most recently inserted captures first. A failing
|
|
214
|
+
batch fails the operation without a process-memory fallback.
|
|
215
|
+
|
|
216
|
+
The viewer lives at `/__dev/outbox` (or the explicit `path`) and accepts GET and
|
|
217
|
+
HEAD. It returns `null` unless `local === true`, the configured origin is an
|
|
218
|
+
exact serialized HTTP(S) loopback origin, and the request URL and Host match
|
|
219
|
+
its scheme and authority. `localhost`, canonical `127.x.x.x`, and `[::1]` are
|
|
220
|
+
supported. Trailing slashes, credentials, paths, queries, fragments, whitespace
|
|
221
|
+
and alternate URL spellings in the configured origin are refused. Host must
|
|
222
|
+
include the configured non-default port. Forwarded headers never grant access;
|
|
223
|
+
a different Origin or a cross-site fetch is also refused before any D1 read.
|
|
224
|
+
The host must bind its development listener to loopback; these request checks
|
|
225
|
+
do not replace that listener configuration.
|
|
226
|
+
|
|
227
|
+
The listing shows subject, recipient and received time. A message detail shows
|
|
228
|
+
escaped text (escaped source for HTML-only messages) and a large **Open this
|
|
229
|
+
link** action for the first valid HTTP(S) link. It never renders live email
|
|
230
|
+
HTML or attachment content. All viewer responses, including errors, carry
|
|
231
|
+
no-store, noindex, nosniff and a strict CSP with an exact static-style hash and
|
|
232
|
+
scripts disabled. The layout wraps long content on a 390px viewport.
|
|
233
|
+
|
|
234
|
+
First-party seeds route local setup through `rer setup` and
|
|
235
|
+
declare their actual mail, auth, database and migration behavior in the RER
|
|
236
|
+
adapter. Base's default seed is the reference for loopback development mail and
|
|
237
|
+
the private local outbox. Follow the runnable checkout's root
|
|
238
|
+
`RUN-LOCALLY.md` for current command and state requirements; the outbox module does not
|
|
239
|
+
replace a seed's owner setup, database bindings, migration directory or sign-in
|
|
240
|
+
policy.
|
|
241
|
+
|
|
242
|
+
`unsubscribe: {url, mailto?, oneClick?}` supplies an HTTPS unsubscribe URL and optional mailbox. One-click generates `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. Insecure URLs, injection and contradictory custom unsubscribe headers fail before dispatch. `applyUnsubscribeHeaders(message)` returns a new prepared message with the `unsubscribe` request removed. Passing an already prepared message to the facade is safe. Direct adapters reject unconsumed requests before any effect; the facade performs assembly once before dispatch. Applications own unsubscribe authorization and processing. Confirm delivered headers and DKIM coverage with a real delivered message; local serialization is not one-click acceptance.
|
|
243
|
+
|
|
244
|
+
## Optional Messaging link
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const sender = createMailSender({
|
|
248
|
+
provider: 'messaging', mode: 'production',
|
|
249
|
+
serviceOrigin: 'https://messaging.example.com', installId, kid, signingKey,
|
|
250
|
+
});
|
|
251
|
+
const result = await sender.send({ ...message, kind: 'transactional',
|
|
252
|
+
expiresAt: '2027-01-01T12:00:00Z' });
|
|
253
|
+
const receipt = await sender.reconcile(message.deliveryId); // null on scoped 404
|
|
254
|
+
await sender.erase('Reader@example.net');
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The ES256 private JWK belongs to this application. The application holds no
|
|
258
|
+
Messaging API key or underlying provider key. A Worker may supply
|
|
259
|
+
`{serviceBinding:env.MESSAGING}` as the second argument; requests still carry the
|
|
260
|
+
same signature. The linked path makes one request, including on timeout; it never
|
|
261
|
+
falls back to a provider or development outbox. Preserve durable delivery IDs and
|
|
262
|
+
use reconciliation after an uncertain result. For same-account public Worker
|
|
263
|
+
fetches, configure `global_fetch_strictly_public`; the optional binding avoids
|
|
264
|
+
that public fetch requirement. Neither path changes the signature contract.
|
|
265
|
+
|
|
266
|
+
Messaging requires exactly one `to` recipient and a granted `kind` (`auth`,
|
|
267
|
+
`transactional`, `bulk`). It carries optional RFC3339 `expiresAt`; direct providers
|
|
268
|
+
reject these service-only fields because they cannot promise expiry. Base
|
|
269
|
+
preserves attachment bytes and prepares unsubscribe headers before signing.
|
|
270
|
+
Applications own consent, unsubscribes, business ledgers and retry scheduling.
|
|
271
|
+
A rate-limited `retryAfter` is an absolute epoch timestamp in seconds. A synchronous
|
|
272
|
+
suppression includes its reason and must be applied to the application's ledger.
|
|
273
|
+
|
|
274
|
+
[The Messaging contract](src/adapters/messaging/README.md) defines every signed
|
|
275
|
+
byte, result, error, reconcile and erasure shape, plus pinned event JWS intake.
|
|
276
|
+
For a D1 application, copy `src/events/d1-schema.sql` into a **new** migration and
|
|
277
|
+
use [the D1 event constructor](src/events/README.md). No migration, linking,
|
|
278
|
+
provider switch or credential removal happens automatically. Self-hosted installs
|
|
279
|
+
stay unlinked until the owner configures them; local development still uses the
|
|
280
|
+
existing D1 outbox without a Messaging service.
|
|
281
|
+
|
|
282
|
+
## The former sendgrid-email API
|
|
283
|
+
|
|
284
|
+
The former sendgrid-email module was a second SendGrid transport. It is now deprecated and re-exports this module, so the repository holds one transport. [`src/sendgrid-email/index.mjs`](src/sendgrid-email/index.mjs) keeps the former API for existing adopters, typed by `index.d.mts`, and builds every request with this module's one SendGrid adapter. Both import paths therefore send identical requests; `modules/sendgrid-email/tests/equivalence.test.ts` proves it against a fake SendGrid API.
|
|
285
|
+
|
|
286
|
+
Switch the import and nothing else:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// before
|
|
290
|
+
import { sendSendGrid, serializeSendGridMessage, SendGridSendError } from './modules/sendgrid-email/sender'
|
|
291
|
+
// after
|
|
292
|
+
import { sendSendGrid, serializeSendGridMessage, SendGridSendError } from './modules/mail/src/sendgrid-email/index.mjs'
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
New code should use `createMailSender({ provider: 'sendgrid', mode: 'production', apiKey })`, which returns a structured result instead of throwing.
|
|
296
|
+
|
|
297
|
+
The former shape is unchanged. `sendSendGrid({ apiKey, message, fetch?, timeoutMilliseconds? })` makes one attempt and resolves `{ provider: 'sendgrid', providerMessageId, state: 'accepted' }` on HTTP 202; `providerMessageId` is null when the receipt is missing or malformed. Anything else throws `SendGridSendError` with an `outcome` and the HTTP `status` when there was one: `rejected` (definite 4xx or invalid input, never sent), `rate_limited` (429) or `ambiguous` (network loss, timeout, 408, 5xx, redirect or any other non-202 status; reconcile before resending). Messages carry `deliveryId`, `from`, `to`, `subject`, `text` and optionally `html`, `replyTo`, `headers`, `customArgs`, `asmGroupId`, `subscriptionTracking` and base64 `attachments` (`{content, filename, type?}`). `serializeSendGridMessage` returns the validated wire bytes for claim fingerprints. `timeoutMilliseconds` is the dependency of the same name described above.
|
|
298
|
+
|
|
299
|
+
Because the one adapter now builds the request, the former module takes this module's validation. Input the provider would refuse is refused earlier and with a fixed code, never echoing values: an empty subject, empty text or attachment content, duplicate recipients, a sender with non-ASCII characters in the mailbox, header names outside the token grammar, `Mime-Version`, `Return-Path` and `Sender` headers, and an API key containing whitespace. Attachment content is decoded from base64 and written back in canonical padded form, and an attachment without `type` is sent with the generic binary content type, octet-stream. Attachment members are written in this module's order (`content`, `type`, `filename`, `disposition`), so a durable fingerprint of a message with attachments taken before the switch will not match one taken after. A message without attachments serializes to the same bytes as before. Consumers that stored receipts as strings must allow null after acceptance, keep durable correlation in `rer_delivery_id`, and never replace a missing remote receipt with that local identity.
|
|
300
|
+
|
|
301
|
+
## Verified events and inbound mail
|
|
302
|
+
|
|
303
|
+
`verifyWebhook({provider,rawBody,headers,key,now,toleranceSeconds?})` returns a boolean. Providers are `sendgrid` and `resend`; `rawBody` is the exact `Uint8Array`, `headers` a `Headers`, and `now` epoch milliseconds. Default clock tolerance is 300 seconds. SendGrid uses base64 SPKI P-256 public keys and DER ECDSA signatures over timestamp plus exact bytes; Resend uses a `whsec_` base64 HMAC secret and Svix framing. Never decode and reserialize JSON or multipart before verification. Signature validity alone establishes neither schema validity nor sender identity. `MAX_WEBHOOK_BYTES` exports the 32 MiB verifier bound; delivery-event intake has its own 1 MiB bound. `createDeliveryEventIntake({provider,tenantId,eventNamespace,key?,storage,authorize,clock?,cloudflareSource?})` verifies, authorizes, then commits receipts and recipient states in one durable transaction. It exposes `ingestWebhook({rawBody,headers})` for SendGrid/Resend or `consumeQueue(batch)` for the trusted Cloudflare queue boundary. Supply actual serializable durable storage (Durable Object SQLite, or `store:createD1EventStore({db:env.DB})` with its additive migration); there is no process-local fallback. See the [event integration contract](src/events/README.md) for atomic deduplication and recipient reconciliation.
|
|
304
|
+
|
|
305
|
+
`normalizeInbound(input, limits)` returns `{provider,messageId,from,to,subject,text,html,inReplyTo,references,attachments}`. Nullable fields remain null, empty subject remains valid, references retain order, and attachment content remains decoded bytes. Required positive-integer limits are `maxRawBytes`, `maxBodyBytes`, `maxAttachments`, `maxAttachmentBytes`, and `maxTotalAttachmentBytes`; limits apply to actual bytes, including streams and decoded attachments. Over-limit/malformed messages fail atomically.
|
|
306
|
+
|
|
307
|
+
Inputs are Cloudflare `{provider:'cloudflare',message:{from,to,headers,raw,rawSize}}` from a Worker email handler, SendGrid `{provider:'sendgrid',rawBody,contentType}` after original multipart signature verification, or Resend `{provider:'resend',email,signedEmailId,fetchAttachment?}` after a verified `email.received` event and checked hydration identity. No unsigned HTTP JSON receiver is provided. Cloudflare delivery events use a trusted Event Subscriptions Queue boundary, never an invented HTTP signature. See [inbound dependencies and trust boundaries](src/inbound/README.md), `signedEmailId` must come from the authenticated event, not the hydrated response. The facade exports `createResendAttachmentFetcher({apiKey,maxAttachmentBytes}, {fetch?,clock?})` for the explicit `fetchAttachment` dependency; it validates metadata, fixed provider/CDN origins, size and redirects. No attachment may be silently omitted.
|
|
308
|
+
|
|
309
|
+
## Doctor and live proof
|
|
310
|
+
|
|
311
|
+
`mailDoctor({domain,provider,dnsRecords?,live:false}, dependencies?)` returns six items (`mx`, `spf`, `dkim`, `dmarc`, `send`, `receive`) with status, next steps, optional records and safe evidence. Plans are not observations: without probes records remain pending/not checked. DNS values must come from provider setup or supplied records; DKIM keys/selectors are never invented. Missing credentials are actionable, never a pass.
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
node modules/mail/doctor.mjs --domain mail.example.com --provider resend --plan --json
|
|
315
|
+
node modules/mail/doctor.mjs --domain mail.example.com --provider resend --plan
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Both plan commands are offline and require no credentials. See [doctor dependencies and explicit live invocation](src/doctor/README.md) for DNS checking and bounded send/receive correlation. A send response cannot make receive pass. The protected [live-proof contract](live-proof/README.md) names the exact runner command and receipt path; its evidence writer uses `EVIDENCE_WRITE_TOKEN`. Hosted workflow wiring and all owner-controlled credentials/resources remain separate integration work.
|
|
319
|
+
|
|
320
|
+
## Verification
|
|
321
|
+
|
|
322
|
+
From the repository root:
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
pnpm install --frozen-lockfile
|
|
326
|
+
pnpm --dir modules/mail install --frozen-lockfile --ignore-workspace --ignore-scripts
|
|
327
|
+
pnpm --dir modules/mail test
|
|
328
|
+
pnpm verify:modules
|
|
329
|
+
pnpm --dir modules/mail test:workerd
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The root install is also required for the bundle regression, which uses the reference app's actual Vite dependency. The facade formerly re-exported the doctor while the doctor dynamically imported the facade, causing Vite's `INEFFECTIVE_DYNAMIC_IMPORT` warning. Both now import a private sender module; the existing facade exports and send behavior remain unchanged. `tests/development/bundle.test.mjs` builds the facade and doctor with Vite and fails on that warning.
|
|
333
|
+
|
|
334
|
+
The module-local lock pins the Miniflare/workerd test runtime; production source has no npm dependencies. Unit suites cover core validation, adapters, events, inbound parsing, development, unsubscribe and doctor behaviour. The Messaging HTTP/service-binding smoke and real D1 intake race/rollback/restart suite join the existing area smokes. They check core portability, remote adapters, inbound hydration, durable event intake, the development outbox and doctor/facade assembly. The storage smokes retain real transaction rollback, namespace isolation, restart and sensitive viewer refusal coverage. `tests/all.test.ts` runs the nested suites serially through `pnpm verify:modules`; each worker is disposed after its run. Former SendGrid API tests remain while seed source still imports the deprecated module.
|
|
335
|
+
|
|
336
|
+
Local conformance does not establish provider configuration, DNS propagation, deployed webhook reachability, DKIM coverage or mailbox delivery. Live acceptance remains **PENDING** until an exact-SHA hosted run sends and receives on the owner's approved test domain with independently correlated mailbox evidence. Resend is the default test sender; SendGrid is selected where Inbound Parse is required. No live request is authorized by running local tests.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { mailDoctor } from './src/doctor/index.mjs';
|
|
5
|
+
import { object } from './src/doctor/common.mjs';
|
|
6
|
+
import { fail } from './src/core/errors.mjs';
|
|
7
|
+
|
|
8
|
+
export const usage = `Mail doctor
|
|
9
|
+
node modules/mail/doctor.mjs --domain mail.example.com --provider resend --plan [--json]
|
|
10
|
+
node modules/mail/doctor.mjs --live --config /path/to/approved-mail-config.json [--json]
|
|
11
|
+
Live mode sends exactly one message and reads an actual Resend receiving mailbox.
|
|
12
|
+
Credentials: MAIL_DOCTOR_SEND_API_KEY and MAIL_DOCTOR_RECEIVE_API_KEY, environment only.
|
|
13
|
+
No mode flag means offline plan. See modules/mail/live-proof/README.md.`;
|
|
14
|
+
|
|
15
|
+
export async function doctorOptions(args, env = process.env) {
|
|
16
|
+
const flags = {};
|
|
17
|
+
for (let index = 0; index < args.length; index++) {
|
|
18
|
+
const name = args[index];
|
|
19
|
+
if (!['--domain', '--provider', '--config', '--plan', '--live', '--json', '--help'].includes(name) || flags[name] !== undefined) fail('invalid_doctor_arguments');
|
|
20
|
+
if (['--domain', '--provider', '--config'].includes(name)) {
|
|
21
|
+
const value = args[++index];
|
|
22
|
+
if (!value || value.startsWith('--')) fail('invalid_doctor_arguments');
|
|
23
|
+
flags[name] = value;
|
|
24
|
+
} else flags[name] = true;
|
|
25
|
+
}
|
|
26
|
+
if (flags['--help']) return { help: true };
|
|
27
|
+
if (flags['--live'] && flags['--plan']) fail('invalid_doctor_arguments');
|
|
28
|
+
let config = {};
|
|
29
|
+
if (flags['--config']) {
|
|
30
|
+
try {
|
|
31
|
+
if ((await stat(flags['--config'])).size > 65536) fail('invalid_doctor_config_file');
|
|
32
|
+
config = JSON.parse(await readFile(flags['--config'], 'utf8'));
|
|
33
|
+
} catch { fail('invalid_doctor_config_file'); }
|
|
34
|
+
object(config, ['domain', 'provider', 'dnsRecords', 'send', 'receive', 'timeoutMs', 'pollIntervalMs'], 'invalid_doctor_config_file');
|
|
35
|
+
if (config.send !== undefined) object(config.send, ['from', 'to', 'accountId'], 'invalid_doctor_config_file');
|
|
36
|
+
if (config.receive !== undefined) object(config.receive, ['provider'], 'invalid_doctor_config_file');
|
|
37
|
+
}
|
|
38
|
+
for (const key of ['domain', 'provider']) {
|
|
39
|
+
if (flags[`--${key}`] && config[key] && flags[`--${key}`] !== config[key]) fail('doctor_config_conflict');
|
|
40
|
+
}
|
|
41
|
+
const options = {
|
|
42
|
+
domain: flags['--domain'] ?? config.domain, provider: flags['--provider'] ?? config.provider,
|
|
43
|
+
dnsRecords: config.dnsRecords, live: flags['--live'] === true,
|
|
44
|
+
};
|
|
45
|
+
if (options.live) {
|
|
46
|
+
if (!flags['--config'] || !config.send || !config.receive) fail('doctor_live_config_required');
|
|
47
|
+
options.send = {
|
|
48
|
+
from: config.send.from, to: config.send.to,
|
|
49
|
+
config: { provider: options.provider, mode: 'production', apiKey: env.MAIL_DOCTOR_SEND_API_KEY,
|
|
50
|
+
...(config.send.accountId === undefined ? {} : { accountId: config.send.accountId }) },
|
|
51
|
+
};
|
|
52
|
+
options.receive = { provider: config.receive.provider, apiKey: env.MAIL_DOCTOR_RECEIVE_API_KEY };
|
|
53
|
+
options.timeoutMs = config.timeoutMs; options.pollIntervalMs = config.pollIntervalMs;
|
|
54
|
+
}
|
|
55
|
+
return { options, json: flags['--json'] === true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatDoctor(result) {
|
|
59
|
+
return [`Mail doctor: ${result.domain} (${result.provider})`, ...result.items.flatMap(item => [
|
|
60
|
+
`${item.id.toUpperCase()}: ${item.status} (${item.evidence.state})`,
|
|
61
|
+
...(item.records ?? []).map(row => ` ${row.type} ${row.name} ${row.priority === undefined ? '' : `${row.priority} `}${row.value}`),
|
|
62
|
+
...item.nextSteps.map(step => ` Next: ${step}`),
|
|
63
|
+
])].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runCli(args, env = process.env) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = await doctorOptions(args, env);
|
|
69
|
+
if (parsed.help) { process.stdout.write(`${usage}\n`); return 0; }
|
|
70
|
+
const result = await mailDoctor(parsed.options);
|
|
71
|
+
process.stdout.write(`${parsed.json ? JSON.stringify(result, null, 2) : formatDoctor(result)}\n`);
|
|
72
|
+
return parsed.options.live && result.items.some(item => item.status !== 'pass') ? 1 : 0;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Only our fixed error codes are printed; never paths, config or remote text.
|
|
75
|
+
const code = error?.name === 'MailValidationError' && /^[a-z][a-z0-9_]{0,63}$/.test(error.code) ? error.code : 'doctor_failed';
|
|
76
|
+
process.stderr.write(`Mail doctor failed: ${code}. Use --help for required configuration.\n`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = await runCli(process.argv.slice(2));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createMailSender } from './src/sender.mjs';
|
|
2
|
+
|
|
3
|
+
export { MailValidationError } from './src/core/errors.mjs';
|
|
4
|
+
export { serializeSendGridMessage } from './src/adapters/sendgrid/index.mjs';
|
|
5
|
+
export { verifyWebhook, MAX_WEBHOOK_BYTES, createDeliveryEventIntake, createD1EventStore } from './src/events/index.mjs';
|
|
6
|
+
export { normalizeInbound, createResendAttachmentFetcher } from './src/inbound/index.mjs';
|
|
7
|
+
export { mailDoctor } from './src/doctor/index.mjs';
|
|
8
|
+
export { applyUnsubscribeHeaders } from './src/unsubscribe/index.mjs';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Protected mail proof preparation
|
|
2
|
+
|
|
3
|
+
Live acceptance is **PENDING**. No live DNS/send/receive operation has been run
|
|
4
|
+
by the local assembly checks. The CLI plan and test fixtures are not delivery evidence.
|
|
5
|
+
|
|
6
|
+
Offline sample (no credentials, DNS, send or receive):
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
node modules/mail/doctor.mjs --domain mail.example.com --provider resend --plan --json
|
|
10
|
+
node modules/mail/doctor.mjs --domain mail.example.com --provider resend --plan
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The human output names MX, SPF, DKIM, DMARC and next actions. To print actual
|
|
14
|
+
record values, pass `--config` with the four DNS arrays copied from provider
|
|
15
|
+
setup. No provider-specific record values can be inferred from a sample domain.
|
|
16
|
+
|
|
17
|
+
The owner must supply a test sending domain, verified sender, Resend sending
|
|
18
|
+
key, configured receiving mailbox and readback credential; throwaway Cloudflare
|
|
19
|
+
resources/credentials where required, and the evidence-store token. SendGrid
|
|
20
|
+
verified sender/Inbound Parse details remain prerequisites for that separate
|
|
21
|
+
path. An existing Base `live-proof` Environment and `SENDGRID_API_KEY` alone are
|
|
22
|
+
not send-and-receive proof. Resend is the default sender for this proof.
|
|
23
|
+
|
|
24
|
+
Use a private throwaway Resend receiving mailbox on the owner-approved test
|
|
25
|
+
domain. Enable its actual receiving MX and retain access to the received-email
|
|
26
|
+
API. The receive key must read that mailbox's received email list and detail;
|
|
27
|
+
a sending-only key is insufficient. This uses actual SMTP delivery into a
|
|
28
|
+
provider receiving mailbox. It does not certify an application's signed inbound
|
|
29
|
+
webhook integration, attachments, unsubscribe delivery or every sending adapter.
|
|
30
|
+
No application/Worker deployment is involved in this runner; the protected
|
|
31
|
+
Actions run is its execution identity.
|
|
32
|
+
|
|
33
|
+
Store nonsecret config in an approved file with this shape (replace the example
|
|
34
|
+
addresses and fill every DNS group with records from actual setup before proof):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"domain": "mail.example.com",
|
|
39
|
+
"provider": "resend",
|
|
40
|
+
"dnsRecords": {"mx": [], "spf": [], "dkim": [], "dmarc": []},
|
|
41
|
+
"send": {"from": "doctor@mail.example.com", "to": "doctor@receive.example.com"},
|
|
42
|
+
"receive": {"provider": "resend"},
|
|
43
|
+
"timeoutMs": 60000,
|
|
44
|
+
"pollIntervalMs": 2000
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The example is a config shape, not a working fixture or evidence. Each DNS entry
|
|
49
|
+
is `{ "type": "TXT", "name": "full.record.name", "value": "exact setup value" }`;
|
|
50
|
+
MX adds integer `priority`. The API guide in `../src/doctor/README.md` describes
|
|
51
|
+
record types and the resolver's precise coverage.
|
|
52
|
+
|
|
53
|
+
After explicit approval, the direct live CLI invocation is:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
node modules/mail/doctor.mjs --live --config /path/to/approved-mail-config.json --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It reads `MAIL_DOCTOR_SEND_API_KEY` and `MAIL_DOCTOR_RECEIVE_API_KEY` from the
|
|
60
|
+
process environment only; never put values on the command line or in JSON.
|
|
61
|
+
The supplied config never chooses a receiving URL or executable. CLI exits 0
|
|
62
|
+
for a valid offline plan, 0 for all six live items passing, 1 for incomplete live
|
|
63
|
+
checks, and 2 for configuration/runtime failure. An uncertain submission must be
|
|
64
|
+
reconciled before another run because each invocation sends a new probe.
|
|
65
|
+
|
|
66
|
+
The Base workflow integrator owns wiring into protected `live-proof.yml`.
|
|
67
|
+
This module installs no shared workflow; integrate the reviewed runner into the
|
|
68
|
+
qualified workflow line before any live run.
|
|
69
|
+
Required invocation contract:
|
|
70
|
+
|
|
71
|
+
1. The owner supplies and approves the test inputs. Configure secrets only in
|
|
72
|
+
`live-proof` with its integration-line/release-tag branch policy. Do not make
|
|
73
|
+
secrets available to task branches or untrusted PR code.
|
|
74
|
+
2. The workflow checks out the intended exact 40-hex SHA, confirms its reachability
|
|
75
|
+
from the permitted integration line or release tag, and reviews this code for
|
|
76
|
+
secret exfiltration before merge. It exposes sending/readback secrets as the
|
|
77
|
+
two `MAIL_DOCTOR_*_API_KEY` variables only to the proof step. Cloudflare send
|
|
78
|
+
mode also needs nonsecret `send.accountId` in the config. No provider login
|
|
79
|
+
on the Mac and no DNS writes are needed or authorized by this runner.
|
|
80
|
+
3. Make the approved nonsecret config available as `$RUNNER_TEMP/mail-config.json`.
|
|
81
|
+
In a clean checkout at `$GITHUB_SHA`, execute exactly:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
node modules/mail/live-proof/run.mjs "$RUNNER_TEMP/mail-config.json" "$RUNNER_TEMP/mail-receipt.json"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. The entry verifies Actions metadata and clean tracked source against
|
|
88
|
+
`GITHUB_SHA`. It reserves a new mode-0600 receipt before remote effects and
|
|
89
|
+
refuses an existing path before sending. An interrupted run leaves a pending
|
|
90
|
+
starting receipt, never successful proof. The calling workflow enforces actual Environment protection and source
|
|
91
|
+
reachability; environment variables alone do not establish either. Upload
|
|
92
|
+
only the receipt using the protected evidence-store mechanism and scoped
|
|
93
|
+
`EVIDENCE_WRITE_TOKEN`; never upload config, env, raw mail or logs with secrets.
|
|
94
|
+
Destination: `mail/live/<sha>/<run-id>-<attempt>/mail-receipt.json`.
|
|
95
|
+
The runner performs no GitHub/evidence-store API operations itself.
|
|
96
|
+
5. Retain workflow run ID, attempt, UTC times, exact SHA and immutable store path.
|
|
97
|
+
A fresh gate reads the receipt and actual protected run. Only a receipt with
|
|
98
|
+
all six checks passing, independent `source:resend_received_email_api` and a
|
|
99
|
+
real mailbox UUID satisfies this probe. A fixture receipt cannot satisfy it.
|
|
100
|
+
|
|
101
|
+
`mail-live-proof-v1` receipt fields: `state:pass|pending`, `sourceSha`, `command`,
|
|
102
|
+
`runUrl`, `runAttempt`, `startedAt`, `phase:starting|completed`, and (after
|
|
103
|
+
completion) `completedAt` and structured doctor `result`.
|
|
104
|
+
The send and receive evidence share a random `correlationId`; receive includes
|
|
105
|
+
actual `inboxMessageId`, receipt timestamp and SHA-256 of body, recipient and
|
|
106
|
+
RFC Message-ID. Send includes a hash of its provider receipt ID (or null when
|
|
107
|
+
absent). Config errors produce no delivery receipt; incomplete checks produce a
|
|
108
|
+
pending receipt and exit 1. Send HTTP acceptance alone never sets receive pass.
|
|
109
|
+
|
|
110
|
+
Unit verification: `node --test modules/mail/tests/doctor/*.test.mjs`.
|
|
111
|
+
Workerd assembly and the live run remain the integrator/fresh gate's distinct
|
|
112
|
+
checks. No plan, secret-presence check, loopback receiver or injected provider
|
|
113
|
+
response substitutes for the protected live receipt.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { doctorOptions } from '../doctor.mjs';
|
|
7
|
+
import { mailDoctor } from '../src/doctor/index.mjs';
|
|
8
|
+
import { validateProbe } from '../src/doctor/probe.mjs';
|
|
9
|
+
import { hostname } from '../src/doctor/common.mjs';
|
|
10
|
+
|
|
11
|
+
// Protected Actions entry. Workflow ownership/approval is enforced by the host
|
|
12
|
+
// workflow, never by interpreting an environment variable as owner consent.
|
|
13
|
+
let receiptFile;
|
|
14
|
+
try {
|
|
15
|
+
const [configPath, receiptPath] = process.argv.slice(2);
|
|
16
|
+
const env = process.env;
|
|
17
|
+
if (!configPath || !receiptPath || process.argv.length !== 4 || env.GITHUB_ACTIONS !== 'true' ||
|
|
18
|
+
!/^[0-9a-f]{40}$/.test(env.GITHUB_SHA ?? '') || !/^[\w.-]+\/[\w.-]+$/.test(env.GITHUB_REPOSITORY ?? '') ||
|
|
19
|
+
!/^\d+$/.test(env.GITHUB_RUN_ID ?? '') || !/^\d+$/.test(env.GITHUB_RUN_ATTEMPT ?? '')) throw new Error();
|
|
20
|
+
const root = fileURLToPath(new URL('../../../', import.meta.url));
|
|
21
|
+
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
22
|
+
const dirty = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
23
|
+
if (head !== env.GITHUB_SHA || dirty || resolve(configPath) === resolve(receiptPath)) throw new Error();
|
|
24
|
+
const parsed = await doctorOptions(['--live', '--config', configPath], env);
|
|
25
|
+
validateProbe({ ...parsed.options, domain: hostname(parsed.options.domain) });
|
|
26
|
+
const startedAt = new Date().toISOString();
|
|
27
|
+
const identity = {
|
|
28
|
+
schema: 'mail-live-proof-v1', sourceSha: head,
|
|
29
|
+
command: 'node modules/mail/live-proof/run.mjs <approved-config.json> <receipt.json>',
|
|
30
|
+
runUrl: `https://github.com/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`,
|
|
31
|
+
runAttempt: Number(env.GITHUB_RUN_ATTEMPT), startedAt,
|
|
32
|
+
};
|
|
33
|
+
// Reserve the output before any remote effect. A repeated output path cannot
|
|
34
|
+
// cause another send; a crashed run leaves an explicitly incomplete record.
|
|
35
|
+
receiptFile = await open(receiptPath, 'wx', 0o600);
|
|
36
|
+
await receiptFile.writeFile(`${JSON.stringify({ ...identity, state: 'pending', phase: 'starting' }, null, 2)}\n`);
|
|
37
|
+
await receiptFile.sync();
|
|
38
|
+
const result = await mailDoctor(parsed.options);
|
|
39
|
+
const state = result.items.every(item => item.status === 'pass') ? 'pass' : 'pending';
|
|
40
|
+
const receipt = { ...identity, state, phase: 'completed', completedAt: new Date().toISOString(), result };
|
|
41
|
+
const bytes = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`);
|
|
42
|
+
await receiptFile.truncate(0);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
while (offset < bytes.length) {
|
|
45
|
+
const { bytesWritten } = await receiptFile.write(bytes, offset, bytes.length - offset, offset);
|
|
46
|
+
if (!bytesWritten) throw new Error();
|
|
47
|
+
offset += bytesWritten;
|
|
48
|
+
}
|
|
49
|
+
await receiptFile.sync();
|
|
50
|
+
process.stdout.write(`Mail live proof ${state}; receipt written.\n`);
|
|
51
|
+
process.exitCode = state === 'pass' ? 0 : 1;
|
|
52
|
+
} catch {
|
|
53
|
+
process.stderr.write('Mail live proof failed: require protected Actions metadata, a clean exact-SHA checkout, explicit config, credentials and a new receipt path. No secret or provider error is logged.\n');
|
|
54
|
+
process.exitCode = 2;
|
|
55
|
+
} finally { await receiptFile?.close(); }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runeditrun/base-mail",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test tests/all.test.ts",
|
|
7
|
+
"test:core": "node --test tests/core/*.test.mjs",
|
|
8
|
+
"test:workerd": "node --test --test-concurrency=1 tests/runtime/*workerd.test.mjs tests/events/*workerd.test.mjs tests/messaging/*workerd.test.mjs tests/inbound/workerd.test.mjs tests/assembly/workerd.test.mjs"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"miniflare": "5.20260907.0-alpha"
|
|
12
|
+
},
|
|
13
|
+
"pnpm": {
|
|
14
|
+
"overrides": {
|
|
15
|
+
"sharp": "0.35.5",
|
|
16
|
+
"undici": "7.29.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|