@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,76 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
|
|
3
|
+
const terminal = new Set(['delivered', 'bounced', 'failed', 'rejected', 'complained']);
|
|
4
|
+
// Only this constructor's stores can be passed as the intake's explicit store.
|
|
5
|
+
const stores = new WeakSet();
|
|
6
|
+
export const isD1EventStore = store => stores.has(store);
|
|
7
|
+
const receipt = `INSERT INTO rer_mail_event_receipts
|
|
8
|
+
(provider, tenant_id, event_namespace, event_id, fingerprint, event_json, commit_token, ordinal)
|
|
9
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
10
|
+
ON CONFLICT(provider, tenant_id, event_namespace, event_id) DO UPDATE SET conflict = 1
|
|
11
|
+
WHERE rer_mail_event_receipts.fingerprint <> excluded.fingerprint
|
|
12
|
+
RETURNING event_id`;
|
|
13
|
+
const state = `INSERT INTO rer_mail_event_states
|
|
14
|
+
(provider, tenant_id, event_namespace, provider_message_id, recipient, status, occurred_at, event_id, terminal)
|
|
15
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (
|
|
16
|
+
SELECT 1 FROM rer_mail_event_receipts WHERE provider = ? AND tenant_id = ? AND event_namespace = ?
|
|
17
|
+
AND event_id = ? AND commit_token = ? AND ordinal = ?)
|
|
18
|
+
ON CONFLICT(provider, tenant_id, event_namespace, provider_message_id, recipient) DO UPDATE SET
|
|
19
|
+
status = excluded.status, occurred_at = excluded.occurred_at, event_id = excluded.event_id, terminal = excluded.terminal
|
|
20
|
+
WHERE (excluded.terminal = 1 AND rer_mail_event_states.terminal = 0)
|
|
21
|
+
OR (excluded.occurred_at > rer_mail_event_states.occurred_at
|
|
22
|
+
AND (rer_mail_event_states.terminal = 0 OR excluded.terminal = 1))
|
|
23
|
+
RETURNING status, occurred_at, event_id`;
|
|
24
|
+
const read = `SELECT status, occurred_at, event_id FROM rer_mail_event_states
|
|
25
|
+
WHERE provider = ? AND tenant_id = ? AND event_namespace = ? AND provider_message_id = ? AND recipient = ?`;
|
|
26
|
+
function conflict(error) {
|
|
27
|
+
for (let current = error, count = 0; current && count < 5; current = current.cause, count++) {
|
|
28
|
+
if (typeof current.message === 'string' && current.message.includes('rer_mail_event_identity_conflict')) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/** D1 batch is one real SQL transaction. No read-before-write and no mutex. */
|
|
33
|
+
export function createD1EventStore({ db } = {}) {
|
|
34
|
+
if (!db || typeof db.prepare !== 'function' || typeof db.batch !== 'function') fail('event_storage_required');
|
|
35
|
+
const store = Object.freeze({
|
|
36
|
+
async commit(events) {
|
|
37
|
+
try {
|
|
38
|
+
const token = crypto.randomUUID();
|
|
39
|
+
const statements = [], layout = [];
|
|
40
|
+
for (const [ordinal, event] of events.entries()) {
|
|
41
|
+
const scope = [event.provider, event.tenantId, event.eventNamespace];
|
|
42
|
+
const entry = { eventId: event.eventId, receipt: statements.length, recipients: [] };
|
|
43
|
+
statements.push(db.prepare(receipt).bind(...scope, event.eventId, event.fingerprint, JSON.stringify(event), token, ordinal));
|
|
44
|
+
for (const recipient of event.recipients) {
|
|
45
|
+
const indices = { recipient, update: null, read: null };
|
|
46
|
+
if (event.status !== null) {
|
|
47
|
+
indices.update = statements.length;
|
|
48
|
+
statements.push(db.prepare(state).bind(...scope, event.providerMessageId, recipient, event.status,
|
|
49
|
+
event.occurredAt, event.eventId, terminal.has(event.status) ? 1 : 0,
|
|
50
|
+
...scope, event.eventId, token, ordinal));
|
|
51
|
+
}
|
|
52
|
+
indices.read = statements.length;
|
|
53
|
+
statements.push(db.prepare(read).bind(...scope, event.providerMessageId, recipient));
|
|
54
|
+
entry.recipients.push(indices);
|
|
55
|
+
}
|
|
56
|
+
layout.push(entry);
|
|
57
|
+
}
|
|
58
|
+
if (!statements.length) return [];
|
|
59
|
+
const results = await db.batch(statements);
|
|
60
|
+
if (results.some(result => result.success === false)) throw new Error();
|
|
61
|
+
return layout.map(entry => {
|
|
62
|
+
if (!results[entry.receipt].results.length) return { eventId: entry.eventId, outcome: 'duplicate', recipients: [] };
|
|
63
|
+
return { eventId: entry.eventId, outcome: 'stored', recipients: entry.recipients.map(item => {
|
|
64
|
+
const row = results[item.read].results[0];
|
|
65
|
+
return { recipient: item.recipient, applied: item.update !== null && results[item.update].results.length > 0,
|
|
66
|
+
state: row ? { status: row.status, occurredAt: row.occurred_at, eventId: row.event_id } : null };
|
|
67
|
+
}) };
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
fail(conflict(error) ? 'event_identity_conflict' : 'event_storage_failed');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
stores.add(store);
|
|
75
|
+
return store;
|
|
76
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { parseAddress } from '../core/validation.mjs';
|
|
3
|
+
import { verifyWebhook } from './verify.mjs';
|
|
4
|
+
import { createDurableEventStore } from './storage.mjs';
|
|
5
|
+
import { isD1EventStore } from './d1-storage.mjs';
|
|
6
|
+
import { pinnedKeys, verifyEventJws } from '../adapters/messaging/jws.mjs';
|
|
7
|
+
import { serviceOrigin, identity, closed } from '../adapters/messaging/contract.mjs';
|
|
8
|
+
import { messagingEvent } from '../adapters/messaging/event-schema.mjs';
|
|
9
|
+
|
|
10
|
+
const MAX_EVENT_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_EVENTS = 1000;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const sgStatuses = { processed: 'accepted', deferred: 'deferred', delivered: 'delivered', bounce: 'bounced', dropped: 'rejected', spamreport: 'complained', open: null, click: null, unsubscribe: null, group_unsubscribe: null, group_resubscribe: null };
|
|
14
|
+
const resendStatuses = { 'email.sent': 'accepted', 'email.delivery_delayed': 'deferred', 'email.delivered': 'delivered', 'email.bounced': 'bounced', 'email.failed': 'failed', 'email.suppressed': 'rejected', 'email.complained': 'complained', 'email.opened': null, 'email.clicked': null };
|
|
15
|
+
const cfStatuses = new Set(['delivered', 'deferred', 'bounced', 'failed', 'rejected', 'complained']);
|
|
16
|
+
|
|
17
|
+
function bounded(value, max = 512) {
|
|
18
|
+
if (typeof value !== 'string' || !value.length || value.length > max || value.trim() !== value || /[\x00-\x1f\x7f]/.test(value)) fail('invalid_event_identity');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function object(value) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid_event_schema');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function recipient(value) {
|
|
26
|
+
const parsed = parseAddress(value);
|
|
27
|
+
if (parsed.name || parsed.email !== value) fail('invalid_event_recipient');
|
|
28
|
+
// Preserve local-part case; lowering it can merge distinct mailbox identities.
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function epoch(value, now) {
|
|
32
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > now + 300_000) fail('invalid_event_timestamp');
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function date(value, now) {
|
|
36
|
+
if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(value)) fail('invalid_event_timestamp');
|
|
37
|
+
const day = new Date(`${value.slice(0, 10)}T00:00:00.000Z`);
|
|
38
|
+
if (!Number.isFinite(day.getTime()) || day.toISOString().slice(0, 10) !== value.slice(0, 10) ||
|
|
39
|
+
Number(value.slice(11, 13)) > 23 || Number(value.slice(14, 16)) > 59 || Number(value.slice(17, 19)) > 59) fail('invalid_event_timestamp');
|
|
40
|
+
return epoch(Date.parse(value), now);
|
|
41
|
+
}
|
|
42
|
+
async function fingerprint(bytes) {
|
|
43
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
44
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Called only on already authenticated, successfully parsed JSON arrays. Slice
|
|
48
|
+
// each original event, so batching/reordering does not alter an event fingerprint.
|
|
49
|
+
function eventSlices(text) {
|
|
50
|
+
const slices = [];
|
|
51
|
+
let depth = 0, quoted = false, escaped = false, start = 0;
|
|
52
|
+
for (let i = 0; i < text.length; i++) {
|
|
53
|
+
const char = text[i];
|
|
54
|
+
if (quoted) {
|
|
55
|
+
if (escaped) escaped = false;
|
|
56
|
+
else if (char === '\\') escaped = true;
|
|
57
|
+
else if (char === '"') quoted = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === '"') quoted = true;
|
|
61
|
+
else if (char === '[' || char === '{') {
|
|
62
|
+
if (depth === 1) start = i;
|
|
63
|
+
depth++;
|
|
64
|
+
} else if (char === ']' || char === '}') {
|
|
65
|
+
depth--;
|
|
66
|
+
if (depth === 1) slices.push(text.slice(start, i + 1));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return slices;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Only returned intake methods can reach these private normalizers. */
|
|
73
|
+
function sendgrid(event, now) {
|
|
74
|
+
object(event);
|
|
75
|
+
if (!Object.hasOwn(sgStatuses, event.event)) fail('unsupported_delivery_event');
|
|
76
|
+
if (!Number.isSafeInteger(event.timestamp) || event.timestamp < 0) fail('invalid_event_timestamp');
|
|
77
|
+
return {
|
|
78
|
+
eventId: bounded(event.sg_event_id), providerMessageId: bounded(event.sg_message_id, 1024),
|
|
79
|
+
type: event.event, status: sgStatuses[event.event], occurredAt: epoch(event.timestamp * 1000, now),
|
|
80
|
+
recipients: [recipient(event.email)],
|
|
81
|
+
deliveryId: event.rer_delivery_id === undefined ? null : bounded(event.rer_delivery_id),
|
|
82
|
+
product: event.rer_product === undefined ? null : bounded(event.rer_product),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resend(event, headers, now) {
|
|
86
|
+
object(event); object(event.data);
|
|
87
|
+
if (!Object.hasOwn(resendStatuses, event.type)) fail('unsupported_delivery_event');
|
|
88
|
+
if (!Array.isArray(event.data.to) || !event.data.to.length || event.data.to.length > MAX_EVENTS) fail('invalid_event_recipients');
|
|
89
|
+
const recipients = event.data.to.map(recipient);
|
|
90
|
+
if (new Set(recipients).size !== recipients.length) fail('invalid_event_recipients');
|
|
91
|
+
return {
|
|
92
|
+
eventId: bounded(headers.get('svix-id')), providerMessageId: bounded(event.data.email_id, 1024),
|
|
93
|
+
type: event.type, status: resendStatuses[event.type], occurredAt: date(event.created_at, now),
|
|
94
|
+
recipients, deliveryId: null, product: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function cloudflare(event, source, now) {
|
|
98
|
+
object(event); object(event.source); object(event.metadata); object(event.payload); object(event.payload.delivery);
|
|
99
|
+
const status = event.payload.delivery.status;
|
|
100
|
+
if (!cfStatuses.has(status) || event.type !== `cf.email.sending.message.${status}` ||
|
|
101
|
+
event.payload.terminal !== (status !== 'deferred') || event.metadata.eventSchemaVersion !== 1 ||
|
|
102
|
+
event.source.type !== 'email.sending') fail('invalid_event_schema');
|
|
103
|
+
if (event.source.zoneId !== source.zoneId || event.source.domain !== source.domain ||
|
|
104
|
+
event.metadata.accountId !== source.accountId || event.metadata.eventSubscriptionId !== source.eventSubscriptionId) fail('event_source_unauthorized');
|
|
105
|
+
return {
|
|
106
|
+
eventId: bounded(event.payload.eventId), providerMessageId: bounded(event.payload.messageId, 1024),
|
|
107
|
+
type: event.type, status, occurredAt: date(event.metadata.eventTimestamp, now),
|
|
108
|
+
recipients: [recipient(event.payload.recipient)], deliveryId: null, product: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createDeliveryEventIntake(options) {
|
|
113
|
+
object(options);
|
|
114
|
+
const { provider, key, storage, authorize, clock = Date.now, toleranceSeconds = 300 } = options;
|
|
115
|
+
const tenantId = bounded(options.tenantId, 256);
|
|
116
|
+
const eventNamespace = bounded(options.eventNamespace, 256);
|
|
117
|
+
if (!['sendgrid', 'resend', 'cloudflare', 'messaging'].includes(provider) || typeof authorize !== 'function' || typeof clock !== 'function') fail('invalid_event_config');
|
|
118
|
+
if (!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0 || toleranceSeconds > 3600) fail('invalid_event_config');
|
|
119
|
+
if (['sendgrid', 'resend'].includes(provider) && (typeof key !== 'string' || !key.length)) fail('event_key_required');
|
|
120
|
+
if (['cloudflare', 'messaging'].includes(provider) && key !== undefined) fail('unsupported_event_key');
|
|
121
|
+
if (options.store !== undefined && (storage !== undefined || !isD1EventStore(options.store))) fail('invalid_event_store');
|
|
122
|
+
const store = options.store ?? createDurableEventStore(storage);
|
|
123
|
+
let messagingSource;
|
|
124
|
+
if (provider === 'messaging') {
|
|
125
|
+
const source = closed(options.messagingSource, ['serviceOrigin', 'installId', 'jwks'], 'invalid_event_config');
|
|
126
|
+
if (options.mode !== undefined && !['production', 'development', 'test'].includes(options.mode)) fail('invalid_event_config');
|
|
127
|
+
messagingSource = { serviceOrigin: serviceOrigin(source.serviceOrigin, options.mode ?? 'production'),
|
|
128
|
+
installId: identity(source.installId, 256), jwks: pinnedKeys(source.jwks) };
|
|
129
|
+
}
|
|
130
|
+
const source = provider === 'cloudflare' ? { ...object(options.cloudflareSource) } : null;
|
|
131
|
+
if (source) {
|
|
132
|
+
for (const field of ['queue', 'zoneId', 'domain', 'accountId', 'eventSubscriptionId']) bounded(source[field]);
|
|
133
|
+
}
|
|
134
|
+
function now() {
|
|
135
|
+
let value;
|
|
136
|
+
try { value = clock(); } catch { fail('invalid_event_clock'); }
|
|
137
|
+
if (!Number.isSafeInteger(value) || value < 0) fail('invalid_event_clock');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
async function persist(records) {
|
|
141
|
+
// Complete schema/authorization of the whole batch before the first write.
|
|
142
|
+
for (const event of records) {
|
|
143
|
+
Object.freeze(event.recipients); if (event.detail) Object.freeze(event.detail); Object.freeze(event);
|
|
144
|
+
let permitted;
|
|
145
|
+
try { permitted = await authorize(event); } catch { fail('event_unauthorized'); }
|
|
146
|
+
if (permitted !== true) fail('event_unauthorized');
|
|
147
|
+
}
|
|
148
|
+
return store.commit(records);
|
|
149
|
+
}
|
|
150
|
+
async function record(event, bytes) {
|
|
151
|
+
return { ...event, provider, tenantId, eventNamespace, fingerprint: await fingerprint(bytes) };
|
|
152
|
+
}
|
|
153
|
+
if (provider === 'cloudflare') return Object.freeze({
|
|
154
|
+
async consumeQueue(batch) {
|
|
155
|
+
// This method is solely for the trusted Worker queue handler. Runtime
|
|
156
|
+
// objects are not an HTTP authentication protocol (see README).
|
|
157
|
+
if (!batch || batch.queue !== source.queue || !Array.isArray(batch.messages) ||
|
|
158
|
+
!batch.messages.length || batch.messages.length > MAX_EVENTS) fail('invalid_event_queue');
|
|
159
|
+
const timestamp = now();
|
|
160
|
+
const pending = [];
|
|
161
|
+
let total = 0;
|
|
162
|
+
for (const message of batch.messages) {
|
|
163
|
+
object(message);
|
|
164
|
+
let text, event;
|
|
165
|
+
try { text = JSON.stringify(message.body); event = JSON.parse(text); } catch { fail('invalid_event_schema'); }
|
|
166
|
+
const bytes = encoder.encode(text); total += bytes.length;
|
|
167
|
+
if (total > MAX_EVENT_BYTES) fail('event_body_too_large');
|
|
168
|
+
pending.push([cloudflare(event, source, timestamp), bytes]);
|
|
169
|
+
}
|
|
170
|
+
return persist(await Promise.all(pending.map(([event, bytes]) => record(event, bytes))));
|
|
171
|
+
// Caller ACKs only after resolution; throws leave the queue retryable.
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
async ingestWebhook(input) {
|
|
176
|
+
if (!(input?.rawBody instanceof Uint8Array) || input.rawBody.length > MAX_EVENT_BYTES || !(input.headers instanceof Headers)) fail('invalid_event_input');
|
|
177
|
+
const rawBody = new Uint8Array(input.rawBody);
|
|
178
|
+
const headers = new Headers(input.headers);
|
|
179
|
+
const timestamp = now();
|
|
180
|
+
if (provider === 'messaging') {
|
|
181
|
+
const events = await verifyEventJws(rawBody, messagingSource, timestamp);
|
|
182
|
+
if (!Array.isArray(events) || !events.length || events.length > MAX_EVENTS) fail('invalid_event_batch');
|
|
183
|
+
const normalized = events.map(event => messagingEvent(event, timestamp));
|
|
184
|
+
return persist(await Promise.all(normalized.map(event => record({
|
|
185
|
+
eventId: event.id, providerMessageId: event.messageId, deliveryId: event.deliveryId,
|
|
186
|
+
type: event.type, status: event.type === 'suppressed' ? 'rejected' : event.type,
|
|
187
|
+
occurredAt: Date.parse(event.occurredAt), recipients: [event.recipient], product: null, detail: event.detail,
|
|
188
|
+
}, encoder.encode(JSON.stringify(event))))));
|
|
189
|
+
}
|
|
190
|
+
if (!await verifyWebhook({ provider, rawBody, headers, key, now: timestamp, toleranceSeconds })) fail('event_signature_invalid');
|
|
191
|
+
let text, payload;
|
|
192
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(rawBody); payload = JSON.parse(text); } catch { fail('invalid_event_json'); }
|
|
193
|
+
if (provider === 'resend') return persist([await record(resend(payload, headers, timestamp), rawBody)]);
|
|
194
|
+
if (!Array.isArray(payload) || !payload.length || payload.length > MAX_EVENTS) fail('invalid_event_batch');
|
|
195
|
+
const normalized = payload.map(event => sendgrid(event, timestamp));
|
|
196
|
+
const slices = eventSlices(text);
|
|
197
|
+
if (slices.length !== normalized.length) fail('invalid_event_batch');
|
|
198
|
+
return persist(await Promise.all(normalized.map((event, index) => record(event, encoder.encode(slices[index])))));
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { fail, MailValidationError } from '../core/errors.mjs';
|
|
2
|
+
|
|
3
|
+
const terminal = new Set(['delivered', 'bounced', 'failed', 'rejected', 'complained']);
|
|
4
|
+
|
|
5
|
+
/** A real DurableObjectStorage transaction; no process-local deduplication. */
|
|
6
|
+
export function createDurableEventStore(storage) {
|
|
7
|
+
if (!storage || typeof storage.transaction !== 'function') fail('event_storage_required');
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
async commit(events) {
|
|
10
|
+
try {
|
|
11
|
+
return await storage.transaction(async tx => {
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const event of events) {
|
|
14
|
+
const namespace = [event.provider, event.tenantId, event.eventNamespace];
|
|
15
|
+
const receiptKey = await key('receipt', [...namespace, event.eventId]);
|
|
16
|
+
const prior = await tx.get(receiptKey);
|
|
17
|
+
if (prior) {
|
|
18
|
+
if (prior.fingerprint !== event.fingerprint) fail('event_identity_conflict');
|
|
19
|
+
results.push({ eventId: event.eventId, outcome: 'duplicate', recipients: [] });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const recipients = [];
|
|
23
|
+
for (const recipient of event.recipients) {
|
|
24
|
+
const stateKey = await key('state', [...namespace, event.providerMessageId, recipient]);
|
|
25
|
+
const previous = await tx.get(stateKey);
|
|
26
|
+
// Final delivery facts never become accepted/deferred. Newer terminal
|
|
27
|
+
// facts (e.g. a later complaint) may supersede earlier terminal facts.
|
|
28
|
+
const apply = event.status !== null && (!previous ||
|
|
29
|
+
(terminal.has(event.status) && !terminal.has(previous.status)) ||
|
|
30
|
+
(event.occurredAt > previous.occurredAt &&
|
|
31
|
+
(!terminal.has(previous.status) || terminal.has(event.status))));
|
|
32
|
+
const state = apply ? { status: event.status, occurredAt: event.occurredAt, eventId: event.eventId } : previous ?? null;
|
|
33
|
+
if (apply) await tx.put(stateKey, state);
|
|
34
|
+
recipients.push({ recipient, applied: apply, state });
|
|
35
|
+
}
|
|
36
|
+
await tx.put(receiptKey, event);
|
|
37
|
+
results.push({ eventId: event.eventId, outcome: 'stored', recipients });
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof MailValidationError && error.code === 'event_identity_conflict') throw error;
|
|
43
|
+
fail('event_storage_failed');
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function key(kind, tuple) {
|
|
50
|
+
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(tuple)));
|
|
51
|
+
return `mail-events:v1:${kind}:${Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { encodeBase64 } from '../core/validation.mjs';
|
|
2
|
+
|
|
3
|
+
// Inbound Parse also uses this verifier; delivery intake applies a smaller cap.
|
|
4
|
+
export const MAX_WEBHOOK_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
|
|
7
|
+
function base64(value, max) {
|
|
8
|
+
if (typeof value !== 'string' || !value.length || value.length > max ||
|
|
9
|
+
!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 === 1) throw new Error();
|
|
10
|
+
const bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
|
11
|
+
const canonical = encodeBase64(bytes);
|
|
12
|
+
if (value !== canonical && value !== canonical.replace(/=+$/, '')) throw new Error();
|
|
13
|
+
return bytes;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Strict DER INTEGERs; WebCrypto expects fixed-width unsigned r || s. */
|
|
17
|
+
function derToP1363(bytes) {
|
|
18
|
+
if (bytes.length < 8 || bytes.length > 72 || bytes[0] !== 0x30 || bytes[1] !== bytes.length - 2) throw new Error();
|
|
19
|
+
const result = new Uint8Array(64);
|
|
20
|
+
let offset = 2;
|
|
21
|
+
for (let part = 0; part < 2; part++) {
|
|
22
|
+
if (bytes[offset++] !== 2) throw new Error();
|
|
23
|
+
const length = bytes[offset++];
|
|
24
|
+
if (!length || length > 33 || offset + length > bytes.length) throw new Error();
|
|
25
|
+
let integer = bytes.subarray(offset, offset + length);
|
|
26
|
+
offset += length;
|
|
27
|
+
if (integer[0] & 0x80) throw new Error();
|
|
28
|
+
if (integer[0] === 0 && integer.length > 1) {
|
|
29
|
+
if (!(integer[1] & 0x80)) throw new Error();
|
|
30
|
+
integer = integer.subarray(1);
|
|
31
|
+
}
|
|
32
|
+
if (integer.length > 32 || integer.every(byte => byte === 0)) throw new Error();
|
|
33
|
+
result.set(integer, part * 32 + 32 - integer.length);
|
|
34
|
+
}
|
|
35
|
+
if (offset !== bytes.length) throw new Error();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function signedBytes(prefix, raw) {
|
|
40
|
+
const first = encoder.encode(prefix);
|
|
41
|
+
const bytes = new Uint8Array(first.length + raw.length);
|
|
42
|
+
bytes.set(first); bytes.set(raw, first.length);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function verifyWebhook(input) {
|
|
47
|
+
try {
|
|
48
|
+
const { provider, rawBody, headers, key, now, toleranceSeconds = 300 } = input;
|
|
49
|
+
if (!['sendgrid', 'resend'].includes(provider) || !(rawBody instanceof Uint8Array) ||
|
|
50
|
+
rawBody.length > MAX_WEBHOOK_BYTES || !(headers instanceof Headers) ||
|
|
51
|
+
!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(toleranceSeconds) ||
|
|
52
|
+
toleranceSeconds < 0 || toleranceSeconds > 3600) return false;
|
|
53
|
+
// Copy before the first await; callers cannot mutate the verified bytes mid-flight.
|
|
54
|
+
const raw = new Uint8Array(rawBody);
|
|
55
|
+
const timestamp = headers.get(provider === 'sendgrid' ? 'x-twilio-email-event-webhook-timestamp' : 'svix-timestamp');
|
|
56
|
+
if (!/^(0|[1-9][0-9]{0,12})$/.test(timestamp ?? '')) return false;
|
|
57
|
+
const milliseconds = Number(timestamp) * 1000;
|
|
58
|
+
if (!Number.isSafeInteger(milliseconds) || Math.abs(now - milliseconds) > toleranceSeconds * 1000) return false;
|
|
59
|
+
if (provider === 'sendgrid') {
|
|
60
|
+
const spki = base64(key, 256);
|
|
61
|
+
const signature = derToP1363(base64(headers.get('x-twilio-email-event-webhook-signature'), 128));
|
|
62
|
+
const publicKey = await crypto.subtle.importKey('spki', spki, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
|
|
63
|
+
// Reject trailing bytes, alternate/noncanonical ASN.1, and wrong algorithms.
|
|
64
|
+
const exported = new Uint8Array(await crypto.subtle.exportKey('spki', publicKey));
|
|
65
|
+
if (encodeBase64(spki) !== encodeBase64(exported)) return false;
|
|
66
|
+
return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, signature, signedBytes(timestamp, raw));
|
|
67
|
+
}
|
|
68
|
+
const id = headers.get('svix-id');
|
|
69
|
+
const signatures = headers.get('svix-signature');
|
|
70
|
+
if (!/^[A-Za-z0-9_-]{1,512}$/.test(id ?? '') || typeof key !== 'string' || !key.startsWith('whsec_') ||
|
|
71
|
+
!signatures || signatures.length > 4096) return false;
|
|
72
|
+
const secret = base64(key.slice(6), 128);
|
|
73
|
+
if (secret.length < 16 || secret.length > 64) return false;
|
|
74
|
+
const tokens = signatures.split(' ');
|
|
75
|
+
if (tokens.length > 16) return false;
|
|
76
|
+
// Validate the entire header, including entries after a valid rotation key.
|
|
77
|
+
const candidates = tokens.map(token => {
|
|
78
|
+
const match = /^(v[1-9][0-9]*),([A-Za-z0-9+/]+={0,2})$/.exec(token);
|
|
79
|
+
if (!match) throw new Error();
|
|
80
|
+
const bytes = base64(match[2], 256);
|
|
81
|
+
if (match[1] === 'v1' && bytes.length !== 32) throw new Error();
|
|
82
|
+
return { version: match[1], bytes };
|
|
83
|
+
});
|
|
84
|
+
const hmac = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
85
|
+
const signed = signedBytes(`${id}.${timestamp}.`, raw);
|
|
86
|
+
let verified = false;
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
if (candidate.version === 'v1') verified = (await crypto.subtle.verify('HMAC', hmac, candidate.bytes, signed)) || verified;
|
|
89
|
+
}
|
|
90
|
+
return verified;
|
|
91
|
+
} catch { return false; }
|
|
92
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Inbound normalization
|
|
2
|
+
|
|
3
|
+
Import `normalizeInbound` from `modules/mail/src/inbound/index.mjs` (also exposed
|
|
4
|
+
by the assembled mail facade). It returns the frozen `InboundMessage` contract.
|
|
5
|
+
These are transport parsers, not authentication or HTTP handlers. No route or
|
|
6
|
+
persistent storage is installed. HTML, text, filenames and sender claims remain
|
|
7
|
+
untrusted content; the consuming application owns rendering sanitization,
|
|
8
|
+
authorization, deduplication, durable quarantine and sender-authentication policy.
|
|
9
|
+
|
|
10
|
+
## Trust boundary and provider inputs
|
|
11
|
+
|
|
12
|
+
- Cloudflare: call only from the Worker `email(message, env, ctx)` event, passing
|
|
13
|
+
`{provider:'cloudflare', message}`. The real `raw` byte stream is parsed; its
|
|
14
|
+
actual length must equal `rawSize`. `from` and `to` are the SMTP envelope values.
|
|
15
|
+
Headers and threading come from the original MIME stream, avoiding synthetic
|
|
16
|
+
JSON conversion and duplicate-header loss through `Headers`.
|
|
17
|
+
- SendGrid: bound and retain the original HTTP multipart bytes, authenticate
|
|
18
|
+
those exact bytes with the configured Inbound Parse signature key, then pass
|
|
19
|
+
`{provider:'sendgrid', rawBody:Uint8Array, contentType}`. Never call parsing or
|
|
20
|
+
store/hydrate a message before successful verification. The normalizer accepts
|
|
21
|
+
both parsed fields/binary attachment parts and the provider's raw `email`
|
|
22
|
+
field. Duplicate fields, mismatched attachment counts/metadata, malformed
|
|
23
|
+
boundaries and invalid transfer encoding reject the whole message.
|
|
24
|
+
- Resend: verify the exact Svix webhook bytes, validate `type:'email.received'`,
|
|
25
|
+
and extract the authenticated event's `data.email_id`. Only then retrieve
|
|
26
|
+
`GET https://api.resend.com/emails/receiving/{email_id}?html_format=cid` using
|
|
27
|
+
the fixed API origin, an authorized server credential, a bounded response
|
|
28
|
+
reader, a deadline and `redirect:'manual'`. Pass the response object as `email`
|
|
29
|
+
and the original authenticated ID as **`signedEmailId`**. The normalizer
|
|
30
|
+
requires `email.id === signedEmailId` before any attachment callback. Do not
|
|
31
|
+
fill `signedEmailId` from the hydrated response. Requesting `html_format=cid`
|
|
32
|
+
preserves original inline image references across providers; the normalizer
|
|
33
|
+
preserves the returned HTML and does not rewrite provider data-URI HTML.
|
|
34
|
+
|
|
35
|
+
A successful webhook signature authenticates the provider's transport, not the
|
|
36
|
+
person named in the mail's From header. Supplying a `signedEmailId` value does
|
|
37
|
+
not perform cryptographic verification; the verified caller is the trust boundary.
|
|
38
|
+
The events unit owns `verifyWebhook`. This unit neither fabricates verification
|
|
39
|
+
nor retries a failed transport.
|
|
40
|
+
|
|
41
|
+
## Resend attachment hydration
|
|
42
|
+
|
|
43
|
+
Resend's received-email response contains attachment metadata, not file bytes.
|
|
44
|
+
Every entry requires `id`, `filename`, `content_type`, a nonnegative integer
|
|
45
|
+
`size`, and optional `content_disposition` / `content_id`. Supply an asynchronous
|
|
46
|
+
`fetchAttachment(attachment) -> Uint8Array` on the input when attachments exist.
|
|
47
|
+
The callback receives a frozen object containing only
|
|
48
|
+
`{emailId,id,filename,content_type,content_disposition,content_id,size}`. Provider
|
|
49
|
+
IDs are bounded safe path segments; URLs and arbitrary response fields are not
|
|
50
|
+
passed through. All message metadata and declared aggregate limits are checked
|
|
51
|
+
before the first callback. Missing hydration, duplicate IDs, oversized bytes,
|
|
52
|
+
or actual length differing from `size` fail the entire result. Callbacks must
|
|
53
|
+
be read-only: an eventual failure cannot undo a caller's storage side effects.
|
|
54
|
+
|
|
55
|
+
A real safe implementation is included:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { normalizeInbound, createResendAttachmentFetcher } from './index.mjs';
|
|
59
|
+
// After verified event validation and identity-bound email retrieval:
|
|
60
|
+
const fetchAttachment = createResendAttachmentFetcher({
|
|
61
|
+
apiKey, maxAttachmentBytes: limits.maxAttachmentBytes,
|
|
62
|
+
});
|
|
63
|
+
const message = await normalizeInbound({
|
|
64
|
+
provider: 'resend', signedEmailId, email, fetchAttachment,
|
|
65
|
+
}, limits);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`createResendAttachmentFetcher({apiKey,maxAttachmentBytes}, {fetch?,clock?})`
|
|
69
|
+
uses the fixed Resend attachment API and verifies returned ID, filename, MIME
|
|
70
|
+
metadata and size against the received-email entry. It permits only the documented
|
|
71
|
+
`https://inbound-cdn.resend.com/{emailId}/attachments/{id}` download location,
|
|
72
|
+
requires an unexpired URL, refuses redirects on both requests, and sends the
|
|
73
|
+
API credential only to `api.resend.com`. The download request has no authorization
|
|
74
|
+
headers. API metadata is bounded to 64 KiB; streamed content is bounded by the
|
|
75
|
+
expected size and configured attachment limit. One 15-second abort deadline
|
|
76
|
+
covers both requests and bodies. Errors are fixed and redacted. There is no
|
|
77
|
+
configurable arbitrary download origin. A provider CDN contract change fails
|
|
78
|
+
closed and requires an explicit source update.
|
|
79
|
+
|
|
80
|
+
A custom callback must enforce the same identity/metadata, origin, redirect,
|
|
81
|
+
timeout and streaming checks. A callback is trusted application code, not
|
|
82
|
+
untrusted webhook input. Neither helper retrieves email metadata before the
|
|
83
|
+
caller has verified the webhook.
|
|
84
|
+
|
|
85
|
+
## Limits and normalization details
|
|
86
|
+
|
|
87
|
+
All five limit values must be positive safe integers. Exact limits succeed.
|
|
88
|
+
`maxRawBytes` measures actual Cloudflare stream bytes or SendGrid multipart bytes,
|
|
89
|
+
and checks Cloudflare's claimed size too. For Resend's object input it measures
|
|
90
|
+
the UTF-8 JSON serialization of the hydrated response; this is not a claim to
|
|
91
|
+
have inspected the original SMTP message or the original HTTP whitespace.
|
|
92
|
+
The caller must also bound the actual HTTP response stream during hydration.
|
|
93
|
+
Downloaded files have separate individual and aggregate actual-byte limits.
|
|
94
|
+
|
|
95
|
+
`maxBodyBytes` is aggregate text plus HTML, checked against both decoded source
|
|
96
|
+
bytes and the resulting UTF-8 bytes (the larger size for each body part).
|
|
97
|
+
`maxAttachments`, `maxAttachmentBytes`, and `maxTotalAttachmentBytes` apply to
|
|
98
|
+
all decoded files, including inline and unnamed MIME parts. Both claimed Resend
|
|
99
|
+
sizes and actual hydrated bytes are checked. Results are returned only after
|
|
100
|
+
all parsing and hydration succeeds; errors never contain partial messages.
|
|
101
|
+
Oversized Cloudflare/download streams are cancelled.
|
|
102
|
+
|
|
103
|
+
MIME supports nested multipart alternatives/mixed/related, folded headers,
|
|
104
|
+
RFC 2047 encoded words, RFC 2231 extended/continued filenames, base64,
|
|
105
|
+
quoted-printable, 7bit/8bit/binary and Web `TextDecoder` character sets. Invalid
|
|
106
|
+
encodings, duplicate singleton headers, incomplete multiparts and nesting over
|
|
107
|
+
30 levels fail closed. Multiple body parts of the same media type are joined
|
|
108
|
+
with a newline, which also counts toward the output body limit. Unsupported
|
|
109
|
+
media types are retained as attachments instead of silently discarded.
|
|
110
|
+
|
|
111
|
+
Output addresses are mailbox addresses (display names are parsed but not
|
|
112
|
+
returned). Cloudflare uses its event envelope; SendGrid and Resend use their
|
|
113
|
+
provided sender/recipient fields. Empty subjects are preserved; absent optional
|
|
114
|
+
thread/body fields use null or empty arrays. Message-ID and ordered References
|
|
115
|
+
retain their angle-bracket values. Content-IDs are normalized without angle
|
|
116
|
+
brackets; an absent content ID is null. Unnamed MIME files retain filename `''`.
|
|
117
|
+
No caller-owned input objects or attachment buffers are mutated.
|
|
118
|
+
|
|
119
|
+
## Checks and references
|
|
120
|
+
|
|
121
|
+
From the Base worktree:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
node --test modules/mail/tests/inbound/normalization.test.mjs modules/mail/tests/inbound/boundaries.test.mjs modules/mail/tests/inbound/hydration.test.mjs
|
|
125
|
+
node --test modules/mail/tests/inbound/workerd.test.mjs
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The latter uses actual workerd through the module's Miniflare harness without
|
|
129
|
+
Node compatibility, with three provider fixtures and the safe hydration protocol.
|
|
130
|
+
Fixtures and provider responses live only under `./tests/inbound`. These checks do
|
|
131
|
+
not prove hosted mail receipt; owner-authorized domain, provider configuration,
|
|
132
|
+
signed routing and send/receive correlation evidence remain separate acceptance.
|
|
133
|
+
|
|
134
|
+
Provider schemas consulted: [Cloudflare email event](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/),
|
|
135
|
+
[SendGrid Inbound Parse fields](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook),
|
|
136
|
+
[Resend received email](https://resend.com/docs/api-reference/emails/retrieve-received-email),
|
|
137
|
+
and [Resend attachment metadata/download](https://resend.com/docs/api-reference/emails/retrieve-received-email-attachment).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { encodeBase64 } from '../core/validation.mjs';
|
|
3
|
+
export const utf8 = new TextEncoder();
|
|
4
|
+
export function record(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail('invalid_inbound');
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function limitsFor(value) {
|
|
9
|
+
record(value);
|
|
10
|
+
for (const key of ['maxRawBytes', 'maxBodyBytes', 'maxAttachments', 'maxAttachmentBytes', 'maxTotalAttachmentBytes']) {
|
|
11
|
+
if (!Number.isSafeInteger(value[key]) || value[key] <= 0) fail('invalid_inbound_limits');
|
|
12
|
+
}
|
|
13
|
+
return { ...value };
|
|
14
|
+
}
|
|
15
|
+
export function bounded(size, limit, code) { if (size > limit) fail(code); }
|
|
16
|
+
export function binary(bytes) {
|
|
17
|
+
let value = '';
|
|
18
|
+
for (let i = 0; i < bytes.length; i += 8192) value += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export const unbinary = value => Uint8Array.from(value, c => c.charCodeAt(0));
|
|
22
|
+
export function decode(bytes, charset = 'utf-8') {
|
|
23
|
+
try { return new TextDecoder(charset, { fatal: true }).decode(bytes); } catch { fail('invalid_inbound_charset'); }
|
|
24
|
+
}
|
|
25
|
+
export function base64(value) {
|
|
26
|
+
const compact = value.replace(/[\r\n\t ]/g, '');
|
|
27
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(compact)) fail('invalid_inbound_base64');
|
|
28
|
+
let bytes;
|
|
29
|
+
try { bytes = unbinary(atob(compact)); } catch { fail('invalid_inbound_base64'); }
|
|
30
|
+
if (encodeBase64(bytes) !== compact) fail('invalid_inbound_base64');
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
export function quotedPrintable(value, word = false) {
|
|
34
|
+
const source = word ? value.replace(/_/g, ' ') : value;
|
|
35
|
+
let result = '';
|
|
36
|
+
for (let i = 0; i < source.length; i++) {
|
|
37
|
+
if (source[i] !== '=') {
|
|
38
|
+
if (/[^\x20-\x7e\t\r\n]/.test(source[i])) fail('invalid_inbound_encoding');
|
|
39
|
+
result += source[i]; continue;
|
|
40
|
+
}
|
|
41
|
+
const next = source.slice(i + 1, i + 3);
|
|
42
|
+
if (!word && next === '\r\n') { i += 2; continue; }
|
|
43
|
+
if (!word && source[i + 1] === '\n') { i++; continue; }
|
|
44
|
+
if (!/^[a-f0-9]{2}$/i.test(next)) fail('invalid_inbound_encoding');
|
|
45
|
+
result += String.fromCharCode(parseInt(next, 16)); i += 2;
|
|
46
|
+
}
|
|
47
|
+
return unbinary(result);
|
|
48
|
+
}
|
|
49
|
+
export function transfer(bytes, encoding = '7bit') {
|
|
50
|
+
switch (encoding.toLowerCase()) {
|
|
51
|
+
case 'base64': return base64(binary(bytes));
|
|
52
|
+
case 'quoted-printable': return quotedPrintable(binary(bytes));
|
|
53
|
+
case '7bit': if (bytes.some(byte => byte > 127)) fail('invalid_inbound_encoding'); return bytes;
|
|
54
|
+
case '8bit': case 'binary': return bytes;
|
|
55
|
+
default: fail('invalid_inbound_encoding');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function readStream(stream, max) {
|
|
59
|
+
if (!stream || typeof stream.getReader !== 'function') fail('invalid_inbound_stream');
|
|
60
|
+
const reader = stream.getReader(); const chunks = []; let size = 0;
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
if (!(value instanceof Uint8Array)) fail('invalid_inbound_stream');
|
|
66
|
+
size += value.byteLength; bounded(size, max, 'inbound_raw_limit'); chunks.push(value.slice());
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
try { await reader.cancel(); } catch { /* Preserve the original redacted failure. */ }
|
|
70
|
+
throw error;
|
|
71
|
+
} finally { reader.releaseLock(); }
|
|
72
|
+
const bytes = new Uint8Array(size); let offset = 0;
|
|
73
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
|
|
74
|
+
return bytes;
|
|
75
|
+
}
|
|
76
|
+
export function bodyBudget(limits) {
|
|
77
|
+
let bodySize = 0, attachmentSize = 0, attachmentCount = 0;
|
|
78
|
+
return {
|
|
79
|
+
body(text, originalSize = 0) {
|
|
80
|
+
bodySize += Math.max(originalSize, utf8.encode(text).length);
|
|
81
|
+
bounded(bodySize, limits.maxBodyBytes, 'inbound_body_limit');
|
|
82
|
+
},
|
|
83
|
+
attachment(bytes) {
|
|
84
|
+
if (!(bytes instanceof Uint8Array)) fail('invalid_inbound_attachment');
|
|
85
|
+
bounded(++attachmentCount, limits.maxAttachments, 'inbound_attachment_count');
|
|
86
|
+
bounded(bytes.length, limits.maxAttachmentBytes, 'inbound_attachment_limit');
|
|
87
|
+
attachmentSize += bytes.length;
|
|
88
|
+
bounded(attachmentSize, limits.maxTotalAttachmentBytes, 'inbound_attachments_limit');
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|