@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,172 @@
|
|
|
1
|
+
import { fail } from './errors.mjs';
|
|
2
|
+
import { validateMessagingConfig, validateMessagingResult, identity, rfc3339 } from '../adapters/messaging/contract.mjs';
|
|
3
|
+
|
|
4
|
+
const providers = ['sendgrid', 'resend', 'cloudflare', 'development', 'messaging'];
|
|
5
|
+
const controls = /[\x00-\x1f\x7f]/;
|
|
6
|
+
const fields = new Set(['deliveryId', 'from', 'to', 'subject', 'text', 'html', 'cc', 'bcc', 'replyTo', 'headers', 'attachments', 'unsubscribe', 'customArgs', 'asmGroupId', 'subscriptionTracking', 'kind', 'expiresAt']);
|
|
7
|
+
const reservedHeaders = new Set(['x-sg-id', 'x-sg-eid', 'received', 'dkim-signature', 'content-type', 'content-transfer-encoding', 'mime-version', 'to', 'from', 'subject', 'reply-to', 'cc', 'bcc', 'return-path', 'sender']);
|
|
8
|
+
const reservedArgs = new Set(['rer_delivery_id', 'email', 'event', 'timestamp', 'sg_event_id', 'sg_message_id']);
|
|
9
|
+
|
|
10
|
+
function record(value, code) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail(code);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function keys(value, allowed, code) {
|
|
15
|
+
record(value, code);
|
|
16
|
+
if (Reflect.ownKeys(value).some(key => typeof key !== 'string' || !allowed.has(key))) fail(code);
|
|
17
|
+
}
|
|
18
|
+
function bounded(value, limit, code, empty = false) {
|
|
19
|
+
if (typeof value !== 'string' || (!empty && !value.trim()) || value.length > limit || controls.test(value)) fail(code);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateConfig(config, dependencies = {}) {
|
|
23
|
+
keys(config, new Set(['provider', 'mode', 'apiKey', 'accountId', 'serviceOrigin', 'installId', 'kid', 'signingKey']), 'invalid_config');
|
|
24
|
+
record(dependencies, 'invalid_dependencies');
|
|
25
|
+
if (!providers.includes(config.provider)) fail('invalid_provider');
|
|
26
|
+
if (!['production', 'development', 'test'].includes(config.mode)) fail('invalid_mode');
|
|
27
|
+
if (dependencies.fetch !== undefined && typeof dependencies.fetch !== 'function') fail('invalid_fetch');
|
|
28
|
+
if (dependencies.clock !== undefined && typeof dependencies.clock !== 'function') fail('invalid_clock');
|
|
29
|
+
if (dependencies.timeoutMilliseconds !== undefined &&
|
|
30
|
+
(!Number.isSafeInteger(dependencies.timeoutMilliseconds) || dependencies.timeoutMilliseconds <= 0)) fail('invalid_timeout');
|
|
31
|
+
if (config.provider === 'messaging') return validateMessagingConfig(config, dependencies) ?? config;
|
|
32
|
+
if (['serviceOrigin', 'installId', 'kid', 'signingKey'].some(field => config[field] !== undefined) || dependencies.serviceBinding !== undefined) fail('unsupported_provider_setting');
|
|
33
|
+
if (config.provider === 'development') {
|
|
34
|
+
if (config.mode !== 'development') fail('development_mode_required');
|
|
35
|
+
if (config.apiKey !== undefined || config.accountId !== undefined) fail('unsupported_development_config');
|
|
36
|
+
if (!dependencies.outbox || typeof dependencies.outbox.put !== 'function') fail('development_outbox_required');
|
|
37
|
+
} else {
|
|
38
|
+
if (typeof config.apiKey !== 'string' || !config.apiKey.trim()) fail('api_key_required');
|
|
39
|
+
bounded(config.apiKey, 4096, 'invalid_api_key');
|
|
40
|
+
if (/\s/.test(config.apiKey)) fail('invalid_api_key');
|
|
41
|
+
if (config.provider === 'cloudflare') {
|
|
42
|
+
if (typeof config.accountId !== 'string' || !/^[a-f0-9]{32}$/i.test(config.accountId)) fail('cloudflare_account_required');
|
|
43
|
+
} else if (config.accountId !== undefined) fail('unsupported_account_id');
|
|
44
|
+
}
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseAddress(value) {
|
|
49
|
+
bounded(value, 998, 'invalid_address');
|
|
50
|
+
const match = value.trim().match(/^(?:(.*?)\s*<([^<>]+)>|([^<>]+))$/u);
|
|
51
|
+
const email = (match?.[2] ?? match?.[3] ?? '').trim();
|
|
52
|
+
// Conservative mailbox subset; never serialize multiple mailboxes as one.
|
|
53
|
+
const parts = email.split('@');
|
|
54
|
+
if (parts.length !== 2 || !parts[0] || parts[0].length > 64 || email.length > 254 ||
|
|
55
|
+
!/^[^\s@<>(),:;"\\]+$/u.test(parts[0]) || parts[0].startsWith('.') || parts[0].endsWith('.') || parts[0].includes('..') ||
|
|
56
|
+
!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(parts[1])) fail('invalid_address');
|
|
57
|
+
let name = match?.[1]?.trim();
|
|
58
|
+
if (name?.startsWith('"') && name.endsWith('"')) name = name.slice(1, -1);
|
|
59
|
+
if (name && /[<>"\\]/.test(name)) fail('invalid_address');
|
|
60
|
+
return name ? { email, name } : { email };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateHeaders(headers = {}) {
|
|
64
|
+
record(headers, 'invalid_headers');
|
|
65
|
+
const names = new Set();
|
|
66
|
+
for (const name of Reflect.ownKeys(headers)) {
|
|
67
|
+
if (typeof name !== 'string' || !/^[!#$%&'*+.^_`|~0-9a-z-]+$/i.test(name) || name.length > 998) fail('invalid_header_name');
|
|
68
|
+
const lower = name.toLowerCase();
|
|
69
|
+
if (reservedHeaders.has(lower) || names.has(lower)) fail('reserved_or_duplicate_header');
|
|
70
|
+
names.add(lower);
|
|
71
|
+
bounded(headers[name], 8192, 'invalid_header_value', true);
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addresses(values, required = false) {
|
|
77
|
+
if (!Array.isArray(values) || (required && !values.length)) fail('invalid_recipients');
|
|
78
|
+
for (const value of values) parseAddress(value);
|
|
79
|
+
}
|
|
80
|
+
function validateAttachment(attachment) {
|
|
81
|
+
keys(attachment, new Set(['filename', 'contentType', 'content', 'disposition', 'contentId']), 'invalid_attachment');
|
|
82
|
+
bounded(attachment.filename, 998, 'invalid_attachment_filename');
|
|
83
|
+
bounded(attachment.contentType, 255, 'invalid_attachment_content_type');
|
|
84
|
+
if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(attachment.contentType)) fail('invalid_attachment_content_type');
|
|
85
|
+
if (!(attachment.content instanceof Uint8Array)) fail('invalid_attachment_content');
|
|
86
|
+
if (attachment.disposition !== undefined && !['attachment', 'inline'].includes(attachment.disposition)) fail('invalid_attachment_disposition');
|
|
87
|
+
if (attachment.contentId !== undefined) {
|
|
88
|
+
bounded(attachment.contentId, 998, 'invalid_attachment_content_id');
|
|
89
|
+
if (/[\s<>]/.test(attachment.contentId)) fail('invalid_attachment_content_id');
|
|
90
|
+
}
|
|
91
|
+
if (attachment.disposition === 'inline' && !attachment.contentId) fail('inline_content_id_required');
|
|
92
|
+
}
|
|
93
|
+
function validateUnsubscribe(value) {
|
|
94
|
+
keys(value, new Set(['url', 'mailto', 'oneClick']), 'invalid_unsubscribe');
|
|
95
|
+
bounded(value.url, 8192, 'invalid_unsubscribe_url');
|
|
96
|
+
let url;
|
|
97
|
+
try { url = new URL(value.url); } catch { fail('invalid_unsubscribe_url'); }
|
|
98
|
+
if (url.protocol !== 'https:' || !url.hostname || url.username || url.password || /[<>]/.test(value.url) || /%0[ad]/i.test(value.url)) fail('invalid_unsubscribe_url');
|
|
99
|
+
if (value.mailto !== undefined) {
|
|
100
|
+
const mailbox = value.mailto.startsWith?.('mailto:') ? value.mailto.slice(7) : value.mailto;
|
|
101
|
+
const parsed = parseAddress(mailbox);
|
|
102
|
+
if (parsed.name || parsed.email !== mailbox) fail('invalid_unsubscribe_mailto');
|
|
103
|
+
}
|
|
104
|
+
if (value.oneClick !== undefined && typeof value.oneClick !== 'boolean') fail('invalid_unsubscribe_one_click');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function validateMessage(message, provider) {
|
|
108
|
+
keys(message, fields, 'invalid_message');
|
|
109
|
+
if (provider !== undefined && !providers.includes(provider)) fail('invalid_provider');
|
|
110
|
+
bounded(message.deliveryId, 512, 'invalid_delivery_id');
|
|
111
|
+
if (provider === 'messaging') {
|
|
112
|
+
identity(message.deliveryId, 512, 'invalid_delivery_id');
|
|
113
|
+
if (message.deliveryId === '.' || message.deliveryId === '..') fail('invalid_delivery_id');
|
|
114
|
+
try { encodeURIComponent(message.deliveryId); } catch { fail('invalid_delivery_id'); }
|
|
115
|
+
if (!['auth', 'transactional', 'bulk'].includes(message.kind)) fail('invalid_message_kind');
|
|
116
|
+
if (!Array.isArray(message.to) || message.to.length !== 1 || message.cc !== undefined || message.bcc !== undefined) fail('messaging_one_recipient_required');
|
|
117
|
+
if (message.expiresAt !== undefined) rfc3339(message.expiresAt, 'invalid_message_expiry');
|
|
118
|
+
} else if (provider !== undefined && (message.kind !== undefined || message.expiresAt !== undefined)) fail('unsupported_provider_setting');
|
|
119
|
+
if (message.kind !== undefined && !['auth', 'transactional', 'bulk'].includes(message.kind)) fail('invalid_message_kind');
|
|
120
|
+
if (message.expiresAt !== undefined) rfc3339(message.expiresAt, 'invalid_message_expiry');
|
|
121
|
+
parseAddress(message.from);
|
|
122
|
+
addresses(message.to, true);
|
|
123
|
+
if (message.cc !== undefined) addresses(message.cc);
|
|
124
|
+
if (message.bcc !== undefined) addresses(message.bcc);
|
|
125
|
+
if (message.replyTo !== undefined) parseAddress(message.replyTo);
|
|
126
|
+
bounded(message.subject, 998, 'invalid_subject', true);
|
|
127
|
+
for (const field of ['text', 'html']) if (message[field] !== undefined && typeof message[field] !== 'string') fail('invalid_body');
|
|
128
|
+
if (!message.text?.trim() && !message.html?.trim()) fail('body_required');
|
|
129
|
+
if (message.headers !== undefined) validateHeaders(message.headers);
|
|
130
|
+
if (message.attachments !== undefined) {
|
|
131
|
+
if (!Array.isArray(message.attachments)) fail('invalid_attachments');
|
|
132
|
+
for (const attachment of message.attachments) validateAttachment(attachment);
|
|
133
|
+
}
|
|
134
|
+
if (message.unsubscribe !== undefined) validateUnsubscribe(message.unsubscribe);
|
|
135
|
+
const settings = ['customArgs', 'asmGroupId', 'subscriptionTracking'];
|
|
136
|
+
if (provider && !['sendgrid', 'development'].includes(provider) && settings.some(key => message[key] !== undefined)) fail('unsupported_provider_setting');
|
|
137
|
+
if (message.asmGroupId !== undefined && (!Number.isSafeInteger(message.asmGroupId) || message.asmGroupId <= 0)) fail('invalid_asm_group');
|
|
138
|
+
if (message.subscriptionTracking !== undefined && typeof message.subscriptionTracking !== 'boolean') fail('invalid_subscription_tracking');
|
|
139
|
+
const args = message.customArgs ?? {};
|
|
140
|
+
if (message.customArgs !== undefined) record(message.customArgs, 'invalid_custom_args');
|
|
141
|
+
for (const key of Reflect.ownKeys(args)) {
|
|
142
|
+
if (typeof key !== 'string' || !key || controls.test(key) || reservedArgs.has(key) || typeof args[key] !== 'string') fail('invalid_custom_arg');
|
|
143
|
+
}
|
|
144
|
+
if (new TextEncoder().encode(JSON.stringify({ ...args, rer_delivery_id: message.deliveryId })).byteLength > 10_000) fail('custom_args_too_large');
|
|
145
|
+
return message;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function encodeBase64(bytes) {
|
|
149
|
+
if (!(bytes instanceof Uint8Array)) fail('invalid_attachment_content');
|
|
150
|
+
let binary = '';
|
|
151
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
|
|
152
|
+
return btoa(binary);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function validateSendResult(result, provider) {
|
|
156
|
+
if (provider === 'messaging') return validateMessagingResult(result);
|
|
157
|
+
keys(result, new Set(['provider', 'status', 'providerMessageId', 'code', 'recipientResults']), 'invalid_send_result');
|
|
158
|
+
if (!providers.includes(provider) || result.provider !== provider || !['accepted', 'rejected', 'rate_limited', 'uncertain', 'development'].includes(result.status)) fail('invalid_send_result');
|
|
159
|
+
if ((provider === 'development') !== (result.status === 'development')) fail('invalid_send_result');
|
|
160
|
+
if (result.providerMessageId !== null) bounded(result.providerMessageId, 1024, 'invalid_provider_message_id');
|
|
161
|
+
if (result.status === 'development' && result.providerMessageId !== null) fail('invalid_send_result');
|
|
162
|
+
if (result.code !== undefined && (typeof result.code !== 'string' || !/^[a-z][a-z0-9_]{0,63}$/.test(result.code))) fail('invalid_result_code');
|
|
163
|
+
if (result.recipientResults !== undefined) {
|
|
164
|
+
if (!Array.isArray(result.recipientResults)) fail('invalid_recipient_results');
|
|
165
|
+
for (const item of result.recipientResults) {
|
|
166
|
+
keys(item, new Set(['recipient', 'status']), 'invalid_recipient_result');
|
|
167
|
+
parseAddress(item.recipient);
|
|
168
|
+
if (!['accepted', 'rejected', 'rate_limited', 'uncertain'].includes(item.status)) fail('invalid_recipient_status');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface DevelopmentMessage {
|
|
2
|
+
deliveryId: string;
|
|
3
|
+
from: string;
|
|
4
|
+
to: string[];
|
|
5
|
+
subject: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
cc?: string[];
|
|
9
|
+
bcc?: string[];
|
|
10
|
+
replyTo?: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
attachments?: Array<{ filename: string; contentType: string; content: Uint8Array; disposition?: 'attachment' | 'inline'; contentId?: string }>;
|
|
13
|
+
customArgs?: Record<string, string>;
|
|
14
|
+
asmGroupId?: number;
|
|
15
|
+
subscriptionTracking?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface DevelopmentCapture { id: string; createdAt: number; message: DevelopmentMessage }
|
|
18
|
+
export type DevelopmentOutboxListMessage = Omit<DevelopmentMessage, 'attachments'>;
|
|
19
|
+
export interface DevelopmentOutboxListCapture { id: string; createdAt: number; message: DevelopmentOutboxListMessage }
|
|
20
|
+
export interface DevelopmentOutbox {
|
|
21
|
+
put(message: DevelopmentMessage): Promise<void>;
|
|
22
|
+
list(): Promise<DevelopmentOutboxListCapture[]>;
|
|
23
|
+
get(id: string): Promise<DevelopmentCapture | null>;
|
|
24
|
+
}
|
|
25
|
+
export interface DevelopmentD1Statement { bind(...values: unknown[]): DevelopmentD1Statement }
|
|
26
|
+
export interface DevelopmentD1Database {
|
|
27
|
+
prepare(sql: string): DevelopmentD1Statement;
|
|
28
|
+
batch(statements: DevelopmentD1Statement[]): Promise<Array<{ success: boolean; results: unknown[] }>>;
|
|
29
|
+
}
|
|
30
|
+
export function createD1DevelopmentOutbox(options: { db: DevelopmentD1Database; maxMessages?: number; maxAgeMs?: number; clock?: () => number }): DevelopmentOutbox;
|
|
31
|
+
export function createDevelopmentOutboxHandler(options: {
|
|
32
|
+
local: boolean; origin: string; outbox: Pick<DevelopmentOutbox, 'list' | 'get'>; path?: string;
|
|
33
|
+
/** The seed's served theme stylesheet, a root-relative `.css` path such as '/rer-theme.css'; other values throw. */
|
|
34
|
+
theme?: string;
|
|
35
|
+
}): (request: Request) => Promise<Response | null>;
|
|
36
|
+
export function sendDevelopment(config: { provider: 'development'; mode: 'development' }, message: DevelopmentMessage,
|
|
37
|
+
dependencies: { outbox: Pick<DevelopmentOutbox, 'put'>; clock?: () => number }): Promise<{ provider: 'development'; status: 'development'; providerMessageId: null }>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MailValidationError, fail } from '../core/errors.mjs';
|
|
2
|
+
import { validateConfig, validateMessage } from '../core/validation.mjs';
|
|
3
|
+
import { sendResult } from '../core/transport.mjs';
|
|
4
|
+
|
|
5
|
+
export { createD1DevelopmentOutbox } from './outbox.mjs';
|
|
6
|
+
export { createDevelopmentOutboxHandler } from './viewer.mjs';
|
|
7
|
+
|
|
8
|
+
function validate(operation, errorCode) {
|
|
9
|
+
try {
|
|
10
|
+
return operation();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof MailValidationError) throw error;
|
|
13
|
+
fail(errorCode);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Store one explicitly selected development message without contacting a provider. */
|
|
18
|
+
export async function sendDevelopment(config, message, dependencies = {}) {
|
|
19
|
+
validate(() => validateConfig(config, dependencies), 'invalid_config');
|
|
20
|
+
if (validate(() => config.provider, 'invalid_config') !== 'development') fail('development_provider_required');
|
|
21
|
+
|
|
22
|
+
validate(() => validateMessage(message, 'development'), 'invalid_message');
|
|
23
|
+
if (validate(() => message.unsubscribe, 'invalid_message') !== undefined) fail('unsubscribe_not_assembled');
|
|
24
|
+
|
|
25
|
+
const snapshot = validate(() => structuredClone(message), 'invalid_message');
|
|
26
|
+
validate(() => validateMessage(snapshot, 'development'), 'invalid_message');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await dependencies.outbox.put(snapshot);
|
|
30
|
+
} catch {
|
|
31
|
+
fail('development_outbox_failed');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return sendResult('development', 'development', null);
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { validateMessage, encodeBase64 } from '../core/validation.mjs';
|
|
2
|
+
import { fail } from '../core/errors.mjs';
|
|
3
|
+
|
|
4
|
+
function positive(value) {
|
|
5
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError('Invalid development outbox retention');
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function restore(row) {
|
|
10
|
+
const message = JSON.parse(row.message);
|
|
11
|
+
if (message.attachments) message.attachments = message.attachments.map(attachment => ({
|
|
12
|
+
...attachment, content: Uint8Array.from(atob(attachment.content), character => character.charCodeAt(0)),
|
|
13
|
+
}));
|
|
14
|
+
return { id: row.id, createdAt: row.created_at, message };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function summarize(row) {
|
|
18
|
+
const message = JSON.parse(row.message);
|
|
19
|
+
delete message.attachments;
|
|
20
|
+
return { id: row.id, createdAt: row.created_at, message };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Durable local capture; each capture has its own ID, even for repeated delivery IDs. */
|
|
24
|
+
export function createD1DevelopmentOutbox({ db, maxMessages = 100, maxAgeMs = 7 * 24 * 60 * 60 * 1000, clock = Date.now } = {}) {
|
|
25
|
+
if (!db || typeof db.prepare !== 'function' || typeof db.batch !== 'function' || typeof clock !== 'function') {
|
|
26
|
+
throw new TypeError('Invalid development outbox configuration');
|
|
27
|
+
}
|
|
28
|
+
positive(maxMessages); positive(maxAgeMs);
|
|
29
|
+
function now() {
|
|
30
|
+
const value = clock();
|
|
31
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 8_640_000_000_000_000) throw new TypeError('Invalid development outbox clock');
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
const statement = (sql, ...values) => db.prepare(sql).bind(...values);
|
|
35
|
+
function retention(time) {
|
|
36
|
+
return [
|
|
37
|
+
statement('DELETE FROM rer_mail_development_outbox WHERE created_at <= ?', time - maxAgeMs),
|
|
38
|
+
statement(`DELETE FROM rer_mail_development_outbox WHERE sequence NOT IN
|
|
39
|
+
(SELECT sequence FROM rer_mail_development_outbox ORDER BY created_at DESC, sequence DESC LIMIT ?)`, maxMessages),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
async function batch(statements) {
|
|
43
|
+
const results = await db.batch(statements);
|
|
44
|
+
if (!Array.isArray(results) || results.length !== statements.length || results.some(result => result.success !== true)) {
|
|
45
|
+
throw new Error('Development outbox storage failed');
|
|
46
|
+
}
|
|
47
|
+
return results.at(-1);
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
async put(message) {
|
|
51
|
+
validateMessage(message, 'development');
|
|
52
|
+
if (message.unsubscribe !== undefined) fail('unsubscribe_not_assembled');
|
|
53
|
+
const snapshot = structuredClone(message);
|
|
54
|
+
validateMessage(snapshot, 'development');
|
|
55
|
+
if (snapshot.unsubscribe !== undefined) fail('unsubscribe_not_assembled');
|
|
56
|
+
if (snapshot.attachments) snapshot.attachments = snapshot.attachments.map(attachment => ({
|
|
57
|
+
...attachment, content: encodeBase64(attachment.content),
|
|
58
|
+
}));
|
|
59
|
+
const time = now();
|
|
60
|
+
await batch([
|
|
61
|
+
statement('INSERT INTO rer_mail_development_outbox(id, created_at, message) VALUES (?, ?, ?)', crypto.randomUUID(), time, JSON.stringify(snapshot)),
|
|
62
|
+
...retention(time),
|
|
63
|
+
]);
|
|
64
|
+
},
|
|
65
|
+
async list() {
|
|
66
|
+
const time = now();
|
|
67
|
+
const result = await batch([statement(`SELECT id, created_at, message FROM rer_mail_development_outbox
|
|
68
|
+
WHERE created_at > ? ORDER BY created_at DESC, sequence DESC LIMIT ?`, time - maxAgeMs, maxMessages)]);
|
|
69
|
+
return result.results.map(summarize);
|
|
70
|
+
},
|
|
71
|
+
async get(id) {
|
|
72
|
+
if (typeof id !== 'string' || !id || id.length > 128) throw new TypeError('Invalid development outbox ID');
|
|
73
|
+
const result = await batch([statement(`SELECT id, created_at, message FROM rer_mail_development_outbox
|
|
74
|
+
WHERE id = ? AND created_at > ?`, id, now() - maxAgeMs)]);
|
|
75
|
+
return result.results.length ? restore(result.results[0]) : null;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Additive local-development storage. Apply explicitly to the local D1 database.
|
|
2
|
+
CREATE TABLE IF NOT EXISTS rer_mail_development_outbox (
|
|
3
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
id TEXT NOT NULL UNIQUE,
|
|
5
|
+
created_at INTEGER NOT NULL,
|
|
6
|
+
message TEXT NOT NULL
|
|
7
|
+
);
|
|
8
|
+
CREATE INDEX IF NOT EXISTS rer_mail_development_outbox_order
|
|
9
|
+
ON rer_mail_development_outbox(created_at DESC, sequence DESC);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const style = `*{box-sizing:border-box}body{margin:0;background:#f5f7fa;color:#17212e;font:16px/1.6 system-ui,sans-serif}main{max-width:52rem;margin:0 auto;padding:24px 16px}h1{font-size:1.75rem;line-height:1.2}article{margin:20px 0;padding:20px;background:white;border:1px solid #cad2dc;border-radius:12px}h2{font-size:1.2rem}h1,h2,p,a,pre{overflow-wrap:anywhere;word-break:normal}pre{white-space:pre-wrap;font:inherit}a{color:#174fb5}a.open{display:block;padding:16px;margin:20px 0;border-radius:8px;background:#174fb5;color:white;font-weight:700;text-align:center;text-decoration:none;min-height:56px}small{color:#465569}`;
|
|
2
|
+
|
|
3
|
+
// Added only when the seed declares a theme: its linked rer-theme.css supplies the
|
|
4
|
+
// --rer-* tokens. Each falls back to the plain sheet's value, and the focus ring is
|
|
5
|
+
// always drawn, so a theme that fails to load leaves a readable, usable page.
|
|
6
|
+
export const themedStyle = `.rer-themed body{background:var(--rer-page,#f5f7fa);color:var(--rer-text,#17212e);font-family:var(--rer-font-body,system-ui,sans-serif)}.rer-themed h1,.rer-themed h2{font-family:var(--rer-font-heading,var(--rer-font-body,system-ui,sans-serif))}.rer-themed article{background:var(--rer-surface,#fff);border-color:var(--rer-border,#cad2dc);border-radius:var(--rer-radius,12px)}.rer-themed p,.rer-themed small{color:var(--rer-muted,#465569)}.rer-themed h2,.rer-themed pre{color:var(--rer-text,#17212e)}.rer-themed a{color:var(--rer-link,#174fb5)}.rer-themed a.open{background:var(--rer-accent,#174fb5);color:var(--rer-accent-text,#fff);border-radius:var(--rer-radius,8px)}.rer-themed :focus-visible{outline:3px solid var(--rer-focus,#17212e);outline-offset:3px}`;
|
|
7
|
+
|
|
8
|
+
export function escape(value) {
|
|
9
|
+
return String(value).replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Read link candidates as data only. Never parse or mount mail HTML in the page.
|
|
13
|
+
export function firstLink(message) {
|
|
14
|
+
for (const body of [message.text, message.html]) {
|
|
15
|
+
if (typeof body !== 'string') continue;
|
|
16
|
+
for (const match of body.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
|
|
17
|
+
try {
|
|
18
|
+
const candidate = match[0].replace(/&/g, '&').replace(/[.,;!?]+$/, '');
|
|
19
|
+
const url = new URL(candidate);
|
|
20
|
+
if (['http:', 'https:'].includes(url.protocol) && url.hostname && !url.username && !url.password) return url.href;
|
|
21
|
+
} catch { /* Malformed candidates are ordinary message text. */ }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function metadata(row) {
|
|
28
|
+
return `<h2>${escape(row.message.subject || '(No subject)')}</h2><p>To: ${escape(row.message.to.join(', '))}</p><p>Received: <time datetime="${escape(new Date(row.createdAt).toISOString())}">${escape(new Date(row.createdAt).toUTCString())}</time></p>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function document(body, theme) {
|
|
32
|
+
return `<!doctype html><html lang="en"${theme ? ' class="rer-themed"' : ''}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex, nofollow"><title>Development outbox</title>${theme ? `<link rel="stylesheet" href="${escape(theme)}">` : ''}<style>${theme ? style + themedStyle : style}</style></head><body><main><h1>Development outbox</h1><p>Messages captured on this computer. No email was sent.</p>${body}</main></body></html>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function listing(rows, path) {
|
|
36
|
+
return rows.length ? rows.map(row => `<article>${metadata(row)}<p><a href="${escape(`${path}?message=${encodeURIComponent(row.id)}`)}">Read message</a></p></article>`).join('') : '<p>No messages yet.</p>';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detail(row, path) {
|
|
40
|
+
const link = firstLink(row.message);
|
|
41
|
+
return `<p><a href="${escape(path)}">All messages</a></p><article>${metadata(row)}${link ? `<a class="open" href="${escape(link)}" rel="noreferrer noopener">Open this link</a>` : ''}<h2>Message text</h2><pre>${escape(row.message.text || row.message.html || '')}</pre>${row.message.attachments?.length ? `<p>${row.message.attachments.length} attachment(s) stored with this message.</p>` : ''}</article>`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { detail, document, listing, style, themedStyle } from './viewer-html.mjs';
|
|
2
|
+
|
|
3
|
+
function localOrigin(origin) {
|
|
4
|
+
if (typeof origin !== 'string') return null;
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(origin);
|
|
7
|
+
const ipv4 = url.hostname.split('.');
|
|
8
|
+
const loopback = url.hostname === 'localhost' || url.hostname === '[::1]' ||
|
|
9
|
+
(ipv4.length === 4 && ipv4[0] === '127' && ipv4.every(part => /^(0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255));
|
|
10
|
+
return loopback && ['http:', 'https:'].includes(url.protocol) && url.origin === origin ? url : null;
|
|
11
|
+
} catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function routePath(path) {
|
|
15
|
+
return typeof path === 'string' && /^\/(?:[A-Za-z0-9_-]+\/?)+$/.test(path) && !path.includes('//');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The seed's own served stylesheet, in the grammar operator-auth branding accepts.
|
|
19
|
+
function themePath(theme) {
|
|
20
|
+
return typeof theme === 'string' && /^\/(?!\/)[A-Za-z0-9._~/-]{1,200}\.css$/.test(theme) && !theme.includes('//') && !theme.includes('/.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Opt-in local route. Rejected requests return null without touching the outbox. */
|
|
24
|
+
export function createDevelopmentOutboxHandler({ local, origin, outbox, path = '/__dev/outbox', theme } = {}) {
|
|
25
|
+
if (theme !== undefined && !themePath(theme)) throw new TypeError('Declare the outbox theme as a same-origin path to the seed\'s served .css file');
|
|
26
|
+
const configured = local === true ? localOrigin(origin) : null;
|
|
27
|
+
if (!configured || !routePath(path)) return async () => null;
|
|
28
|
+
if (!outbox || typeof outbox.list !== 'function' || typeof outbox.get !== 'function') throw new TypeError('Invalid development outbox reader');
|
|
29
|
+
const sheet = theme ? style + themedStyle : style;
|
|
30
|
+
const styles = hash => theme ? `style-src 'self' 'sha256-${hash}'; font-src 'self'; img-src 'self'` : `style-src 'sha256-${hash}'`;
|
|
31
|
+
let hash;
|
|
32
|
+
async function response(body, status = 200, head = false) {
|
|
33
|
+
hash ??= crypto.subtle.digest('SHA-256', new TextEncoder().encode(sheet)).then(bytes => btoa(String.fromCharCode(...new Uint8Array(bytes))));
|
|
34
|
+
return new Response(head ? null : document(body, theme), { status, headers: {
|
|
35
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
36
|
+
'Cache-Control': 'no-store',
|
|
37
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
38
|
+
'X-Content-Type-Options': 'nosniff',
|
|
39
|
+
'Referrer-Policy': 'no-referrer',
|
|
40
|
+
'Content-Security-Policy': `default-src 'none'; script-src 'none'; ${styles(await hash)}; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'`,
|
|
41
|
+
...(status === 405 ? { Allow: 'GET, HEAD' } : {}),
|
|
42
|
+
} });
|
|
43
|
+
}
|
|
44
|
+
return async function handle(request) {
|
|
45
|
+
let url;
|
|
46
|
+
try { url = new URL(request.url); } catch { return null; }
|
|
47
|
+
if (url.origin !== configured.origin || url.username || url.password || request.headers.get('host') !== configured.host || url.pathname !== path) return null;
|
|
48
|
+
const requestOrigin = request.headers.get('origin');
|
|
49
|
+
if ((requestOrigin !== null && requestOrigin !== configured.origin) || request.headers.get('sec-fetch-site') === 'cross-site') return null;
|
|
50
|
+
const head = request.method === 'HEAD';
|
|
51
|
+
if (request.method !== 'GET' && !head) return response('<p>Use GET to read messages.</p>', 405);
|
|
52
|
+
if ([...url.searchParams.keys()].some(key => key !== 'message') || url.searchParams.getAll('message').length > 1) return response('<p>Invalid message request.</p>', 400, head);
|
|
53
|
+
try {
|
|
54
|
+
if (url.searchParams.has('message')) {
|
|
55
|
+
const id = url.searchParams.get('message');
|
|
56
|
+
if (!id || id.length > 128) return response('<p>Invalid message request.</p>', 400, head);
|
|
57
|
+
const row = await outbox.get(id);
|
|
58
|
+
return response(row ? detail(row, path) : '<p>Message not found or expired.</p>', row ? 200 : 404, head);
|
|
59
|
+
}
|
|
60
|
+
return response(listing(await outbox.list(), path), 200, head);
|
|
61
|
+
} catch {
|
|
62
|
+
return response('<p>The development outbox is unavailable.</p>', 503, head);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Mail doctor API
|
|
2
|
+
|
|
3
|
+
`mailDoctor(options, dependencies = {})` is portable ESM. Import from the public
|
|
4
|
+
mail facade, or directly from
|
|
5
|
+
`modules/mail/src/doctor/index.mjs`. No Node APIs enter this runtime graph.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const result = await mailDoctor({
|
|
9
|
+
domain: 'mail.example.com', provider: 'resend', live: false,
|
|
10
|
+
dnsRecords: { mx: [], spf: [], dkim: [], dmarc: [] },
|
|
11
|
+
});
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each supplied DNS group contains `{type, name, value, priority?}` records. Types
|
|
15
|
+
are uppercase `MX`, `TXT`, `CNAME`; names are fully qualified (a final dot is
|
|
16
|
+
optional). MX requires numeric priority. Supply unquoted TXT values, including
|
|
17
|
+
the complete SPF/DMARC policy. SPF supports a provider return-path CNAME; DKIM
|
|
18
|
+
supports TXT or CNAME at the provider-assigned `selector._domainkey` name. Do
|
|
19
|
+
not guess a selector, public key, MX host, SPF include or return-path value.
|
|
20
|
+
Copy exact values from the selected provider's domain setup. Receiving MX may
|
|
21
|
+
belong to a different receiving provider and domain from the sender. All four
|
|
22
|
+
groups can therefore contain names other than `domain`.
|
|
23
|
+
|
|
24
|
+
The six ordered items are `mx`, `spf`, `dkim`, `dmarc`, `send`, `receive`. Each
|
|
25
|
+
has `status`, `nextSteps`, optional `records`, and redacted `evidence`. A plan
|
|
26
|
+
has only `pending`/`not_checked` items. Supplied records remain a plan until
|
|
27
|
+
queried. Missing setup values have actionable instructions and empty records;
|
|
28
|
+
they are never invented or treated as published records.
|
|
29
|
+
|
|
30
|
+
For explicit live operation add:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
{
|
|
34
|
+
live: true,
|
|
35
|
+
send: {config: senderConfig, from: verifiedSender, to: ownerTestInbox},
|
|
36
|
+
receive: readbackConfig,
|
|
37
|
+
timeoutMs: 60000, pollIntervalMs: 2000
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`senderConfig` is an ordinary mail sender configuration, for example
|
|
42
|
+
`{provider: 'resend', mode: 'production', apiKey}` with the sending credential
|
|
43
|
+
as `apiKey`. `readbackConfig` is `{provider: 'resend', apiKey}` with a separate
|
|
44
|
+
readback credential as `apiKey`. Both credentials come from the environment.
|
|
45
|
+
|
|
46
|
+
Sender provider must agree with `options.provider`; sending domain must match
|
|
47
|
+
`domain`. SendGrid and Cloudflare senders use the same facade (Cloudflare also
|
|
48
|
+
requires `config.accountId`). The built-in readback is an actual Resend receiving
|
|
49
|
+
mailbox, independent of the sending provider. An existing arbitrary mailbox
|
|
50
|
+
cannot be proven by this readback: provision an approved Resend receiving address
|
|
51
|
+
or implement a separately reviewed readback before claiming proof. All live
|
|
52
|
+
inputs validate before any effect. Complete live config is required even when
|
|
53
|
+
some DNS setup values have not yet been supplied.
|
|
54
|
+
|
|
55
|
+
DNS uses a fixed Cloudflare DNS-over-HTTPS endpoint and bounded GETs. Comparison
|
|
56
|
+
reports `match`, `missing`, `mismatch`, `error` or `unchecked` in evidence. Empty
|
|
57
|
+
NOERROR/NXDOMAIN means missing; resolver transport failures, malformed responses
|
|
58
|
+
and other DNS response codes mean error. TXT character strings are concatenated;
|
|
59
|
+
MX priorities and CNAME hostnames compare canonically. Duplicate SPF/DMARC
|
|
60
|
+
policies fail. A matching explicit record plan does not prove full recursive SPF
|
|
61
|
+
evaluation, DKIM message signatures or DMARC alignment. Undeclared selectors,
|
|
62
|
+
parent-domain DMARC discovery and provider dashboard verification are outside
|
|
63
|
+
this check. A missing plan remains unchecked even in live mode.
|
|
64
|
+
|
|
65
|
+
Live sends exactly once through `createMailSender`; there is no retry. Every run
|
|
66
|
+
creates a random UUID in the subject and plain-text body. `send:pass` means
|
|
67
|
+
provider acceptance only. `receive:pass` requires an independent received-mail
|
|
68
|
+
list and detail read, matching received ID, exact sender/recipient, fresh receipt
|
|
69
|
+
time (one-second timestamp tolerance), exact subject/body and an RFC Message-ID.
|
|
70
|
+
Uncertain submissions may be read back but retain their uncertain send result.
|
|
71
|
+
No send response or sent-mail status endpoint can establish receipt.
|
|
72
|
+
|
|
73
|
+
The default receive timeout is 60 seconds, configurable from 100 to 120000 ms;
|
|
74
|
+
poll spacing is 100–10000 ms. Limits are 30 polls, three pages of 100 messages per
|
|
75
|
+
poll, eight seconds per HTTP request and 256 KiB per receive response. A busy
|
|
76
|
+
mailbox exceeding that scan remains pending. DNS performs at most 32 distinct
|
|
77
|
+
eight-second queries concurrently; send has a 16-second outer bound. DNS and
|
|
78
|
+
send/readback run concurrently, after validation. Redirects are refused. Only
|
|
79
|
+
fixed provider origins are fetched; response IDs must be UUIDs before use in
|
|
80
|
+
paths, and remote attachment/raw-download URLs are never followed.
|
|
81
|
+
|
|
82
|
+
Dependencies are `{fetch?, resolveDns?, sender?}`. `fetch` is the standard Web
|
|
83
|
+
fetch host/test seam. `resolveDns({name,type,signal})` returns
|
|
84
|
+
`{status:'ok'|'missing'|'error',records:[{type,name,value,priority?}]}`; errors can
|
|
85
|
+
omit records. The injected resolver must honor abort; the caller also bounds
|
|
86
|
+
the wait. `sender:{send(message)}` is an explicit unit-test seam using the frozen
|
|
87
|
+
sender result contract. Production omits it and loads the real sender facade.
|
|
88
|
+
Do not expose dependency selection to application users. There is no fixture
|
|
89
|
+
provider, automatic outbox selection, custom URL, or dynamic code loader.
|
|
90
|
+
|
|
91
|
+
Evidence contains the random correlation ID, actual received-mail UUID, hashes
|
|
92
|
+
of send-provider/RFC message IDs, recipient and body, timing and counts. It never
|
|
93
|
+
returns credentials, message content, arbitrary headers, raw responses or remote
|
|
94
|
+
error messages. API keys belong only in private process/config memory. A UI
|
|
95
|
+
calling this function owns user authorization and test-inbox approval.
|
|
96
|
+
|
|
97
|
+
Run unit checks: `node --test modules/mail/tests/doctor/*.test.mjs`. These tests
|
|
98
|
+
inject explicit sender and network fixtures only under `./tests/doctor`; they prove
|
|
99
|
+
protocol/conformance, not delivery. `tests/assembly/workerd.test.mjs` exercises
|
|
100
|
+
the doctor through the real facade and all three sending adapters in workerd. See `../../live-proof/README.md` for protected proof.
|
|
101
|
+
|
|
102
|
+
Primary contracts checked 2026-10-06:
|
|
103
|
+
|
|
104
|
+
- [Cloudflare DNS JSON](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/)
|
|
105
|
+
- [Resend received-mail listing](https://resend.com/docs/api-reference/emails/list-received-emails)
|
|
106
|
+
- [Resend received-mail retrieval](https://resend.com/docs/api-reference/emails/retrieve-received-email)
|
|
107
|
+
- [Resend pagination](https://resend.com/docs/api-reference/pagination)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
|
|
3
|
+
export function object(value, allowed, code = 'invalid_doctor_options') {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
|
|
5
|
+
Reflect.ownKeys(value).some(key => typeof key !== 'string' || !allowed.includes(key))) fail(code);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hostname(value, underscore = false) {
|
|
10
|
+
if (typeof value !== 'string' || value.length > 253) fail('invalid_doctor_domain');
|
|
11
|
+
const name = value.toLowerCase().replace(/\.$/, '');
|
|
12
|
+
const label = underscore ? /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/ : /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
13
|
+
if (!name.includes('.') || !name.split('.').every(part => label.test(part)) || /^\d+(\.\d+)+$/.test(name)) fail('invalid_doctor_domain');
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function integer(value, min, max, code) {
|
|
18
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) fail(code);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function credential(value) {
|
|
23
|
+
if (typeof value !== 'string' || !value || value.length > 4096 || /\s|[\x00-\x1f\x7f]/.test(value)) fail('doctor_credential_required');
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Both the request and body read share a deadline. Never include remote errors.
|
|
28
|
+
export async function bounded(operation, timeoutMs, code = 'doctor_timeout') {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
let timer;
|
|
31
|
+
try {
|
|
32
|
+
return await Promise.race([
|
|
33
|
+
Promise.resolve().then(() => operation(controller.signal)),
|
|
34
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error(code)); }, timeoutMs); }),
|
|
35
|
+
]);
|
|
36
|
+
} finally { clearTimeout(timer); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readJson(response, maxBytes = 262144) {
|
|
40
|
+
if (!response.body || (Number(response.headers.get('content-length')) > maxBytes)) fail('doctor_response_limit');
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let size = 0;
|
|
44
|
+
try {
|
|
45
|
+
while (true) {
|
|
46
|
+
const { value, done } = await reader.read();
|
|
47
|
+
if (done) break;
|
|
48
|
+
size += value.byteLength;
|
|
49
|
+
if (size > maxBytes) fail('doctor_response_limit');
|
|
50
|
+
chunks.push(value);
|
|
51
|
+
}
|
|
52
|
+
} catch { void reader.cancel().catch(() => {}); fail('doctor_response_invalid'); }
|
|
53
|
+
finally { reader.releaseLock(); }
|
|
54
|
+
const bytes = new Uint8Array(size);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
|
|
57
|
+
try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
58
|
+
catch { fail('doctor_response_invalid'); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function digest(value) {
|
|
62
|
+
const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
63
|
+
return [...new Uint8Array(bytes)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
64
|
+
}
|