@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,120 @@
|
|
|
1
|
+
# Resend outbound adapter
|
|
2
|
+
|
|
3
|
+
`sendResend(config, message, dependencies = {})` exports the portable, single
|
|
4
|
+
submission adapter from `index.mjs`. Use the mail facade for ordinary sends and
|
|
5
|
+
unsubscribe assembly. Direct calls validate configuration and the message through
|
|
6
|
+
the real core helpers before any request. No SDK, Node API or build is required.
|
|
7
|
+
|
|
8
|
+
The adapter submits JSON to `https://api.resend.com/emails` using `POST` and
|
|
9
|
+
`Authorization: Bearer <apiKey>`. Envelope display names, `to`, `cc`, `bcc`, subject,
|
|
10
|
+
text/HTML, `reply_to` and validated custom/thread headers are preserved. Names with
|
|
11
|
+
mailbox delimiters are quoted. Omitted text allows Resend's HTML-to-text generation;
|
|
12
|
+
an explicit empty text string disables that generation. At most 50 `to` recipients
|
|
13
|
+
are accepted. [Send API](https://resend.com/docs/api-reference/emails/send-email)
|
|
14
|
+
|
|
15
|
+
Attachments carry the original filename, Base64-encoded bytes and `content_type`.
|
|
16
|
+
An inline attachment requires `contentId`; this becomes `content_id`. With no
|
|
17
|
+
explicit disposition, a supplied content ID selects inline delivery. Without an
|
|
18
|
+
ID the attachment is ordinary. Explicit `disposition: 'attachment'` together with
|
|
19
|
+
an ID is rejected as `unsupported_resend_attachment_disposition`: Resend has no
|
|
20
|
+
independent send disposition field. No remote attachment URLs are fetched.
|
|
21
|
+
[Wire schema](https://github.com/resend/resend-openapi/blob/main/resend.yaml)
|
|
22
|
+
and [inline images](https://resend.com/docs/dashboard/emails/embed-inline-images)
|
|
23
|
+
|
|
24
|
+
## Outbound size budget
|
|
25
|
+
|
|
26
|
+
Resend documents a **40 MB total email limit, including attachments after Base64
|
|
27
|
+
encoding**. This adapter treats MB as decimal (40,000,000 bytes) and enforces a
|
|
28
|
+
conservative local budget before making any request.
|
|
29
|
+
[Provider attachment limitations](https://resend.com/docs/dashboard/emails/attachments#attachment-limitations)
|
|
30
|
+
|
|
31
|
+
- The projected email starts with a 1,000,000-byte reserve for provider-added
|
|
32
|
+
envelope, MIME boundaries, headers and signatures. Each text/HTML or attachment
|
|
33
|
+
part adds 2,048 bytes. Metadata (sender, all `to`/`cc`/`bcc`, reply address,
|
|
34
|
+
subject, header names/values, filenames, content types and content IDs) costs
|
|
35
|
+
six times its UTF-8 byte length plus 128 bytes per string, covering encoding and
|
|
36
|
+
folding. An HTML-only request also reserves an equivalent generated text part.
|
|
37
|
+
- Each attachment costs `4 * ceil(rawByteLength / 3)` Base64 bytes, plus two bytes
|
|
38
|
+
per 76-character MIME line. The aggregate includes every attachment, including
|
|
39
|
+
inline and empty attachments; there is no separate per-file allowance.
|
|
40
|
+
- Text and HTML cost the larger of folded Base64 and conservative quoted-printable
|
|
41
|
+
encoding. The latter counts printable ASCII other than `=` as one byte, CR/LF
|
|
42
|
+
as two each, and other UTF-8 bytes as three; all spaces/tabs are counted as
|
|
43
|
+
encoded. It reserves three bytes per 73 encoded bytes for soft line breaks.
|
|
44
|
+
- The projected JSON request must fit 39,000,000 UTF-8 bytes. It counts JSON
|
|
45
|
+
escapes (including lone surrogates), Base64 padding, 1,024 bytes of fixed
|
|
46
|
+
syntax, 64 extra bytes per metadata string and 128 per attachment. After
|
|
47
|
+
serialization, the actual request is independently checked against the same
|
|
48
|
+
39,000,000-byte ceiling before fetch.
|
|
49
|
+
|
|
50
|
+
The checks are incremental and do not serialize metadata, allocate UTF-8 copies,
|
|
51
|
+
map recipient/attachment arrays or read attachment content to estimate size. All
|
|
52
|
+
attachments pass the aggregate check before the first Base64 encoding. The normal
|
|
53
|
+
facade still validates and snapshots the caller's input as documented; these
|
|
54
|
+
limits bound outbound serialization, not caller-owned input allocation.
|
|
55
|
+
|
|
56
|
+
Exceeding either budget throws the public redacted `MailValidationError` with
|
|
57
|
+
code `resend_message_too_large`, from both direct adapter and facade calls. No
|
|
58
|
+
fetch, outbox write, retry, truncation or alternate send occurs. This is invalid
|
|
59
|
+
input, not an uncertain submission. Bounds are inclusive; additional metadata or
|
|
60
|
+
a second alternative reduces the permitted body/attachment size. These local
|
|
61
|
+
estimates deliberately reject some requests below the provider maximum; the
|
|
62
|
+
provider remains responsible for validating its final generated MIME message.
|
|
63
|
+
|
|
64
|
+
SendGrid-specific `customArgs`, `asmGroupId` and `subscriptionTracking` are
|
|
65
|
+
unsupported and rejected. A remaining `unsubscribe` property is rejected with
|
|
66
|
+
`unsubscribe_requires_facade`; the facade must first assemble List-Unsubscribe
|
|
67
|
+
headers and remove the request property. The adapter also refuses custom email
|
|
68
|
+
headers named `Idempotency-Key` or `Resend-Idempotency-Key`, avoiding a second source
|
|
69
|
+
of correlation/suppression policy. The caller's `deliveryId` is never inserted as
|
|
70
|
+
a provider receipt.
|
|
71
|
+
|
|
72
|
+
## Idempotency and outcomes
|
|
73
|
+
|
|
74
|
+
`deliveryId` supplies the exact HTTP `Idempotency-Key`. This adapter accepts 1–256
|
|
75
|
+
printable ASCII characters with no leading/trailing whitespace, so Fetch cannot
|
|
76
|
+
silently alter the key. It does not hash or truncate unsupported keys. Applications
|
|
77
|
+
must namespace their keys and retain the immutable payload for a send command.
|
|
78
|
+
Resend retains keys for 24 hours: repeating the same request in that window yields
|
|
79
|
+
the original response without another send. After expiry, reuse can send again;
|
|
80
|
+
this is not indefinite duplicate suppression. A different payload under the same
|
|
81
|
+
key produces HTTP 409 `invalid_idempotent_request`; an unfinished concurrent request
|
|
82
|
+
produces 409 `concurrent_idempotent_requests`.
|
|
83
|
+
[Provider idempotency contract](https://resend.com/docs/dashboard/emails/idempotency-keys)
|
|
84
|
+
|
|
85
|
+
- HTTP 200 with a nonempty, control-free, bounded JSON `id` returns `accepted` and
|
|
86
|
+
that actual ID. Missing/malformed IDs, malformed/oversized JSON, response stream
|
|
87
|
+
failure and unexpected success statuses return `uncertain` with a null ID.
|
|
88
|
+
- 429 returns `rate_limited`; 408, redirects, server errors and transport failures
|
|
89
|
+
return `uncertain`. Other ordinary 4xx statuses return `rejected`.
|
|
90
|
+
- A recognized payload conflict returns `rejected`/`idempotency_conflict` for this
|
|
91
|
+
attempt. It does not establish whether the original request was accepted.
|
|
92
|
+
A concurrent conflict returns `uncertain`/`idempotency_in_progress`. Unknown or
|
|
93
|
+
malformed 409 responses are conservatively `uncertain`.
|
|
94
|
+
|
|
95
|
+
There are no retries or alternate providers/outboxes. Core transport forces manual
|
|
96
|
+
redirects and a 15-second request timeout. Structured response reads additionally
|
|
97
|
+
stop at 64 KiB or 15 seconds and cancel unfinished streams. Diagnostics are fixed
|
|
98
|
+
codes; response/error bodies and credentials are never returned or logged. HTTP
|
|
99
|
+
acceptance is not delivery, inbox receipt, or proof of delivered header/DKIM
|
|
100
|
+
coverage. Applications own durable policy and reconciliation.
|
|
101
|
+
|
|
102
|
+
## Verification
|
|
103
|
+
|
|
104
|
+
Run from the Base root:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
node --test modules/mail/tests/resend/*.test.mjs
|
|
108
|
+
node --check modules/mail/src/adapters/resend/index.mjs
|
|
109
|
+
node --check modules/mail/src/adapters/resend/serialize.mjs
|
|
110
|
+
node --check modules/mail/src/adapters/resend/response.mjs
|
|
111
|
+
node --check modules/mail/src/adapters/resend/size.mjs
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Fixtures live only in `modules/mail/tests/resend`; they inspect real Web Request
|
|
115
|
+
bytes and Response streams. Size boundaries run in the adapter unit suite;
|
|
116
|
+
`tests/runtime/workerd.test.mjs` checks a binary attachment through the facade
|
|
117
|
+
and adapters, and `tests/assembly/workerd.test.mjs` checks the doctor sender chain.
|
|
118
|
+
These checks establish local protocol behavior, not live-provider delivery.
|
|
119
|
+
No provider credentials or live mail are used by these unit tests. Official
|
|
120
|
+
references were inspected on 6 October 2026.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { validateConfig } from '../../core/validation.mjs';
|
|
3
|
+
import { requestProvider } from '../../core/transport.mjs';
|
|
4
|
+
import { serializeResendMessage } from './serialize.mjs';
|
|
5
|
+
import { resendResult } from './response.mjs';
|
|
6
|
+
|
|
7
|
+
/** One submission. Acceptance is not mailbox delivery; callers own reconciliation. */
|
|
8
|
+
export async function sendResend(config, message, dependencies = {}) {
|
|
9
|
+
validateConfig(config, dependencies);
|
|
10
|
+
if (config.provider !== 'resend') fail('invalid_provider');
|
|
11
|
+
const body = serializeResendMessage(message);
|
|
12
|
+
const response = await requestProvider('https://api.resend.com/emails', {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
'Idempotency-Key': message.deliveryId,
|
|
18
|
+
},
|
|
19
|
+
body,
|
|
20
|
+
}, dependencies);
|
|
21
|
+
return resendResult(response);
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { classifyHttpStatus, safeProviderMessageId, sendResult } from '../../core/transport.mjs';
|
|
2
|
+
|
|
3
|
+
// Only bounded structured responses are inspected. Body text never becomes diagnostics.
|
|
4
|
+
async function readObject(response) {
|
|
5
|
+
let reader;
|
|
6
|
+
let timer;
|
|
7
|
+
try {
|
|
8
|
+
reader = response.body?.getReader();
|
|
9
|
+
if (!reader) return null;
|
|
10
|
+
const expired = new Promise(resolve => { timer = setTimeout(() => resolve(null), 15_000); });
|
|
11
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
12
|
+
let text = '';
|
|
13
|
+
let size = 0;
|
|
14
|
+
while (true) {
|
|
15
|
+
const chunk = await Promise.race([reader.read(), expired]);
|
|
16
|
+
if (!chunk) return null;
|
|
17
|
+
if (chunk.done) break;
|
|
18
|
+
size += chunk.value.byteLength;
|
|
19
|
+
if (size > 65_536) return null;
|
|
20
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
21
|
+
}
|
|
22
|
+
const data = JSON.parse(text + decoder.decode());
|
|
23
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
finally {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
// Cancellation itself can fail or stall on a broken stream; never wait for it.
|
|
28
|
+
if (reader) void reader.cancel().catch(() => {});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function resendResult(response) {
|
|
33
|
+
if (!response) return sendResult('resend', 'uncertain', null, 'transport_error');
|
|
34
|
+
if (response.status === 409) {
|
|
35
|
+
const data = await readObject(response);
|
|
36
|
+
if (data?.name === 'invalid_idempotent_request') return sendResult('resend', 'rejected', null, 'idempotency_conflict');
|
|
37
|
+
if (data?.name === 'concurrent_idempotent_requests') return sendResult('resend', 'uncertain', null, 'idempotency_in_progress');
|
|
38
|
+
return sendResult('resend', 'uncertain', null, 'idempotency_unknown_conflict');
|
|
39
|
+
}
|
|
40
|
+
const status = classifyHttpStatus(response.status);
|
|
41
|
+
if (status) {
|
|
42
|
+
if (response.body) void response.body.cancel().catch(() => {});
|
|
43
|
+
return sendResult('resend', status, null, status === 'rate_limited' ? 'rate_limited' : status === 'rejected' ? 'provider_rejected' : 'unexpected_response');
|
|
44
|
+
}
|
|
45
|
+
// POST /emails documents HTTP 200 with an id, not bare HTTP success.
|
|
46
|
+
if (response.status !== 200) {
|
|
47
|
+
if (response.body) void response.body.cancel().catch(() => {});
|
|
48
|
+
return sendResult('resend', 'uncertain', null, 'unexpected_response');
|
|
49
|
+
}
|
|
50
|
+
const data = await readObject(response);
|
|
51
|
+
const id = safeProviderMessageId(data?.id);
|
|
52
|
+
if (!id || id !== data.id || 'error' in data || 'name' in data) return sendResult('resend', 'uncertain', null, 'invalid_provider_response');
|
|
53
|
+
return sendResult('resend', 'accepted', id);
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { encodeBase64, parseAddress, validateMessage } from '../../core/validation.mjs';
|
|
3
|
+
import { assertResendMessageSize, assertResendRequestSize } from './size.mjs';
|
|
4
|
+
|
|
5
|
+
function mailbox(value) {
|
|
6
|
+
const { email, name } = parseAddress(value);
|
|
7
|
+
// Quote names containing mailbox delimiters so one address remains one address.
|
|
8
|
+
return name ? `${/[(),:;@\[\]]/.test(name) ? `"${name}"` : name} <${email}>` : email;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function serializeResendMessage(message) {
|
|
12
|
+
if (message && Object.hasOwn(message, 'unsubscribe')) fail('unsubscribe_requires_facade');
|
|
13
|
+
validateMessage(message, 'resend');
|
|
14
|
+
// Preserve the exact key across Fetch's header normalization. No hashing/truncation.
|
|
15
|
+
if (!/^[\x20-\x7e]{1,256}$/.test(message.deliveryId) || message.deliveryId.trim() !== message.deliveryId) fail('invalid_resend_idempotency_key');
|
|
16
|
+
if (message.to.length > 50) fail('resend_recipient_limit');
|
|
17
|
+
assertResendMessageSize(message);
|
|
18
|
+
for (const name in message.headers) {
|
|
19
|
+
if (!Object.hasOwn(message.headers, name)) continue;
|
|
20
|
+
if (['idempotency-key', 'resend-idempotency-key'].includes(name.toLowerCase())) fail('reserved_resend_header');
|
|
21
|
+
}
|
|
22
|
+
const attachments = message.attachments?.map(attachment => {
|
|
23
|
+
// Resend derives inline disposition from content_id; no independent disposition exists.
|
|
24
|
+
if (attachment.disposition === 'attachment' && attachment.contentId !== undefined) fail('unsupported_resend_attachment_disposition');
|
|
25
|
+
return {
|
|
26
|
+
filename: attachment.filename,
|
|
27
|
+
content: encodeBase64(attachment.content),
|
|
28
|
+
content_type: attachment.contentType,
|
|
29
|
+
...(attachment.contentId === undefined ? {} : { content_id: attachment.contentId }),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const body = JSON.stringify({
|
|
33
|
+
from: mailbox(message.from),
|
|
34
|
+
to: message.to.map(mailbox),
|
|
35
|
+
subject: message.subject,
|
|
36
|
+
...(message.text === undefined ? {} : { text: message.text }),
|
|
37
|
+
...(message.html === undefined ? {} : { html: message.html }),
|
|
38
|
+
...(message.cc === undefined ? {} : { cc: message.cc.map(mailbox) }),
|
|
39
|
+
...(message.bcc === undefined ? {} : { bcc: message.bcc.map(mailbox) }),
|
|
40
|
+
...(message.replyTo === undefined ? {} : { reply_to: mailbox(message.replyTo) }),
|
|
41
|
+
...(message.headers === undefined ? {} : { headers: message.headers }),
|
|
42
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
43
|
+
});
|
|
44
|
+
assertResendRequestSize(body);
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
|
|
3
|
+
// Resend documents 40 MB including Base64 attachments. Use decimal MB and leave
|
|
4
|
+
// 1 MB for provider-added MIME/envelope/signature data; see the adapter README.
|
|
5
|
+
export const MAX_RESEND_EMAIL_BYTES = 40_000_000;
|
|
6
|
+
export const MAX_RESEND_REQUEST_BYTES = 39_000_000;
|
|
7
|
+
const ENVELOPE_RESERVE = MAX_RESEND_EMAIL_BYTES - MAX_RESEND_REQUEST_BYTES;
|
|
8
|
+
const PART_RESERVE = 2048;
|
|
9
|
+
const tooLarge = () => fail('resend_message_too_large');
|
|
10
|
+
const base64Size = bytes => 4 * Math.ceil(bytes / 3);
|
|
11
|
+
const foldedBase64Size = bytes => {
|
|
12
|
+
const encoded = base64Size(bytes);
|
|
13
|
+
return encoded + 2 * Math.ceil(encoded / 76);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Count without TextEncoder/JSON copies, including JSON escapes and lone UTF-16
|
|
17
|
+
// surrogates. Stop once even the source string exceeds the remaining budget.
|
|
18
|
+
function stringSize(value, limit) {
|
|
19
|
+
if (value.length > limit) tooLarge();
|
|
20
|
+
let utf8 = 0;
|
|
21
|
+
let json = 2; // quotes
|
|
22
|
+
let quotedPrintable = 0;
|
|
23
|
+
for (let i = 0; i < value.length; i++) {
|
|
24
|
+
const code = value.charCodeAt(i);
|
|
25
|
+
let bytes = code < 0x80 ? 1 : code < 0x800 ? 2 : 3;
|
|
26
|
+
let unpaired = code >= 0xd800 && code <= 0xdfff;
|
|
27
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
28
|
+
const next = value.charCodeAt(i + 1);
|
|
29
|
+
if (next >= 0xdc00 && next <= 0xdfff) { bytes = 4; unpaired = false; i++; }
|
|
30
|
+
}
|
|
31
|
+
utf8 += bytes;
|
|
32
|
+
json += unpaired || code < 0x20 && ![8, 9, 10, 12, 13].includes(code) ? 6
|
|
33
|
+
: code === 34 || code === 92 || code < 0x20 ? 2 : bytes;
|
|
34
|
+
// Encode all whitespace conservatively, including trailing spaces/tabs.
|
|
35
|
+
quotedPrintable += code >= 33 && code <= 126 && code !== 61 ? 1
|
|
36
|
+
: code === 10 || code === 13 ? 2 : 3 * bytes;
|
|
37
|
+
if (json > limit) tooLarge();
|
|
38
|
+
}
|
|
39
|
+
return { utf8, json, quotedPrintable };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Preflight validated inputs before mailbox arrays, Base64 or JSON allocation. */
|
|
43
|
+
export function assertResendMessageSize(message) {
|
|
44
|
+
let request = 1024; // fixed JSON field names, delimiters and optional containers
|
|
45
|
+
let email = ENVELOPE_RESERVE;
|
|
46
|
+
function add(requestBytes, emailBytes) {
|
|
47
|
+
request += requestBytes;
|
|
48
|
+
email += emailBytes;
|
|
49
|
+
if (request > MAX_RESEND_REQUEST_BYTES || email > MAX_RESEND_EMAIL_BYTES) tooLarge();
|
|
50
|
+
}
|
|
51
|
+
function metadata(value) {
|
|
52
|
+
const size = stringSize(value, MAX_RESEND_REQUEST_BYTES - request);
|
|
53
|
+
// Covers MIME header encoding/folding plus address quoting/normalization.
|
|
54
|
+
add(size.json + 64, 6 * size.utf8 + 128);
|
|
55
|
+
}
|
|
56
|
+
metadata(message.from);
|
|
57
|
+
metadata(message.subject);
|
|
58
|
+
if (message.replyTo !== undefined) metadata(message.replyTo);
|
|
59
|
+
for (const field of ['to', 'cc', 'bcc']) {
|
|
60
|
+
for (const value of message[field] ?? []) metadata(value);
|
|
61
|
+
}
|
|
62
|
+
for (const name in message.headers) {
|
|
63
|
+
if (!Object.hasOwn(message.headers, name)) continue;
|
|
64
|
+
metadata(name);
|
|
65
|
+
metadata(message.headers[name]);
|
|
66
|
+
}
|
|
67
|
+
for (const field of ['text', 'html']) {
|
|
68
|
+
if (message[field] === undefined) continue;
|
|
69
|
+
const size = stringSize(message[field], MAX_RESEND_REQUEST_BYTES - request);
|
|
70
|
+
// Reserve the larger of Base64 and conservative quoted-printable transfer
|
|
71
|
+
// encodings, including line folding, without assuming Resend's MIME choice.
|
|
72
|
+
const quotedPrintable = size.quotedPrintable + 3 * Math.ceil(size.quotedPrintable / 73);
|
|
73
|
+
const part = PART_RESERVE + Math.max(foldedBase64Size(size.utf8), quotedPrintable);
|
|
74
|
+
// HTML-only requests permit provider-generated plain text. Budget another
|
|
75
|
+
// part; explicit text (including an empty string) suppresses that generation.
|
|
76
|
+
add(size.json, part * (field === 'html' && message.text === undefined ? 2 : 1));
|
|
77
|
+
}
|
|
78
|
+
for (const attachment of message.attachments ?? []) {
|
|
79
|
+
// Charge the complete aggregate before any attachment bytes are read.
|
|
80
|
+
add(base64Size(attachment.content.byteLength) + 128,
|
|
81
|
+
PART_RESERVE + foldedBase64Size(attachment.content.byteLength));
|
|
82
|
+
metadata(attachment.filename);
|
|
83
|
+
metadata(attachment.contentType);
|
|
84
|
+
if (attachment.contentId !== undefined) metadata(attachment.contentId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Final actual UTF-8 request bound, independently of the preflight estimate. */
|
|
89
|
+
export function assertResendRequestSize(body) {
|
|
90
|
+
// A valid JSON string has no unescaped lone surrogates. Counting code points
|
|
91
|
+
// avoids allocating a second body-sized Uint8Array just to measure the request.
|
|
92
|
+
if (body.length > MAX_RESEND_REQUEST_BYTES) tooLarge();
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
for (const character of body) {
|
|
95
|
+
const code = character.codePointAt(0);
|
|
96
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
97
|
+
if (bytes > MAX_RESEND_REQUEST_BYTES) tooLarge();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# SendGrid outgoing adapter
|
|
2
|
+
|
|
3
|
+
`index.mjs` exports the portable ESM functions
|
|
4
|
+
`sendSendGrid(config, message, dependencies = {})` and
|
|
5
|
+
`serializeSendGridMessage(message)`. Both accept the frozen IF-056-MAIL-v1
|
|
6
|
+
message shape. The serializer validates and returns deterministic JSON request
|
|
7
|
+
bytes without effects; the sender validates config and message independently
|
|
8
|
+
and makes one POST to `https://api.sendgrid.com/v3/mail/send` with Bearer API-key
|
|
9
|
+
authentication. Config must name `provider: 'sendgrid'`, an explicit mode, and
|
|
10
|
+
`apiKey`. Remote sending remains remote in test/development modes. There is no
|
|
11
|
+
endpoint override, provider fallback, outbox selection or automatic retry.
|
|
12
|
+
|
|
13
|
+
The facade must call the actual `applyUnsubscribeHeaders` implementation before
|
|
14
|
+
this adapter. That assembler consumes `unsubscribe` and returns a new message
|
|
15
|
+
with the real headers. Direct callers must call the assembler themselves, or
|
|
16
|
+
supply their already-validated headers without an `unsubscribe` request. The
|
|
17
|
+
adapter rejects every unconsumed `unsubscribe` request, including one accompanied
|
|
18
|
+
by matching-looking headers, with `unsubscribe_requires_facade`. It validates
|
|
19
|
+
header syntax/reserved names and preserves headers; it does not certify the
|
|
20
|
+
semantics or delivered/DKIM coverage of caller-supplied unsubscribe headers.
|
|
21
|
+
|
|
22
|
+
The wire preserves To/Cc/Bcc display names, plain/HTML alternatives, reply address,
|
|
23
|
+
thread and custom headers, binary attachments, disposition and content IDs.
|
|
24
|
+
Attachments use base64 generated from `Uint8Array`; callers never supply base64.
|
|
25
|
+
`rer_delivery_id` always carries the caller's delivery ID. Custom arguments retain
|
|
26
|
+
the original reserved-name and 10,000-byte UTF-8 JSON limit. Positive safe-integer
|
|
27
|
+
ASM group IDs and optional boolean subscription tracking are supported. Omitted
|
|
28
|
+
subscription tracking remains omitted. Open, click and Google Analytics tracking
|
|
29
|
+
are explicitly disabled. Subscription tracking may append provider content;
|
|
30
|
+
its selection is not proof of delivered unsubscribe headers.
|
|
31
|
+
|
|
32
|
+
Shared core validation rejects unknown fields, header/address injection and
|
|
33
|
+
unsupported shapes. SendGrid additionally rejects empty subjects, explicitly
|
|
34
|
+
empty content parts/attachments, duplicate recipients, non-ASCII sender mailbox
|
|
35
|
+
addresses, and more than 1,000 total To/Cc/Bcc recipients. Omit unused content
|
|
36
|
+
parts. Local resource ceilings are 1,000 attachments, 1,000 header entries (custom arguments retain their 10,000-byte limit), 1,000,000 header key/value code units, and request JSON strictly below
|
|
37
|
+
30,000,000 UTF-8 bytes. Body and base64-size checks run before encoding attachments;
|
|
38
|
+
final serialized bytes are checked too. The wire ceiling does not establish that
|
|
39
|
+
the provider-generated MIME is below its separate 30 MB message limit. Oversize
|
|
40
|
+
or unsupported requests fail before fetch with fixed `MailValidationError` codes;
|
|
41
|
+
no values appear in diagnostics.
|
|
42
|
+
|
|
43
|
+
The shared transport enforces a 15-second deadline, abort signal and manual
|
|
44
|
+
redirect handling. Only HTTP 202 reports `accepted`; its real, bounded
|
|
45
|
+
`X-Message-Id` is retained or explicitly null. That is submission acceptance,
|
|
46
|
+
not mailbox receipt. HTTP 429 is `rate_limited`; other 4xx except 408 are
|
|
47
|
+
`rejected`. Network loss, deadline expiry, redirects, 408, 5xx and unexpected
|
|
48
|
+
non-202 responses are `uncertain`. Results never expose provider bodies/errors.
|
|
49
|
+
Correlation is not duplicate suppression: reconcile an uncertain submission
|
|
50
|
+
before deciding whether to submit again.
|
|
51
|
+
|
|
52
|
+
## Verification
|
|
53
|
+
|
|
54
|
+
From Base root:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
node --test modules/mail/tests/sendgrid/*.test.mjs modules/mail/tests/core/*.test.mjs
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tests include complete wire conformance, all response classes, no retry/forwarding,
|
|
61
|
+
redaction, nullable receipts, direct-call validation, binary round trips, exact
|
|
62
|
+
custom-argument limit, a real 15-second deadline, and isolated ESM execution with
|
|
63
|
+
Web APIs only (no Buffer/process/require). They use injected test-only responses;
|
|
64
|
+
no live provider API is called. `tests/sendgrid/conformance.mjs` exports
|
|
65
|
+
`runSendGridConformance()` for reuse inside workerd, and
|
|
66
|
+
`tests/sendgrid/worker.mjs` is its Worker entry. The isolated harness can run it with
|
|
67
|
+
`createWorker('tests/sendgrid/worker.mjs', ['tests/sendgrid/conformance.mjs',
|
|
68
|
+
'src/adapters/sendgrid/index.mjs'])`, then dispatch a fetch and dispose the worker.
|
|
69
|
+
`tests/assembly/workerd.test.mjs` exercises the complete facade inside actual
|
|
70
|
+
workerd, including unsubscribe assembly and the doctor sender chain. Hosted send/receive proof remains pending.
|
|
71
|
+
|
|
72
|
+
Provider references checked 6 October 2026:
|
|
73
|
+
[Mail Send](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send),
|
|
74
|
+
[limits](https://www.twilio.com/docs/sendgrid/api-reference/mail-send), and
|
|
75
|
+
[X-Message-ID](https://www.twilio.com/docs/sendgrid/glossary/x-message-id).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { validateConfig } from '../../core/validation.mjs';
|
|
2
|
+
import { fail, MailValidationError } from '../../core/errors.mjs';
|
|
3
|
+
import { requestProvider, classifyHttpStatus, sendResult, safeProviderMessageId } from '../../core/transport.mjs';
|
|
4
|
+
import { serializeSendGridMessage } from './serialize.mjs';
|
|
5
|
+
|
|
6
|
+
export { serializeSendGridMessage } from './serialize.mjs';
|
|
7
|
+
|
|
8
|
+
/** One submission; acceptance is not mailbox delivery or duplicate suppression. */
|
|
9
|
+
export async function sendSendGrid(config, message, dependencies = {}) {
|
|
10
|
+
let body;
|
|
11
|
+
let apiKey;
|
|
12
|
+
try {
|
|
13
|
+
validateConfig(config, dependencies);
|
|
14
|
+
if (config.provider !== 'sendgrid') fail('sendgrid_provider_required');
|
|
15
|
+
body = serializeSendGridMessage(message);
|
|
16
|
+
apiKey = config.apiKey;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof MailValidationError) throw error;
|
|
19
|
+
fail('invalid_mail_input');
|
|
20
|
+
}
|
|
21
|
+
const response = await requestProvider('https://api.sendgrid.com/v3/mail/send', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
24
|
+
body,
|
|
25
|
+
}, dependencies);
|
|
26
|
+
if (response === null) return sendResult('sendgrid', 'uncertain', null, 'transport_uncertain');
|
|
27
|
+
// Provider bodies may contain secrets or personal information. Never read them.
|
|
28
|
+
if (response.status === 202) {
|
|
29
|
+
return sendResult('sendgrid', 'accepted', safeProviderMessageId(response.headers.get('x-message-id')));
|
|
30
|
+
}
|
|
31
|
+
const status = classifyHttpStatus(response.status) ?? 'uncertain';
|
|
32
|
+
return sendResult('sendgrid', status, null, {
|
|
33
|
+
rejected: 'provider_rejected', rate_limited: 'provider_rate_limited', uncertain: 'provider_uncertain',
|
|
34
|
+
}[status]);
|
|
35
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { validateMessage, parseAddress, encodeBase64 } from '../../core/validation.mjs';
|
|
2
|
+
import { fail, MailValidationError } from '../../core/errors.mjs';
|
|
3
|
+
|
|
4
|
+
// Local allocation/wire ceilings, not a promise that provider-generated MIME fits.
|
|
5
|
+
const maxWireBytes = 30_000_000;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
|
|
8
|
+
function preflight(message) {
|
|
9
|
+
if (!message || typeof message !== 'object') fail('invalid_message');
|
|
10
|
+
let recipients = 0;
|
|
11
|
+
for (const field of ['to', 'cc', 'bcc']) {
|
|
12
|
+
if (Array.isArray(message[field])) recipients += message[field].length;
|
|
13
|
+
}
|
|
14
|
+
if (recipients > 1000) fail('sendgrid_too_many_recipients');
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
for (const field of ['text', 'html']) {
|
|
17
|
+
const value = message[field];
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
if (value.length >= maxWireBytes) fail('sendgrid_message_too_large');
|
|
20
|
+
bytes += encoder.encode(value).byteLength;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(message.attachments)) {
|
|
24
|
+
if (message.attachments.length > 1000) fail('sendgrid_too_many_attachments');
|
|
25
|
+
for (const attachment of message.attachments) {
|
|
26
|
+
if (attachment?.content instanceof Uint8Array) bytes += 4 * Math.ceil(attachment.content.byteLength / 3);
|
|
27
|
+
if (bytes >= maxWireBytes) fail('sendgrid_message_too_large');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (bytes >= maxWireBytes) fail('sendgrid_message_too_large');
|
|
31
|
+
// Bound records before the shared validator serializes custom arguments.
|
|
32
|
+
for (const field of ['headers', 'customArgs']) {
|
|
33
|
+
const record = message[field];
|
|
34
|
+
if (record && typeof record === 'object') {
|
|
35
|
+
const keys = Reflect.ownKeys(record);
|
|
36
|
+
if (keys.length > (field === 'customArgs' ? 10_000 : 1000)) fail('sendgrid_metadata_too_large');
|
|
37
|
+
let size = 0;
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const value = record[key];
|
|
40
|
+
size += typeof key === 'string' ? key.length : 0;
|
|
41
|
+
size += typeof value === 'string' ? value.length : 0;
|
|
42
|
+
if (size > (field === 'customArgs' ? 10_000 : 1_000_000)) fail('sendgrid_metadata_too_large');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Validated deterministic request bytes, without any provider or outbox effect. */
|
|
49
|
+
export function serializeSendGridMessage(message) {
|
|
50
|
+
try {
|
|
51
|
+
preflight(message);
|
|
52
|
+
validateMessage(message, 'sendgrid');
|
|
53
|
+
// Only the real unsubscribe assembler consumes this feature request.
|
|
54
|
+
if (message.unsubscribe !== undefined) fail('unsubscribe_requires_facade');
|
|
55
|
+
if (!message.subject.length) fail('sendgrid_subject_required');
|
|
56
|
+
const from = parseAddress(message.from);
|
|
57
|
+
if (/[^\x00-\x7f]/.test(from.email)) fail('sendgrid_ascii_sender_required');
|
|
58
|
+
const personalization = { to: message.to.map(parseAddress) };
|
|
59
|
+
if (message.cc?.length) personalization.cc = message.cc.map(parseAddress);
|
|
60
|
+
if (message.bcc?.length) personalization.bcc = message.bcc.map(parseAddress);
|
|
61
|
+
const recipients = new Set();
|
|
62
|
+
for (const list of Object.values(personalization)) {
|
|
63
|
+
for (const { email } of list) {
|
|
64
|
+
const key = email.toLowerCase();
|
|
65
|
+
if (recipients.has(key)) fail('sendgrid_duplicate_recipient');
|
|
66
|
+
recipients.add(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const content = [];
|
|
70
|
+
for (const [field, type] of [['text', 'text/plain'], ['html', 'text/html']]) {
|
|
71
|
+
if (message[field] !== undefined) {
|
|
72
|
+
if (!message[field].length) fail('sendgrid_empty_content');
|
|
73
|
+
content.push({ type, value: message[field] });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const attachments = message.attachments?.map(attachment => {
|
|
77
|
+
if (!attachment.content.byteLength) fail('sendgrid_empty_attachment');
|
|
78
|
+
return {
|
|
79
|
+
content: encodeBase64(attachment.content), type: attachment.contentType,
|
|
80
|
+
filename: attachment.filename, disposition: attachment.disposition ?? 'attachment',
|
|
81
|
+
...(attachment.contentId === undefined ? {} : { content_id: attachment.contentId }),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
personalizations: [personalization], from, subject: message.subject, content,
|
|
86
|
+
...(message.replyTo === undefined ? {} : { reply_to: parseAddress(message.replyTo) }),
|
|
87
|
+
// The facade always supplies a headers object; an empty one is the same request as none.
|
|
88
|
+
...(message.headers === undefined || !Reflect.ownKeys(message.headers).length ? {} : { headers: message.headers }),
|
|
89
|
+
...(attachments?.length ? { attachments } : {}),
|
|
90
|
+
custom_args: { ...message.customArgs, rer_delivery_id: message.deliveryId },
|
|
91
|
+
...(message.asmGroupId === undefined ? {} : { asm: { group_id: message.asmGroupId } }),
|
|
92
|
+
tracking_settings: {
|
|
93
|
+
click_tracking: { enable: false, enable_text: false }, open_tracking: { enable: false },
|
|
94
|
+
ganalytics: { enable: false },
|
|
95
|
+
...(message.subscriptionTracking === undefined ? {} : { subscription_tracking: { enable: message.subscriptionTracking } }),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (encoder.encode(body).byteLength >= maxWireBytes) fail('sendgrid_message_too_large');
|
|
99
|
+
return body;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof MailValidationError) throw error;
|
|
102
|
+
fail('invalid_mail_input');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Input values never appear in validation diagnostics. */
|
|
2
|
+
export class MailValidationError extends Error {
|
|
3
|
+
constructor(code = 'invalid_mail_input') {
|
|
4
|
+
super('Mail input is invalid; check the error code and module contract.');
|
|
5
|
+
this.name = 'MailValidationError';
|
|
6
|
+
this.code = typeof code === 'string' && /^[a-z][a-z0-9_]{0,63}$/.test(code) ? code : 'invalid_mail_input';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function fail(code) { throw new MailValidationError(code); }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { validateSendResult } from './validation.mjs';
|
|
2
|
+
|
|
3
|
+
/** One bounded request. Fetch exceptions never escape with credential/body text. */
|
|
4
|
+
export async function requestProvider(url, init, dependencies = {}) {
|
|
5
|
+
const timeout = dependencies.timeoutMilliseconds ?? 15_000;
|
|
6
|
+
let timer;
|
|
7
|
+
try {
|
|
8
|
+
const deadline = new Promise(resolve => { timer = setTimeout(() => resolve(null), timeout); });
|
|
9
|
+
return await Promise.race([
|
|
10
|
+
Promise.resolve().then(() => (dependencies.fetch ?? globalThis.fetch)(url, {
|
|
11
|
+
...init, redirect: 'manual', signal: AbortSignal.timeout(timeout),
|
|
12
|
+
})).then(response => response instanceof Response ? response : null, () => null),
|
|
13
|
+
deadline,
|
|
14
|
+
]);
|
|
15
|
+
} catch { return null; }
|
|
16
|
+
finally { clearTimeout(timer); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function classifyHttpStatus(status) {
|
|
20
|
+
if (status === 429) return 'rate_limited';
|
|
21
|
+
if (status >= 400 && status < 500 && status !== 408) return 'rejected';
|
|
22
|
+
if (status >= 200 && status < 300) return null;
|
|
23
|
+
return 'uncertain';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function safeProviderMessageId(value) {
|
|
27
|
+
return typeof value === 'string' && value.trim() && value.length <= 1024 && !/[\x00-\x1f\x7f]/.test(value) ? value.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
export function sendResult(provider, status, providerMessageId = null, code, recipientResults) {
|
|
30
|
+
return validateSendResult({ provider, status, providerMessageId,
|
|
31
|
+
...(code === undefined ? {} : { code }), ...(recipientResults === undefined ? {} : { recipientResults }),
|
|
32
|
+
}, provider);
|
|
33
|
+
}
|