@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,102 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { bounded, hostname, readJson } from './common.mjs';
|
|
3
|
+
import { record } from './records.mjs';
|
|
4
|
+
|
|
5
|
+
const types = { MX: 15, TXT: 16, CNAME: 5 };
|
|
6
|
+
|
|
7
|
+
// DNS presentation strings can contain several quoted character strings.
|
|
8
|
+
export function decodeTxt(value) {
|
|
9
|
+
if (typeof value !== 'string' || value.length > 16384) fail('doctor_dns_response_invalid');
|
|
10
|
+
let result = '', offset = 0, chunks = 0;
|
|
11
|
+
while (offset < value.length) {
|
|
12
|
+
while (value[offset] === ' ') offset++;
|
|
13
|
+
if (offset === value.length) break;
|
|
14
|
+
if (value[offset++] !== '"') fail('doctor_dns_response_invalid');
|
|
15
|
+
chunks++;
|
|
16
|
+
let closed = false;
|
|
17
|
+
while (offset < value.length) {
|
|
18
|
+
const char = value[offset++];
|
|
19
|
+
if (char === '"') { closed = true; break; }
|
|
20
|
+
if (char !== '\\') { result += char; continue; }
|
|
21
|
+
if (offset === value.length) fail('doctor_dns_response_invalid');
|
|
22
|
+
const decimal = value.slice(offset, offset + 3);
|
|
23
|
+
if (/^\d{3}$/.test(decimal)) {
|
|
24
|
+
if (Number(decimal) > 255) fail('doctor_dns_response_invalid');
|
|
25
|
+
result += String.fromCharCode(Number(decimal)); offset += 3;
|
|
26
|
+
} else result += value[offset++];
|
|
27
|
+
}
|
|
28
|
+
if (!closed || (offset < value.length && value[offset] !== ' ')) fail('doctor_dns_response_invalid');
|
|
29
|
+
}
|
|
30
|
+
if (!chunks) fail('doctor_dns_response_invalid');
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Fixed public DNS resolver: no caller-controlled host or credentials. */
|
|
35
|
+
export async function resolveDns({ name, type, signal }, fetcher = globalThis.fetch) {
|
|
36
|
+
const url = new URL('https://cloudflare-dns.com/dns-query');
|
|
37
|
+
url.searchParams.set('name', name); url.searchParams.set('type', type);
|
|
38
|
+
const response = await fetcher(url, { headers: { accept: 'application/dns-json' }, redirect: 'manual', signal });
|
|
39
|
+
if (response.status !== 200) return { status: 'error' };
|
|
40
|
+
const data = await readJson(response, 65536);
|
|
41
|
+
if (!data || data.TC === true || ![0, 3].includes(data.Status)) return { status: 'error' };
|
|
42
|
+
if (data.Status === 3) return { status: 'missing', records: [] };
|
|
43
|
+
if (data.Answer !== undefined && (!Array.isArray(data.Answer) || data.Answer.length > 128)) return { status: 'error' };
|
|
44
|
+
const records = [];
|
|
45
|
+
for (const answer of data.Answer ?? []) {
|
|
46
|
+
if (answer.type !== types[type] || hostname(answer.name, true) !== name) continue;
|
|
47
|
+
let value = answer.data, priority;
|
|
48
|
+
if (type === 'TXT') value = decodeTxt(value);
|
|
49
|
+
if (type === 'MX') {
|
|
50
|
+
const parts = /^(\d+)\s+(\S+)$/.exec(value);
|
|
51
|
+
if (!parts) return { status: 'error' };
|
|
52
|
+
priority = Number(parts[1]); value = parts[2];
|
|
53
|
+
}
|
|
54
|
+
// A null MX explicitly means this name does not accept mail.
|
|
55
|
+
if (type === 'MX' && value === '.') continue;
|
|
56
|
+
records.push(record({ type, name, value, ...(priority === undefined ? {} : { priority }) }));
|
|
57
|
+
}
|
|
58
|
+
return { status: records.length ? 'ok' : 'missing', records };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function same(a, b) {
|
|
62
|
+
return a.type === b.type && a.name === b.name && a.value === b.value && a.priority === b.priority;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function checkDns(items, dependencies) {
|
|
66
|
+
const resolver = dependencies.resolveDns ?? (query => resolveDns(query, dependencies.fetch));
|
|
67
|
+
const cache = new Map();
|
|
68
|
+
function lookup(expected) {
|
|
69
|
+
const key = `${expected.type}:${expected.name}`;
|
|
70
|
+
if (!cache.has(key)) cache.set(key, bounded(signal => resolver({ name: expected.name, type: expected.type, signal }), 8000)
|
|
71
|
+
.then(result => {
|
|
72
|
+
if (!result || !['ok', 'missing', 'error'].includes(result.status)) return { status: 'error' };
|
|
73
|
+
if (result.status === 'error') return { status: 'error' };
|
|
74
|
+
if (!Array.isArray(result.records) || result.records.length > 128) return { status: 'error' };
|
|
75
|
+
const records = result.records.map(record).filter(item => item.name === expected.name && item.type === expected.type);
|
|
76
|
+
if (result.status === 'missing' && records.length) return { status: 'error' };
|
|
77
|
+
return { status: records.length ? 'ok' : 'missing', records };
|
|
78
|
+
}).catch(() => ({ status: 'error' })));
|
|
79
|
+
return cache.get(key);
|
|
80
|
+
}
|
|
81
|
+
return Promise.all(items.map(async item => {
|
|
82
|
+
if (!item.records.length) return item;
|
|
83
|
+
const checks = await Promise.all(item.records.map(async expected => {
|
|
84
|
+
const found = await lookup(expected);
|
|
85
|
+
let state = found.status === 'error' ? 'error' : found.status === 'missing' ? 'missing' : 'mismatch';
|
|
86
|
+
if (found.status === 'ok') {
|
|
87
|
+
const prefix = item.id === 'spf' ? /^v=spf1(?:\s|$)/i : item.id === 'dmarc' ? /^v=DMARC1;/i : null;
|
|
88
|
+
const policies = prefix ? found.records.filter(row => row.type === 'TXT' && prefix.test(row.value)) : [];
|
|
89
|
+
if (found.records.some(row => same(row, expected)) && policies.length <= 1) state = 'match';
|
|
90
|
+
}
|
|
91
|
+
return { type: expected.type, name: expected.name, state };
|
|
92
|
+
}));
|
|
93
|
+
const state = ['error', 'missing', 'mismatch'].find(value => checks.some(check => check.state === value)) ?? 'match';
|
|
94
|
+
const next = {
|
|
95
|
+
error: 'DNS lookup failed or returned malformed data. Check resolver availability and retry the DNS check; no record absence is inferred.',
|
|
96
|
+
missing: 'Publish the missing records from provider setup, allow DNS propagation, then check again.',
|
|
97
|
+
mismatch: 'Compare the published values and MX priorities with provider setup; remove duplicate SPF/DMARC policies and correct mismatches.',
|
|
98
|
+
match: 'The queried records match this setup plan. Verify provider domain status and actual delivered authentication separately.',
|
|
99
|
+
};
|
|
100
|
+
return { ...item, status: state === 'match' ? 'pass' : 'fail', nextSteps: [next[state]], evidence: { state, checks } };
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { hostname, object } from './common.mjs';
|
|
3
|
+
import { recordPlan, dnsPlan } from './records.mjs';
|
|
4
|
+
import { checkDns } from './dns.mjs';
|
|
5
|
+
import { runProbe, validateProbe } from './probe.mjs';
|
|
6
|
+
|
|
7
|
+
/** Portable structured setup diagnosis. No network operation without live:true. */
|
|
8
|
+
export async function mailDoctor(options, dependencies = {}) {
|
|
9
|
+
object(options, ['domain', 'provider', 'dnsRecords', 'live', 'send', 'receive', 'timeoutMs', 'pollIntervalMs']);
|
|
10
|
+
const domain = hostname(options.domain);
|
|
11
|
+
if (!['sendgrid', 'resend', 'cloudflare'].includes(options.provider)) fail('invalid_doctor_provider');
|
|
12
|
+
if (options.live !== undefined && typeof options.live !== 'boolean') fail('invalid_doctor_live');
|
|
13
|
+
object(dependencies, ['fetch', 'resolveDns', 'sender'], 'invalid_doctor_dependencies');
|
|
14
|
+
if (dependencies.fetch !== undefined && typeof dependencies.fetch !== 'function' || dependencies.resolveDns !== undefined && typeof dependencies.resolveDns !== 'function') fail('invalid_doctor_dependencies');
|
|
15
|
+
const plan = recordPlan(options.dnsRecords);
|
|
16
|
+
let items = dnsPlan(domain, options.provider, plan);
|
|
17
|
+
if (options.live) {
|
|
18
|
+
// Validate every credential/address before DNS or sending has any effect.
|
|
19
|
+
const liveOptions = { ...options, domain };
|
|
20
|
+
validateProbe(liveOptions);
|
|
21
|
+
const [dns, probe] = await Promise.all([checkDns(items, dependencies), runProbe(liveOptions, dependencies)]);
|
|
22
|
+
items = [...dns, ...probe];
|
|
23
|
+
} else items.push(
|
|
24
|
+
{ id: 'send', status: 'not_checked', nextSteps: ['Configure a verified sender and an explicit provider API credential; run the live test only for an owner-approved test inbox.'], evidence: { state: 'unchecked' } },
|
|
25
|
+
{ id: 'receive', status: 'not_checked', nextSteps: ['Enable receiving on an owner test mailbox in Resend and configure its readback API credential; the live test matches a fresh random subject and body.'], evidence: { state: 'unchecked' } },
|
|
26
|
+
);
|
|
27
|
+
return { domain, provider: options.provider, items };
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createMailSender } from '../sender.mjs';
|
|
2
|
+
import { fail } from '../core/errors.mjs';
|
|
3
|
+
import { parseAddress, validateConfig, validateSendResult } from '../core/validation.mjs';
|
|
4
|
+
import { bounded, credential, digest, integer, object } from './common.mjs';
|
|
5
|
+
import { receiveProbe } from './receive.mjs';
|
|
6
|
+
|
|
7
|
+
export function validateProbe(options) {
|
|
8
|
+
object(options.send, ['config', 'from', 'to'], 'doctor_send_config_required');
|
|
9
|
+
validateConfig(options.send.config);
|
|
10
|
+
if (options.send.config.provider !== options.provider || options.send.config.mode !== 'production') fail('doctor_remote_production_required');
|
|
11
|
+
const from = parseAddress(options.send.from).email;
|
|
12
|
+
if (from.split('@')[1].toLowerCase() !== options.domain) fail('doctor_sender_domain_mismatch');
|
|
13
|
+
const to = parseAddress(options.send.to).email;
|
|
14
|
+
object(options.receive, ['provider', 'apiKey'], 'doctor_receive_config_required');
|
|
15
|
+
if (options.receive.provider !== 'resend') fail('doctor_receive_provider_unsupported');
|
|
16
|
+
credential(options.receive.apiKey);
|
|
17
|
+
return {
|
|
18
|
+
from, to,
|
|
19
|
+
timeoutMs: integer(options.timeoutMs ?? 60000, 100, 120000, 'invalid_doctor_timeout'),
|
|
20
|
+
pollIntervalMs: integer(options.pollIntervalMs ?? 2000, 100, 10000, 'invalid_doctor_poll_interval'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runProbe(options, dependencies) {
|
|
25
|
+
const config = validateProbe(options);
|
|
26
|
+
let sender;
|
|
27
|
+
try {
|
|
28
|
+
sender = dependencies.sender ?? createMailSender(options.send.config, { fetch: dependencies.fetch });
|
|
29
|
+
if (!sender || typeof sender.send !== 'function') fail('doctor_sender_unavailable');
|
|
30
|
+
} catch {
|
|
31
|
+
return [
|
|
32
|
+
{ id: 'send', status: 'fail', nextSteps: ['Check sender configuration and install the complete mail module before retrying.'], evidence: { state: 'configuration_error' } },
|
|
33
|
+
{ id: 'receive', status: 'not_checked', nextSteps: ['Complete sender configuration before testing receipt.'], evidence: { state: 'unchecked' } },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
const correlationId = crypto.randomUUID();
|
|
37
|
+
const probe = {
|
|
38
|
+
deliveryId: correlationId, from: config.from, to: [config.to],
|
|
39
|
+
subject: `Mail doctor ${correlationId}`,
|
|
40
|
+
text: `Mail doctor receive correlation: ${correlationId}`,
|
|
41
|
+
};
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = validateSendResult(await bounded(() => sender.send(probe), 16000), options.provider);
|
|
46
|
+
} catch {
|
|
47
|
+
result = { provider: options.provider, status: 'uncertain', providerMessageId: null };
|
|
48
|
+
}
|
|
49
|
+
const accepted = result.status === 'accepted' && (!result.recipientResults ||
|
|
50
|
+
result.recipientResults.some(row => parseAddress(row.recipient).email.toLowerCase() === config.to.toLowerCase() && row.status === 'accepted'));
|
|
51
|
+
const send = {
|
|
52
|
+
id: 'send', status: accepted ? 'pass' : result.status === 'uncertain' ? 'pending' : 'fail',
|
|
53
|
+
nextSteps: [accepted ? 'The provider accepted the submission. Inspect independent mailbox receipt below.' :
|
|
54
|
+
result.status === 'uncertain' ? 'Submission is uncertain. Reconcile provider and mailbox records before any resend.' :
|
|
55
|
+
'The provider did not accept the test recipient. Check sender verification, credentials, limits and suppression; no retry was attempted.'],
|
|
56
|
+
evidence: {
|
|
57
|
+
state: result.status, recipientAccepted: accepted, correlationId,
|
|
58
|
+
providerMessageIdSha256: result.providerMessageId ? await digest(result.providerMessageId) : null,
|
|
59
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const receive = accepted || result.status === 'uncertain'
|
|
63
|
+
? await receiveProbe({ apiKey: options.receive.apiKey, probe, startedAt, timeoutMs: config.timeoutMs, pollIntervalMs: config.pollIntervalMs }, dependencies)
|
|
64
|
+
: { id: 'receive', status: 'not_checked', nextSteps: ['Resolve the rejected submission before initiating another test.'], evidence: { state: 'unchecked' } };
|
|
65
|
+
return [send, receive];
|
|
66
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { parseAddress } from '../core/validation.mjs';
|
|
3
|
+
import { bounded, digest, readJson } from './common.mjs';
|
|
4
|
+
|
|
5
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
function mailbox(value) {
|
|
7
|
+
try { return parseAddress(value).email.toLowerCase(); } catch { return null; }
|
|
8
|
+
}
|
|
9
|
+
function candidate(email, probe, startedAt, now) {
|
|
10
|
+
const received = Date.parse(email?.created_at);
|
|
11
|
+
return email && uuid.test(email.id) && email.subject === probe.subject &&
|
|
12
|
+
mailbox(email.from) === mailbox(probe.from) && Array.isArray(email.to) && email.to.some(value => mailbox(value) === mailbox(probe.to[0])) &&
|
|
13
|
+
Number.isFinite(received) && received >= startedAt - 1000 && received <= now + 1000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read received mail only; never read sent-message status as delivery proof. */
|
|
17
|
+
export async function receiveProbe({ apiKey, probe, startedAt, timeoutMs, pollIntervalMs }, dependencies = {}) {
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
20
|
+
let polls = 0, requests = 0;
|
|
21
|
+
async function get(path) {
|
|
22
|
+
const remaining = deadline - Date.now();
|
|
23
|
+
if (remaining <= 0) fail('doctor_receive_timeout');
|
|
24
|
+
requests++;
|
|
25
|
+
return bounded(async signal => {
|
|
26
|
+
const response = await fetcher(`https://api.resend.com${path}`, {
|
|
27
|
+
method: 'GET', redirect: 'manual', signal, headers: { authorization: `Bearer ${apiKey}` },
|
|
28
|
+
});
|
|
29
|
+
if (response.status !== 200) fail('doctor_readback_failed');
|
|
30
|
+
return readJson(response);
|
|
31
|
+
}, Math.min(8000, remaining));
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
while (Date.now() < deadline && polls < 30) {
|
|
35
|
+
polls++;
|
|
36
|
+
let after = '';
|
|
37
|
+
for (let page = 0; page < 3; page++) {
|
|
38
|
+
const list = await get(`/emails/receiving?limit=100${after ? `&after=${after}` : ''}`);
|
|
39
|
+
if (!list || list.object !== 'list' || !Array.isArray(list.data) || list.data.length > 100 || typeof list.has_more !== 'boolean') fail('doctor_readback_failed');
|
|
40
|
+
for (const summary of list.data) {
|
|
41
|
+
if (!candidate(summary, probe, startedAt, Date.now())) continue;
|
|
42
|
+
const email = await get(`/emails/receiving/${summary.id}`);
|
|
43
|
+
if (email?.id !== summary.id || email.object !== 'email' || !candidate(email, probe, startedAt, Date.now()) ||
|
|
44
|
+
typeof email.text !== 'string' || email.text.trimEnd() !== probe.text ||
|
|
45
|
+
typeof email.message_id !== 'string' || !/^<[^\s<>]{1,990}>$/.test(email.message_id)) fail('doctor_readback_conflict');
|
|
46
|
+
return {
|
|
47
|
+
id: 'receive', status: 'pass', nextSteps: ['Retain this independent received-mail receipt with the exact source SHA and protected workflow run.'],
|
|
48
|
+
evidence: {
|
|
49
|
+
state: 'received', source: 'resend_received_email_api', inboxMessageId: email.id,
|
|
50
|
+
rfcMessageIdSha256: await digest(email.message_id), receivedAt: new Date(Date.parse(email.created_at)).toISOString(),
|
|
51
|
+
correlationId: probe.deliveryId, bodySha256: await digest(probe.text),
|
|
52
|
+
recipientSha256: await digest(mailbox(probe.to[0])), polls, requests,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!list.has_more) break;
|
|
57
|
+
const cursor = list.data.at(-1)?.id;
|
|
58
|
+
if (!uuid.test(cursor ?? '') || cursor === after) fail('doctor_readback_failed');
|
|
59
|
+
after = cursor;
|
|
60
|
+
}
|
|
61
|
+
const remaining = deadline - Date.now();
|
|
62
|
+
if (remaining > 0 && polls < 30) {
|
|
63
|
+
const nextPollAt = Math.min(deadline, Date.now() + pollIntervalMs);
|
|
64
|
+
// A timer can wake before the wall-clock boundary due to rounding.
|
|
65
|
+
// Recheck the intended wake time before spending another provider read.
|
|
66
|
+
while (Date.now() < nextPollAt) {
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(1, nextPollAt - Date.now())));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
if (Date.now() < deadline) return {
|
|
73
|
+
id: 'receive', status: 'fail', nextSteps: ['Mailbox readback failed or returned conflicting data. Check receiving permissions and the retained provider message; no resend was attempted.'],
|
|
74
|
+
evidence: { state: 'error', polls, requests, correlationId: probe.deliveryId },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: 'receive', status: 'pending', nextSteps: ['No matching received message appeared within the bounded mailbox scan. Inspect routing, spam and provider records before considering another send.'],
|
|
79
|
+
evidence: { state: 'timeout', polls, requests, correlationId: probe.deliveryId, maxPagesPerPoll: 3, maxMessagesPerPage: 100 },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { hostname, integer, object } from './common.mjs';
|
|
3
|
+
|
|
4
|
+
export const DNS_IDS = ['mx', 'spf', 'dkim', 'dmarc'];
|
|
5
|
+
|
|
6
|
+
export function record(value) {
|
|
7
|
+
object(value, ['type', 'name', 'value', 'priority'], 'invalid_doctor_record');
|
|
8
|
+
if (!['MX', 'TXT', 'CNAME'].includes(value.type) || typeof value.value !== 'string' || !value.value.trim() || value.value.length > 4096 || /[\x00-\x1f\x7f]/.test(value.value)) fail('invalid_doctor_record');
|
|
9
|
+
const result = { type: value.type, name: hostname(value.name, true), value: value.value };
|
|
10
|
+
if (value.type === 'MX') result.priority = integer(value.priority, 0, 65535, 'invalid_doctor_priority');
|
|
11
|
+
else if (value.priority !== undefined) fail('invalid_doctor_priority');
|
|
12
|
+
if (value.type !== 'TXT') result.value = hostname(value.value);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function recordPlan(input = {}) {
|
|
17
|
+
object(input, DNS_IDS, 'invalid_doctor_records');
|
|
18
|
+
const plan = {};
|
|
19
|
+
let total = 0;
|
|
20
|
+
for (const id of DNS_IDS) {
|
|
21
|
+
const values = input[id] ?? [];
|
|
22
|
+
if (!Array.isArray(values) || (total += values.length) > 32) fail('invalid_doctor_records');
|
|
23
|
+
plan[id] = values.map(record);
|
|
24
|
+
for (const item of plan[id]) {
|
|
25
|
+
if ((id === 'mx' && item.type !== 'MX') || (id === 'dmarc' && (item.type !== 'TXT' || !item.name.startsWith('_dmarc.') || !/^v=DMARC1;/i.test(item.value))) ||
|
|
26
|
+
(id === 'spf' && !(item.type === 'CNAME' || item.type === 'TXT' && /^v=spf1(?:\s|$)/i.test(item.value))) ||
|
|
27
|
+
(id === 'dkim' && (!item.name.includes('._domainkey.') || !['TXT', 'CNAME'].includes(item.type)))) fail('invalid_doctor_record_kind');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return plan;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function dnsPlan(domain, provider, plan) {
|
|
34
|
+
const setup = {
|
|
35
|
+
mx: `Copy the receiving service's MX host and priority for ${domain}; sending and receiving MX may be different.`,
|
|
36
|
+
spf: `Copy ${provider}'s exact SPF TXT or return-path CNAME name and value from domain setup; keep one SPF policy per name.`,
|
|
37
|
+
dkim: `Copy every ${provider} DKIM selector and TXT public key or CNAME target from domain setup; no selector or key can be inferred.`,
|
|
38
|
+
dmarc: `Choose a DMARC policy and supply its TXT record at _dmarc.${domain}; confirm alignment with your sending domain.`,
|
|
39
|
+
};
|
|
40
|
+
return DNS_IDS.map(id => ({
|
|
41
|
+
id, status: plan[id].length ? 'pending' : 'not_checked', records: plan[id],
|
|
42
|
+
nextSteps: [plan[id].length ? 'Review these explicit setup records, publish them through your DNS administrator, then request a live DNS check.' : setup[id]],
|
|
43
|
+
evidence: { state: 'unchecked', expectedRecordsSupplied: plan[id].length > 0 },
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Verified delivery-event intake
|
|
2
|
+
|
|
3
|
+
Public exports from `src/events/index.mjs` (re-export these from the mail facade):
|
|
4
|
+
|
|
5
|
+
* `verifyWebhook(input): Promise<boolean>` — the frozen IF-056-MAIL-v1 signature.
|
|
6
|
+
* `MAX_WEBHOOK_BYTES` — 32 MiB, including signed Inbound Parse multipart bytes.
|
|
7
|
+
* `createDeliveryEventIntake(options)` — authenticated normalization plus atomic
|
|
8
|
+
durable receipt and recipient-state storage. No public unverified normalizer.
|
|
9
|
+
|
|
10
|
+
## Signature verification
|
|
11
|
+
|
|
12
|
+
`verifyWebhook({provider, rawBody, headers, key, now, toleranceSeconds = 300})`
|
|
13
|
+
takes a `Uint8Array`, `Headers`, and epoch milliseconds. Only SendGrid and Resend
|
|
14
|
+
are supported. It returns false for malformed inputs, keys, signature encodings,
|
|
15
|
+
or timestamps. Tolerance is an integer from zero through 3600 seconds, inclusive;
|
|
16
|
+
both clock boundaries are inclusive. The raw body is copied before asynchronous
|
|
17
|
+
verification. SendGrid verifies SHA-256 ECDSA with a canonical base64 P-256 SPKI
|
|
18
|
+
key and strict ASN.1 DER signature. Resend verifies Svix v1 HMAC-SHA256 over the
|
|
19
|
+
original `id.timestamp.` prefix and raw bytes, including all valid rotation
|
|
20
|
+
signatures. Base64 may omit padding but must retain canonical zero padding bits;
|
|
21
|
+
whitespace and URL-safe substitutions are rejected. Svix signing keys must have
|
|
22
|
+
the `whsec_` prefix and decode to 16–64 bytes.
|
|
23
|
+
|
|
24
|
+
This stateless primitive does **not** prevent replay within the clock window,
|
|
25
|
+
validate a provider schema, authenticate a mail sender, or authorize a tenant.
|
|
26
|
+
Delivery routes should use the complete intake below. Signed Inbound Parse can
|
|
27
|
+
use the primitive on the original multipart bytes before invoking inbound
|
|
28
|
+
normalization; the application separately owns inbound authorization and durable
|
|
29
|
+
replay protection. Bound streamed HTTP reads before materializing `rawBody`.
|
|
30
|
+
|
|
31
|
+
## Complete HTTP intake
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const intake = createDeliveryEventIntake({
|
|
35
|
+
provider: 'resend', // or 'sendgrid'
|
|
36
|
+
tenantId, // trusted server-side routing, never request JSON
|
|
37
|
+
eventNamespace, // stable webhook/account identity across key rotations
|
|
38
|
+
key, // configured for this endpoint and tenant
|
|
39
|
+
storage: ctx.storage, // real DurableObjectStorage, described below
|
|
40
|
+
authorize: async event => {
|
|
41
|
+
return await applicationOwnsMessageAndAllRecipients(event);
|
|
42
|
+
},
|
|
43
|
+
clock: Date.now, // optional, returns epoch milliseconds
|
|
44
|
+
toleranceSeconds: 300, // optional
|
|
45
|
+
});
|
|
46
|
+
const results = await intake.ingestWebhook({ rawBody, headers });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The mandatory `authorize(event)` must return exactly true for every event in the
|
|
50
|
+
batch. It runs only after successful signature and schema verification, and must
|
|
51
|
+
check the server-selected tenant, provider message (or SendGrid signed delivery
|
|
52
|
+
correlation), and **every** recipient against application records. It must be a
|
|
53
|
+
read-only authorization decision, never a delivery side effect. A provider
|
|
54
|
+
signature authenticates the configured provider endpoint; it does not authorize
|
|
55
|
+
an arbitrary signed `rer_delivery_id`, product marker, mailbox, or tenant.
|
|
56
|
+
|
|
57
|
+
Each frozen authorization event has:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
{
|
|
61
|
+
provider, tenantId, eventNamespace,
|
|
62
|
+
eventId, providerMessageId, type,
|
|
63
|
+
status, // accepted|deferred|delivered|bounced|failed|rejected|complained|null
|
|
64
|
+
occurredAt, // epoch milliseconds, from provider event time
|
|
65
|
+
recipients: ['mailbox@example.com'],
|
|
66
|
+
deliveryId, product, // signed SendGrid custom args, otherwise null
|
|
67
|
+
fingerprint, // SHA-256 hex of full provider event payload
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
SendGrid arrays remain distinct events, keyed by `sg_event_id`; each recipient
|
|
72
|
+
keeps its own state. Resend uses the signed `svix-id` as event identity and keeps
|
|
73
|
+
the provider's `data.to` recipient list. Email local-part case is preserved. All
|
|
74
|
+
required IDs are nonempty, bounded and control-free; missing message identities,
|
|
75
|
+
recipient addresses or times fail. Only known delivery/tracking event types are
|
|
76
|
+
accepted; inbound events belong to the inbound pipeline. Open/click/unsubscribe
|
|
77
|
+
events retain receipts with `status: null` and cannot change delivery state.
|
|
78
|
+
|
|
79
|
+
HTTP intake permits at most 1 MiB of raw bytes and 1000 events or recipients.
|
|
80
|
+
Exact limits succeed; malformed UTF-8/JSON or any invalid/unauthorized event
|
|
81
|
+
rejects the whole batch before storage. Stored event timestamps may be old
|
|
82
|
+
(provider retries are legitimate), but cannot exceed the clock by five minutes.
|
|
83
|
+
Fresh request signatures remain required on every retry.
|
|
84
|
+
|
|
85
|
+
The result is an array in input event order:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
{ eventId, outcome: 'stored', recipients: [
|
|
89
|
+
{ recipient, applied: true, state: { status, occurredAt, eventId } },
|
|
90
|
+
] }
|
|
91
|
+
// Replay, no state mutation:
|
|
92
|
+
{ eventId, outcome: 'duplicate', recipients: [] }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Duplicate retries can be acknowledged without repeating application effects.
|
|
96
|
+
Same ID with a different fingerprint throws `event_identity_conflict`, rolling
|
|
97
|
+
back the entire batch. SendGrid fingerprints use the exact original JSON bytes
|
|
98
|
+
of each event, independently of its array position, surrounding whitespace or
|
|
99
|
+
other batch members. Internal event whitespace changes conflict conservatively.
|
|
100
|
+
Resend fingerprints cover the complete raw body. Every provider field therefore
|
|
101
|
+
participates, including fields not selected for the normalized record.
|
|
102
|
+
|
|
103
|
+
## Durable storage seam and state policy
|
|
104
|
+
|
|
105
|
+
`storage.transaction(async tx => ...)`, `tx.get(key)` and `tx.put(key, value)`
|
|
106
|
+
match real Cloudflare Durable Object storage. The supplied implementation must
|
|
107
|
+
provide serializable, rollback-on-throw, durably committed transactions across
|
|
108
|
+
all concurrent calls and restarts. Route a given namespace consistently to the
|
|
109
|
+
same Durable Object. A Node host can supply the same methods backed by its real
|
|
110
|
+
transactional database; this module has no Node imports or process-local store.
|
|
111
|
+
Plain KV, a Map, check-then-put, and an in-process mutex are insufficient.
|
|
112
|
+
There is no automatic storage selection and no in-memory fallback.
|
|
113
|
+
|
|
114
|
+
Receipt keys hash the tuple `[provider, tenantId, eventNamespace, eventId]` and
|
|
115
|
+
state keys hash `[provider, tenantId, eventNamespace, providerMessageId, recipient]`
|
|
116
|
+
with SHA-256 over JSON, prefixed `mail-events:v1:receipt:` or
|
|
117
|
+
`mail-events:v1:state:`. Receipts store the entire normalized authorization event,
|
|
118
|
+
including fingerprint. States store `{status, occurredAt, eventId}`. Both writes
|
|
119
|
+
commit together. No expiry or pruning runs implicitly: retain receipts through
|
|
120
|
+
the provider's possible replay period, and retain final state for the full
|
|
121
|
+
application delivery lifetime. Deleting records weakens these guarantees.
|
|
122
|
+
|
|
123
|
+
A terminal state cannot become accepted or deferred. A terminal event can
|
|
124
|
+
finalize a pending state even when received out of order. A newer terminal fact
|
|
125
|
+
may supersede an older terminal fact (for example a complaint after delivery).
|
|
126
|
+
Older/equal-time events never replace an existing terminal state; equal-time
|
|
127
|
+
conflicting terminal facts retain the first committed state, while both receipts
|
|
128
|
+
remain available. Application policy can inspect the retained facts separately.
|
|
129
|
+
Delivery means recipient mail-server acceptance, not evidence of human receipt.
|
|
130
|
+
|
|
131
|
+
No application callback runs inside the transaction, and this API does not
|
|
132
|
+
promise exactly-once external side effects. Consume persisted receipts using an
|
|
133
|
+
application-owned durable workflow/outbox; do not equate a successful return with
|
|
134
|
+
atomic completion of a separate external action. Storage/authorization exceptions
|
|
135
|
+
are redacted; validation errors carry fixed `.code` values.
|
|
136
|
+
|
|
137
|
+
## Cloudflare trusted queue boundary
|
|
138
|
+
|
|
139
|
+
Configure `provider: 'cloudflare'`, omit `key`, and supply:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
cloudflareSource: { queue, accountId, zoneId, domain, eventSubscriptionId }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The returned object has **only** `consumeQueue(batch)`, not `ingestWebhook`.
|
|
146
|
+
Invoke it only from the actual Worker `queue(batch, env, ctx)` consumer for an
|
|
147
|
+
Event Subscription queue with controlled producer permissions. Match the
|
|
148
|
+
configured queue, sending domain, zone, account, subscription, schema version 1
|
|
149
|
+
and `email.sending` source. It supports documented delivered/deferred/bounced/
|
|
150
|
+
failed/rejected/complained events and validates their terminal flags. The same
|
|
151
|
+
mandatory application authorization and durable transaction apply. The entire
|
|
152
|
+
batch is bounded to 1 MiB after queue-body JSON serialization and 1000 messages;
|
|
153
|
+
the serialized full event supplies the fingerprint.
|
|
154
|
+
|
|
155
|
+
Queue name and event fields are validation, not authentication. Anyone can forge
|
|
156
|
+
a JavaScript object with those properties: never turn HTTP JSON into a queue
|
|
157
|
+
batch or expose `consumeQueue` through an unauthenticated HTTP/RPC method. The
|
|
158
|
+
trusted Cloudflare queue invocation and producer controls are the boundary.
|
|
159
|
+
For Durable Object storage, the queue handler may forward
|
|
160
|
+
`{queue: batch.queue, messages: batch.messages.map(({body}) => ({body}))}` through
|
|
161
|
+
its **private Durable Object binding** to an application-owned method which
|
|
162
|
+
constructs this intake with `this.ctx.storage`. This transports the authenticated
|
|
163
|
+
queue data without trying to serialize runtime ACK functions. Keep that method
|
|
164
|
+
inaccessible from public HTTP and validate the configured source there too.
|
|
165
|
+
Call `batch.ackAll()` only after `consumeQueue` resolves; on a throw, retain/retry
|
|
166
|
+
or quarantine according to application policy. The module never ACKs first and
|
|
167
|
+
never invents a Cloudflare HTTP signature. Routing inbound email is a separate
|
|
168
|
+
Worker `email` event and does not enter this delivery pipeline.
|
|
169
|
+
|
|
170
|
+
## Verification and references
|
|
171
|
+
|
|
172
|
+
From the Base root, with the module's existing development dependency available:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
node --test modules/mail/tests/events/verify.test.mjs
|
|
176
|
+
node --test modules/mail/tests/events/boundaries.test.mjs
|
|
177
|
+
node --test modules/mail/tests/events/workerd.test.mjs
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If using an existing cached dependency in another checkout, set
|
|
181
|
+
`MAIL_MINIFLARE_MODULE` to its absolute `miniflare/dist/src/index.js` path; no
|
|
182
|
+
installation is required. Tests create/dispose actual workerd and SQLite-backed
|
|
183
|
+
Durable Objects, test races, atomic rollback and full runtime restart, and remove
|
|
184
|
+
their temporary storage. Provider fixtures and test routes exist only in tests.
|
|
185
|
+
The tests cover crypto in Node and workerd, but hosted proof remains
|
|
186
|
+
**PENDING**. No live provider API, mail, or deployment is exercised.
|
|
187
|
+
|
|
188
|
+
Primary contracts checked 6 October 2026:
|
|
189
|
+
|
|
190
|
+
* [SendGrid security](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features)
|
|
191
|
+
* [SendGrid signed multipart](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/securing-your-parse-webhooks)
|
|
192
|
+
* [SendGrid event fields](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event)
|
|
193
|
+
* [Resend verification](https://resend.com/docs/webhooks/verify-webhooks-requests)
|
|
194
|
+
* [Svix framing and published test vector](https://docs.svix.com/receiving/verifying-payloads/how-manual)
|
|
195
|
+
* [Resend recipient event scope](https://www.resend.com/changelog/webhook-event-visibility)
|
|
196
|
+
* [Cloudflare event schemas](https://developers.cloudflare.com/email-service/platform/event-subscriptions/)
|
|
197
|
+
* [Durable Object storage transactions](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/)
|
|
198
|
+
|
|
199
|
+
## Messaging events and real D1 storage
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
import { createDeliveryEventIntake, createD1EventStore } from './modules/mail/index.mjs';
|
|
203
|
+
const intake = createDeliveryEventIntake({
|
|
204
|
+
provider: 'messaging', tenantId: trustedTenant, eventNamespace: 'messaging-link',
|
|
205
|
+
messagingSource: {
|
|
206
|
+
serviceOrigin: 'https://messaging.example.com',
|
|
207
|
+
installId: localInstallId,
|
|
208
|
+
jwks: pinnedEventJwks, // public ES256 keys obtained when linking
|
|
209
|
+
},
|
|
210
|
+
store: createD1EventStore({ db: env.DB }),
|
|
211
|
+
authorize: event => ownsDelivery(event.deliveryId, event.providerMessageId,
|
|
212
|
+
event.recipients, event.tenantId),
|
|
213
|
+
});
|
|
214
|
+
// Bound the read to 1 MiB before allocating; this is the actual compact JWS body.
|
|
215
|
+
const results = await intake.ingestWebhook({ rawBody, headers: request.headers });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For the exact independent wire format, signature envelope and closed detail
|
|
219
|
+
fields, see [Messaging v1](../adapters/messaging/README.md). `key` is forbidden for
|
|
220
|
+
this source. The event JWKS is copied when constructing intake; mutating the
|
|
221
|
+
caller configuration cannot change an existing trust root. No network lookup
|
|
222
|
+
runs during verification. Messaging's envelope clock tolerance is fixed at 60s;
|
|
223
|
+
the existing `toleranceSeconds` option still configures Resend/SendGrid only.
|
|
224
|
+
HTTPS service origins are required; explicit `mode:'test'|'development'` permits
|
|
225
|
+
HTTP loopback for local integration. The signature establishes the service and
|
|
226
|
+
installation; application authorization must still check every correlated
|
|
227
|
+
message and recipient. All schema validation and whole-batch authorization
|
|
228
|
+
finish before any SQL effect. Normalized `detail` is frozen before authorization.
|
|
229
|
+
|
|
230
|
+
Apply `src/events/d1-schema.sql` as a new application migration before using the
|
|
231
|
+
store. This additive schema owns only `rer_mail_event_receipts` and
|
|
232
|
+
`rer_mail_event_states`; it leaves existing application and DO storage intact.
|
|
233
|
+
Pass the constructed store as `store`, and omit `storage`. Existing DO users keep
|
|
234
|
+
`storage:ctx.storage` unchanged. The D1 store also accepts authenticated Resend,
|
|
235
|
+
SendGrid and Cloudflare intake; choosing the database does not change a source's
|
|
236
|
+
authentication boundary. Only the real D1 constructor's stores are accepted,
|
|
237
|
+
with no process-local fallback or arbitrary unverified normalizer.
|
|
238
|
+
|
|
239
|
+
Every `commit` issues exactly one D1 `batch()` containing all receipt inserts,
|
|
240
|
+
conditional recipient-state upserts and state reads. The D1 transaction is the
|
|
241
|
+
concurrency and rollback boundary; no process mutex or preliminary read is used.
|
|
242
|
+
Receipts have a primary key on `(provider,tenant_id,event_namespace,event_id)`.
|
|
243
|
+
A conflicting fingerprint intentionally violates the named
|
|
244
|
+
`rer_mail_event_identity_conflict` SQL CHECK; D1 rolls back the **whole batch**,
|
|
245
|
+
including receipts and states inserted earlier in it. Other SQL errors also roll
|
|
246
|
+
back the transaction and return a redacted `event_storage_failed` error.
|
|
247
|
+
|
|
248
|
+
Each insert carries a random commit token and its batch ordinal. Conditional
|
|
249
|
+
state writes require that exact receipt token/ordinal, so concurrent duplicates,
|
|
250
|
+
repeated events within one batch and post-restart retries cannot reapply state.
|
|
251
|
+
Receipt `event_json` stores the full normalized event, including correlation and
|
|
252
|
+
detail; state keys include provider, tenant, namespace, provider message ID and
|
|
253
|
+
case-preserved mailbox. Terminal-state ordering is the same as the DO store.
|
|
254
|
+
Stored result reads occur within the same batch, so each event's returned state
|
|
255
|
+
reflects its place in that batch. Duplicate results have no recipient effects.
|
|
256
|
+
|
|
257
|
+
D1 bind/statement limits still apply to the host's database. Oversized batches
|
|
258
|
+
fail atomically rather than being split into partial commits. Applications should
|
|
259
|
+
size service pushes accordingly. Receipts and state are retained until the app
|
|
260
|
+
explicitly prunes or erases them; this module has no implicit cleanup. The app
|
|
261
|
+
owns an atomic business workflow for any external effects after intake, just as
|
|
262
|
+
with the DO store.
|
|
263
|
+
|
|
264
|
+
`tests/events/d1-workerd.test.mjs` drives public intake against real workerd D1.
|
|
265
|
+
It covers signature/issuer/audience/key/time refusal, whole-batch schema and
|
|
266
|
+
ownership checks, duplicate/conflicting races, rollback after an earlier insert,
|
|
267
|
+
forced SQL failure rollback, restart duplicates, tenant/namespace isolation,
|
|
268
|
+
local-part case and terminal ordering. The existing DO/Queue smoke remains.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Additive migration. Never drop existing delivery receipts or state.
|
|
2
|
+
CREATE TABLE IF NOT EXISTS rer_mail_event_receipts (
|
|
3
|
+
provider TEXT NOT NULL,
|
|
4
|
+
tenant_id TEXT NOT NULL,
|
|
5
|
+
event_namespace TEXT NOT NULL,
|
|
6
|
+
event_id TEXT NOT NULL,
|
|
7
|
+
fingerprint TEXT NOT NULL,
|
|
8
|
+
event_json TEXT NOT NULL,
|
|
9
|
+
commit_token TEXT NOT NULL,
|
|
10
|
+
ordinal INTEGER NOT NULL,
|
|
11
|
+
conflict INTEGER NOT NULL DEFAULT 0 CONSTRAINT rer_mail_event_identity_conflict CHECK (conflict = 0),
|
|
12
|
+
PRIMARY KEY (provider, tenant_id, event_namespace, event_id)
|
|
13
|
+
);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS rer_mail_event_states (
|
|
15
|
+
provider TEXT NOT NULL,
|
|
16
|
+
tenant_id TEXT NOT NULL,
|
|
17
|
+
event_namespace TEXT NOT NULL,
|
|
18
|
+
provider_message_id TEXT NOT NULL,
|
|
19
|
+
recipient TEXT NOT NULL,
|
|
20
|
+
status TEXT NOT NULL,
|
|
21
|
+
occurred_at INTEGER NOT NULL,
|
|
22
|
+
event_id TEXT NOT NULL,
|
|
23
|
+
terminal INTEGER NOT NULL CHECK (terminal IN (0, 1)),
|
|
24
|
+
PRIMARY KEY (provider, tenant_id, event_namespace, provider_message_id, recipient)
|
|
25
|
+
);
|