@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,143 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { parseAddress } from '../core/validation.mjs';
|
|
3
|
+
import { base64, decode, quotedPrintable, record } from './bytes.mjs';
|
|
4
|
+
const singleton = new Set(['message-id', 'in-reply-to', 'references', 'subject', 'from', 'to', 'content-type', 'content-transfer-encoding', 'content-disposition', 'content-id']);
|
|
5
|
+
export function headerText(value, empty = true) {
|
|
6
|
+
if (typeof value !== 'string' || (!empty && !value) || /[\x00-\x08\x0a-\x1f\x7f]/.test(value)) fail('invalid_inbound_header');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function unfold(value) {
|
|
10
|
+
if (typeof value !== 'string') fail('invalid_inbound_header');
|
|
11
|
+
return headerText(value.replace(/\r?\n[ \t]+/g, ' ')).trim();
|
|
12
|
+
}
|
|
13
|
+
export function parseHeaders(value) {
|
|
14
|
+
if (typeof value !== 'string' || /\r(?!\n)/.test(value)) fail('invalid_inbound_headers');
|
|
15
|
+
const map = new Map(); let previous = null;
|
|
16
|
+
for (const line of value.split(/\r?\n/)) {
|
|
17
|
+
if (!line) { previous = null; continue; }
|
|
18
|
+
if (/^[ \t]/.test(line)) {
|
|
19
|
+
if (!previous) fail('invalid_inbound_headers');
|
|
20
|
+
map.set(previous, `${map.get(previous)} ${headerText(line.trim())}`); continue;
|
|
21
|
+
}
|
|
22
|
+
const match = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+):[ \t]*(.*)$/.exec(line);
|
|
23
|
+
if (!match) fail('invalid_inbound_headers');
|
|
24
|
+
previous = match[1].toLowerCase();
|
|
25
|
+
if (map.has(previous) && singleton.has(previous)) fail('duplicate_inbound_header');
|
|
26
|
+
const text = headerText(match[2].trim());
|
|
27
|
+
map.set(previous, map.has(previous) ? `${map.get(previous)}, ${text}` : text);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
export function objectHeaders(value = {}) {
|
|
32
|
+
record(value); const map = new Map();
|
|
33
|
+
for (const [key, val] of Object.entries(value)) {
|
|
34
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key) || map.has(key.toLowerCase())) fail('invalid_inbound_headers');
|
|
35
|
+
map.set(key.toLowerCase(), unfold(val));
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
export function words(value) {
|
|
40
|
+
// RFC 2047 suppresses whitespace between adjacent encoded words.
|
|
41
|
+
const source = value.replace(/(\?=)[ \t]+(?==\?)/g, '$1');
|
|
42
|
+
const result = source.replace(/=\?([^?\s]+)\?([bq])\?([^?]*)\?=/gi, (_, charset, encoding, content) => {
|
|
43
|
+
if (/[^\x21-\x7e]/.test(content)) fail('invalid_inbound_encoded_word');
|
|
44
|
+
return decode(encoding.toLowerCase() === 'b' ? base64(content) : quotedPrintable(content, true), charset);
|
|
45
|
+
});
|
|
46
|
+
if (/=\?/.test(result)) fail('invalid_inbound_encoded_word');
|
|
47
|
+
return headerText(result);
|
|
48
|
+
}
|
|
49
|
+
export function parameterized(value) {
|
|
50
|
+
headerText(value); let i = 0;
|
|
51
|
+
while (i < value.length && value[i] !== ';') i++;
|
|
52
|
+
const type = value.slice(0, i).trim().toLowerCase(); const params = new Map();
|
|
53
|
+
while (i < value.length) {
|
|
54
|
+
i++; while (/[ \t]/.test(value[i] ?? '') && i < value.length) i++;
|
|
55
|
+
const rest = value.slice(i); const name = /^([a-z0-9!#$%&'*+.^_`|~-]+)[ \t]*=[ \t]*/i.exec(rest);
|
|
56
|
+
if (!name) fail('invalid_inbound_mime');
|
|
57
|
+
i += name[0].length; let text = '';
|
|
58
|
+
if (value[i] === '"') {
|
|
59
|
+
i++; let closed = false;
|
|
60
|
+
while (i < value.length) {
|
|
61
|
+
const char = value[i++];
|
|
62
|
+
if (char === '"') { closed = true; break; }
|
|
63
|
+
if (char === '\\') { if (i >= value.length) fail('invalid_inbound_mime'); text += value[i++]; }
|
|
64
|
+
else text += char;
|
|
65
|
+
}
|
|
66
|
+
if (!closed) fail('invalid_inbound_mime');
|
|
67
|
+
while (i < value.length && /[ \t]/.test(value[i])) i++;
|
|
68
|
+
if (i < value.length && value[i] !== ';') fail('invalid_inbound_mime');
|
|
69
|
+
} else {
|
|
70
|
+
const start = i; while (i < value.length && value[i] !== ';') i++;
|
|
71
|
+
text = value.slice(start, i).trim();
|
|
72
|
+
if (!text || /[\s"\\]/.test(text)) fail('invalid_inbound_mime');
|
|
73
|
+
}
|
|
74
|
+
const key = name[1].toLowerCase();
|
|
75
|
+
if (params.has(key)) fail('invalid_inbound_mime'); params.set(key, text);
|
|
76
|
+
}
|
|
77
|
+
return { type, params };
|
|
78
|
+
}
|
|
79
|
+
function extended(value) {
|
|
80
|
+
const match = /^([^']+)'[^']*'(.*)$/.exec(value);
|
|
81
|
+
if (!match) fail('invalid_inbound_mime');
|
|
82
|
+
const bytes = []; const content = match[2];
|
|
83
|
+
for (let i = 0; i < content.length; i++) {
|
|
84
|
+
if (content[i] === '%') {
|
|
85
|
+
if (!/^[a-f0-9]{2}$/i.test(content.slice(i + 1, i + 3))) fail('invalid_inbound_mime');
|
|
86
|
+
bytes.push(parseInt(content.slice(i + 1, i + 3), 16)); i += 2;
|
|
87
|
+
} else { if (content.charCodeAt(i) > 127) fail('invalid_inbound_mime'); bytes.push(content.charCodeAt(i)); }
|
|
88
|
+
}
|
|
89
|
+
return decode(new Uint8Array(bytes), match[1]);
|
|
90
|
+
}
|
|
91
|
+
export function parameter(params, name) {
|
|
92
|
+
if (params.has(`${name}*`)) return headerText(extended(params.get(`${name}*`)));
|
|
93
|
+
const pieces = [...params.keys()].filter(key => key.startsWith(`${name}*`));
|
|
94
|
+
if (pieces.length) {
|
|
95
|
+
let joined = '', encoded = false;
|
|
96
|
+
for (let index = 0; index < pieces.length; index++) {
|
|
97
|
+
const key = `${name}*${index}`;
|
|
98
|
+
if (params.has(`${key}*`)) { if (params.has(key)) fail('invalid_inbound_mime'); encoded = true; joined += params.get(`${key}*`); }
|
|
99
|
+
else if (params.has(key)) joined += params.get(key);
|
|
100
|
+
else fail('invalid_inbound_mime');
|
|
101
|
+
}
|
|
102
|
+
return headerText(encoded ? extended(joined) : words(joined));
|
|
103
|
+
}
|
|
104
|
+
return params.has(name) ? words(params.get(name)) : null;
|
|
105
|
+
}
|
|
106
|
+
export function contentType(value = 'application/octet-stream') {
|
|
107
|
+
if (typeof value !== 'string' || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(value)) fail('invalid_inbound_content_type');
|
|
108
|
+
return value.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
export function contentId(value) {
|
|
111
|
+
if (value == null || value === '') return null;
|
|
112
|
+
headerText(value); const id = value.startsWith('<') && value.endsWith('>') ? value.slice(1, -1) : value;
|
|
113
|
+
if (!id || /[\s<>]/.test(id)) fail('invalid_inbound_content_id'); return id;
|
|
114
|
+
}
|
|
115
|
+
export function address(value) { return parseAddress(words(unfold(value))).email; }
|
|
116
|
+
export function addresses(value) {
|
|
117
|
+
if (Array.isArray(value)) { if (!value.length) fail('invalid_inbound_recipients'); return value.map(address); }
|
|
118
|
+
const source = unfold(value); const parts = []; let part = '', quoted = false, angle = false, escape = false;
|
|
119
|
+
for (const char of source) {
|
|
120
|
+
if (escape) { part += char; escape = false; continue; }
|
|
121
|
+
if (char === '\\' && quoted) { part += char; escape = true; continue; }
|
|
122
|
+
if (char === '"') quoted = !quoted;
|
|
123
|
+
if (!quoted && char === '<') { if (angle) fail('invalid_inbound_recipients'); angle = true; }
|
|
124
|
+
if (!quoted && char === '>') { if (!angle) fail('invalid_inbound_recipients'); angle = false; }
|
|
125
|
+
if (char === ',' && !quoted && !angle) { parts.push(address(part)); part = ''; } else part += char;
|
|
126
|
+
}
|
|
127
|
+
if (quoted || angle || escape) fail('invalid_inbound_recipients');
|
|
128
|
+
parts.push(address(part)); return parts;
|
|
129
|
+
}
|
|
130
|
+
export function messageIds(value) {
|
|
131
|
+
if (value == null || value === '') return [];
|
|
132
|
+
const source = unfold(value); const ids = source.match(/<[^<>\s]+>/g) ?? [];
|
|
133
|
+
if (!ids.length || source.replace(/<[^<>\s]+>/g, '').trim()) fail('invalid_inbound_thread');
|
|
134
|
+
return ids;
|
|
135
|
+
}
|
|
136
|
+
export function metadata(headers, from, to, subject) {
|
|
137
|
+
const ids = messageIds(headers.get('message-id'));
|
|
138
|
+
if (ids.length > 1) fail('invalid_inbound_thread');
|
|
139
|
+
const reply = messageIds(headers.get('in-reply-to'));
|
|
140
|
+
return { messageId: ids[0] ?? null, from: address(from), to: addresses(to),
|
|
141
|
+
subject: words(unfold(subject ?? headers.get('subject') ?? '')),
|
|
142
|
+
inReplyTo: reply.length ? reply.join(' ') : null, references: messageIds(headers.get('references')) };
|
|
143
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { fail, MailValidationError } from '../core/errors.mjs';
|
|
2
|
+
import { bounded, limitsFor, readStream, record } from './bytes.mjs';
|
|
3
|
+
import { parseMime } from './mime.mjs';
|
|
4
|
+
import { normalizeResend } from './resend.mjs';
|
|
5
|
+
import { normalizeSendGrid } from './sendgrid.mjs';
|
|
6
|
+
/** Trusted transport parser. Verify webhook bytes BEFORE calling or hydrating. */
|
|
7
|
+
export async function normalizeInbound(input, limits) {
|
|
8
|
+
try {
|
|
9
|
+
record(input); limits = limitsFor(limits); let result;
|
|
10
|
+
switch (input.provider) {
|
|
11
|
+
case 'cloudflare': {
|
|
12
|
+
const message = input.message;
|
|
13
|
+
if (!message || !(message.headers instanceof Headers) || !Number.isSafeInteger(message.rawSize) || message.rawSize < 0) fail('invalid_inbound_message');
|
|
14
|
+
bounded(message.rawSize, limits.maxRawBytes, 'inbound_raw_limit');
|
|
15
|
+
const raw = await readStream(message.raw, limits.maxRawBytes);
|
|
16
|
+
if (raw.length !== message.rawSize) fail('inbound_raw_size_mismatch');
|
|
17
|
+
result = parseMime(raw, limits, { from: message.from, to: message.to }); break;
|
|
18
|
+
}
|
|
19
|
+
case 'sendgrid': result = normalizeSendGrid(input, limits); break;
|
|
20
|
+
case 'resend': result = await normalizeResend(input, limits); break;
|
|
21
|
+
default: fail('invalid_inbound_provider');
|
|
22
|
+
}
|
|
23
|
+
return { provider: input.provider, ...result };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof MailValidationError) throw error;
|
|
26
|
+
fail('invalid_inbound');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export { createResendAttachmentFetcher } from './resend-attachment.mjs';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { bodyBudget, bounded, decode, transfer, utf8 } from './bytes.mjs';
|
|
3
|
+
import { contentId, contentType, headerText, metadata, parameter, parameterized } from './headers.mjs';
|
|
4
|
+
import { entity, multipart } from './multipart.mjs';
|
|
5
|
+
export function parseMime(bytes, limits, envelope) {
|
|
6
|
+
const root = entity(bytes); const budget = bodyBudget(limits);
|
|
7
|
+
const text = [], html = [], attachments = [];
|
|
8
|
+
function visit(part, depth = 0) {
|
|
9
|
+
if (depth > 30) fail('inbound_mime_depth');
|
|
10
|
+
const type = parameterized(part.headers.get('content-type') ?? 'text/plain');
|
|
11
|
+
contentType(type.type);
|
|
12
|
+
const disposition = parameterized(part.headers.get('content-disposition') ?? 'inline');
|
|
13
|
+
if (!['inline', 'attachment'].includes(disposition.type)) fail('invalid_inbound_disposition');
|
|
14
|
+
const filename = parameter(disposition.params, 'filename') ?? parameter(type.params, 'name');
|
|
15
|
+
const isAttachment = disposition.type === 'attachment' || filename !== null;
|
|
16
|
+
const encoding = part.headers.get('content-transfer-encoding');
|
|
17
|
+
if (type.type.startsWith('multipart/') && !isAttachment) {
|
|
18
|
+
if (encoding && !['7bit', '8bit', 'binary'].includes(encoding.toLowerCase())) fail('invalid_inbound_encoding');
|
|
19
|
+
for (const child of multipart(part.body, type.params.get('boundary'))) visit(entity(child), depth + 1);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const decoded = transfer(part.body, encoding ?? '8bit');
|
|
23
|
+
if (!isAttachment && ['text/plain', 'text/html'].includes(type.type)) {
|
|
24
|
+
const value = decode(decoded, type.params.get('charset') ?? 'utf-8');
|
|
25
|
+
budget.body(value, decoded.length);
|
|
26
|
+
(type.type === 'text/plain' ? text : html).push(value);
|
|
27
|
+
} else {
|
|
28
|
+
budget.attachment(decoded);
|
|
29
|
+
attachments.push({ filename: headerText(filename ?? ''), contentType: type.type, content: decoded.slice(), contentId: contentId(part.headers.get('content-id')) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
visit(root);
|
|
33
|
+
const plain = text.length ? text.join('\n') : null, rich = html.length ? html.join('\n') : null;
|
|
34
|
+
bounded(utf8.encode(plain ?? '').length + utf8.encode(rich ?? '').length, limits.maxBodyBytes, 'inbound_body_limit');
|
|
35
|
+
return { ...metadata(root.headers, envelope?.from ?? root.headers.get('from'), envelope?.to ?? root.headers.get('to')), text: plain, html: rich, attachments };
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { binary, decode } from './bytes.mjs';
|
|
3
|
+
import { parseHeaders } from './headers.mjs';
|
|
4
|
+
export function entity(bytes) {
|
|
5
|
+
const source = binary(bytes); const divider = /\r?\n\r?\n/.exec(source);
|
|
6
|
+
if (!divider) fail('invalid_inbound_mime');
|
|
7
|
+
return { headers: parseHeaders(decode(bytes.subarray(0, divider.index))), body: bytes.subarray(divider.index + divider[0].length) };
|
|
8
|
+
}
|
|
9
|
+
export function multipart(bytes, boundary) {
|
|
10
|
+
if (typeof boundary !== 'string' || !/^[0-9A-Za-z'()+_,\-./:=? ]{1,70}$/.test(boundary) || boundary.endsWith(' ')) fail('invalid_inbound_boundary');
|
|
11
|
+
const escaped = boundary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
const marker = new RegExp(`^--${escaped}(--)?[ \\t]*(?:\\r?\\n|$)`, 'gm');
|
|
13
|
+
const source = binary(bytes); const parts = []; let start = null, closed = false;
|
|
14
|
+
for (const match of source.matchAll(marker)) {
|
|
15
|
+
if (closed) fail('invalid_inbound_multipart');
|
|
16
|
+
if (start !== null) {
|
|
17
|
+
let end = match.index;
|
|
18
|
+
if (source[end - 1] === '\n') { end--; if (source[end - 1] === '\r') end--; }
|
|
19
|
+
if (end <= start) fail('invalid_inbound_multipart');
|
|
20
|
+
parts.push(bytes.subarray(start, end));
|
|
21
|
+
}
|
|
22
|
+
if (match[1]) { if (start === null) fail('invalid_inbound_multipart'); closed = true; }
|
|
23
|
+
else start = match.index + match[0].length;
|
|
24
|
+
}
|
|
25
|
+
if (!closed || !parts.length) fail('invalid_inbound_multipart');
|
|
26
|
+
return parts;
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fail, MailValidationError } from '../core/errors.mjs';
|
|
2
|
+
import { validateConfig } from '../core/validation.mjs';
|
|
3
|
+
import { decode, readStream } from './bytes.mjs';
|
|
4
|
+
import { attachmentMetadata } from './resend.mjs';
|
|
5
|
+
/** Call only after authenticating email.received and binding the hydrated email ID. */
|
|
6
|
+
export function createResendAttachmentFetcher({ apiKey, maxAttachmentBytes }, dependencies = {}) {
|
|
7
|
+
validateConfig({ provider: 'resend', mode: 'production', apiKey }, dependencies);
|
|
8
|
+
if (!Number.isSafeInteger(maxAttachmentBytes) || maxAttachmentBytes <= 0) fail('invalid_inbound_limits');
|
|
9
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
10
|
+
const clock = dependencies.clock ?? Date.now;
|
|
11
|
+
return async expected => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), 15_000);
|
|
14
|
+
try {
|
|
15
|
+
const item = attachmentMetadata(expected, expected.emailId);
|
|
16
|
+
if (item.size > maxAttachmentBytes) fail('inbound_attachment_limit');
|
|
17
|
+
// Fixed API origin, escaped identifiers, and manual redirects prevent credential forwarding.
|
|
18
|
+
const response = await fetcher(`https://api.resend.com/emails/receiving/${encodeURIComponent(item.emailId)}/attachments/${encodeURIComponent(item.id)}`, {
|
|
19
|
+
method: 'GET', headers: { authorization: `Bearer ${apiKey}`, accept: 'application/json' }, redirect: 'manual', signal: controller.signal,
|
|
20
|
+
});
|
|
21
|
+
if (response.status !== 200) fail('inbound_attachment_fetch_failed');
|
|
22
|
+
const data = JSON.parse(decode(await readStream(response.body, 65_536)));
|
|
23
|
+
const actual = attachmentMetadata(data, item.emailId);
|
|
24
|
+
for (const key of ['id', 'filename', 'content_type', 'content_disposition', 'content_id', 'size']) {
|
|
25
|
+
if (actual[key] !== item[key]) fail('inbound_attachment_metadata_mismatch');
|
|
26
|
+
}
|
|
27
|
+
const url = new URL(data.download_url);
|
|
28
|
+
// The documented provider CDN is the only accepted download origin.
|
|
29
|
+
if (url.origin !== 'https://inbound-cdn.resend.com' || url.username || url.password || url.hash ||
|
|
30
|
+
url.pathname !== `/${item.emailId}/attachments/${item.id}` || !Number.isFinite(Date.parse(data.expires_at)) || Date.parse(data.expires_at) <= clock()) fail('unsafe_inbound_attachment_url');
|
|
31
|
+
const download = await fetcher(url.href, { method: 'GET', redirect: 'manual', signal: controller.signal });
|
|
32
|
+
if (download.status !== 200) fail('inbound_attachment_fetch_failed');
|
|
33
|
+
const length = download.headers.get('content-length');
|
|
34
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) !== item.size)) fail('inbound_attachment_size_mismatch');
|
|
35
|
+
const bytes = await readStream(download.body, Math.min(item.size, maxAttachmentBytes));
|
|
36
|
+
if (bytes.length !== item.size) fail('inbound_attachment_size_mismatch');
|
|
37
|
+
return bytes;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof MailValidationError) throw error;
|
|
40
|
+
fail('inbound_attachment_fetch_failed');
|
|
41
|
+
} finally { clearTimeout(timer); }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { bodyBudget, bounded, record, utf8 } from './bytes.mjs';
|
|
3
|
+
import { contentId, contentType, headerText, metadata, objectHeaders } from './headers.mjs';
|
|
4
|
+
export function providerId(value) {
|
|
5
|
+
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) fail('invalid_inbound_provider_id');
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function attachmentMetadata(value, emailId) {
|
|
9
|
+
record(value); providerId(emailId);
|
|
10
|
+
const id = providerId(value.id);
|
|
11
|
+
if (!Number.isSafeInteger(value.size) || value.size < 0) fail('invalid_inbound_attachment_size');
|
|
12
|
+
if (value.content_disposition != null && !['inline', 'attachment'].includes(value.content_disposition)) fail('invalid_inbound_disposition');
|
|
13
|
+
return Object.freeze({ emailId, id, filename: headerText(value.filename), content_type: contentType(value.content_type),
|
|
14
|
+
content_disposition: value.content_disposition ?? null, content_id: contentId(value.content_id), size: value.size });
|
|
15
|
+
}
|
|
16
|
+
export async function normalizeResend(input, limits) {
|
|
17
|
+
const email = record(input.email);
|
|
18
|
+
const id = providerId(email.id);
|
|
19
|
+
if (id !== providerId(input.signedEmailId)) fail('inbound_identity_mismatch');
|
|
20
|
+
// Hydrated API JSON is the available raw representation, not the SMTP original.
|
|
21
|
+
let raw; try { raw = JSON.stringify(email); } catch { fail('invalid_inbound_raw'); }
|
|
22
|
+
bounded(utf8.encode(raw).length, limits.maxRawBytes, 'inbound_raw_limit');
|
|
23
|
+
const headers = objectHeaders(email.headers);
|
|
24
|
+
if (email.message_id != null) {
|
|
25
|
+
if (headers.has('message-id') && headers.get('message-id') !== email.message_id) fail('inbound_identity_mismatch');
|
|
26
|
+
headers.set('message-id', email.message_id);
|
|
27
|
+
}
|
|
28
|
+
const result = metadata(headers, email.from, email.to, email.subject);
|
|
29
|
+
const text = email.text ?? null, html = email.html ?? null;
|
|
30
|
+
if ((text !== null && typeof text !== 'string') || (html !== null && typeof html !== 'string')) fail('invalid_inbound_body');
|
|
31
|
+
const budget = bodyBudget(limits);
|
|
32
|
+
if (text !== null) budget.body(text);
|
|
33
|
+
if (html !== null) budget.body(html);
|
|
34
|
+
if (!Array.isArray(email.attachments)) fail('invalid_inbound_attachments');
|
|
35
|
+
bounded(email.attachments.length, limits.maxAttachments, 'inbound_attachment_count');
|
|
36
|
+
const declared = email.attachments.map(item => attachmentMetadata(item, id));
|
|
37
|
+
if (new Set(declared.map(item => item.id)).size !== declared.length) fail('duplicate_inbound_attachment');
|
|
38
|
+
let total = 0;
|
|
39
|
+
for (const item of declared) {
|
|
40
|
+
bounded(item.size, limits.maxAttachmentBytes, 'inbound_attachment_limit'); total += item.size;
|
|
41
|
+
bounded(total, limits.maxTotalAttachmentBytes, 'inbound_attachments_limit');
|
|
42
|
+
}
|
|
43
|
+
if (text === null && html === null && !declared.length) fail('invalid_inbound_body');
|
|
44
|
+
if (declared.length && typeof input.fetchAttachment !== 'function') fail('inbound_attachment_fetch_required');
|
|
45
|
+
const attachments = [];
|
|
46
|
+
for (const item of declared) {
|
|
47
|
+
// Only verified identifiers and sanitized metadata cross this seam; never URLs.
|
|
48
|
+
const content = await input.fetchAttachment(item);
|
|
49
|
+
budget.attachment(content);
|
|
50
|
+
if (content.length !== item.size) fail('inbound_attachment_size_mismatch');
|
|
51
|
+
attachments.push({ filename: item.filename, contentType: item.content_type, content: content.slice(), contentId: item.content_id });
|
|
52
|
+
}
|
|
53
|
+
return { ...result, text, html, attachments };
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fail } from '../core/errors.mjs';
|
|
2
|
+
import { bodyBudget, bounded, decode, record, transfer } from './bytes.mjs';
|
|
3
|
+
import { contentId, contentType, headerText, metadata, parameter, parameterized, parseHeaders } from './headers.mjs';
|
|
4
|
+
import { entity, multipart } from './multipart.mjs';
|
|
5
|
+
import { parseMime } from './mime.mjs';
|
|
6
|
+
function json(value) {
|
|
7
|
+
try { return record(JSON.parse(value)); } catch { fail('invalid_inbound_json'); }
|
|
8
|
+
}
|
|
9
|
+
export function normalizeSendGrid(input, limits) {
|
|
10
|
+
if (!(input.rawBody instanceof Uint8Array)) fail('invalid_inbound_raw');
|
|
11
|
+
bounded(input.rawBody.length, limits.maxRawBytes, 'inbound_raw_limit');
|
|
12
|
+
const formType = parameterized(input.contentType);
|
|
13
|
+
if (formType.type !== 'multipart/form-data') fail('invalid_inbound_content_type');
|
|
14
|
+
const fields = new Map(), files = new Map();
|
|
15
|
+
for (const raw of multipart(input.rawBody, formType.params.get('boundary'))) {
|
|
16
|
+
const part = entity(raw); const disposition = parameterized(part.headers.get('content-disposition') ?? '');
|
|
17
|
+
const name = disposition.params.get('name');
|
|
18
|
+
if (disposition.type !== 'form-data' || !name || fields.has(name) || files.has(name)) fail('invalid_inbound_form');
|
|
19
|
+
const filename = parameter(disposition.params, 'filename');
|
|
20
|
+
const bytes = transfer(part.body, part.headers.get('content-transfer-encoding') ?? 'binary');
|
|
21
|
+
if (filename !== null) {
|
|
22
|
+
if (!/^attachment\d+$/.test(name)) fail('invalid_inbound_form');
|
|
23
|
+
const type = parameterized(part.headers.get('content-type') ?? 'application/octet-stream');
|
|
24
|
+
files.set(name, { filename, contentType: contentType(type.type), content: bytes.slice(), contentId: contentId(part.headers.get('content-id')) });
|
|
25
|
+
} else fields.set(name, { bytes, type: parameterized(part.headers.get('content-type') ?? 'text/plain') });
|
|
26
|
+
}
|
|
27
|
+
const charsets = fields.has('charsets') ? json(decode(fields.get('charsets').bytes)) : {};
|
|
28
|
+
for (const value of Object.values(charsets)) if (typeof value !== 'string') fail('invalid_inbound_charset');
|
|
29
|
+
const field = name => fields.has(name) ? decode(fields.get(name).bytes, fields.get(name).type.params.get('charset') ?? charsets[name] ?? 'utf-8') : undefined;
|
|
30
|
+
if (fields.has('email')) {
|
|
31
|
+
if (files.size || fields.has('text') || fields.has('html')) fail('ambiguous_inbound_mime');
|
|
32
|
+
return parseMime(fields.get('email').bytes, limits);
|
|
33
|
+
}
|
|
34
|
+
const headers = parseHeaders(field('headers') ?? '');
|
|
35
|
+
const result = metadata(headers, field('from') ?? headers.get('from'), field('to') ?? headers.get('to'), field('subject'));
|
|
36
|
+
const text = field('text') ?? null, html = field('html') ?? null;
|
|
37
|
+
if (text === null && html === null && files.size === 0) fail('invalid_inbound_body');
|
|
38
|
+
const count = field('attachments') ?? '0';
|
|
39
|
+
if (!/^(?:0|[1-9]\d*)$/.test(count) || Number(count) !== files.size) fail('invalid_inbound_attachment_count');
|
|
40
|
+
const info = fields.has('attachment-info') ? json(field('attachment-info')) : {};
|
|
41
|
+
const ids = fields.has('content-ids') ? json(field('content-ids')) : {};
|
|
42
|
+
for (const name of Object.keys(info)) if (!files.has(name)) fail('invalid_inbound_attachment_metadata');
|
|
43
|
+
const reverseIds = new Map();
|
|
44
|
+
for (const [id, name] of Object.entries(ids)) {
|
|
45
|
+
if (typeof name !== 'string' || !files.has(name) || reverseIds.has(name)) fail('invalid_inbound_attachment_metadata');
|
|
46
|
+
reverseIds.set(name, contentId(id));
|
|
47
|
+
}
|
|
48
|
+
const budget = bodyBudget(limits); const attachments = [];
|
|
49
|
+
if (text !== null) budget.body(text, fields.get('text').bytes.length);
|
|
50
|
+
if (html !== null) budget.body(html, fields.get('html').bytes.length);
|
|
51
|
+
for (const [name, file] of files) {
|
|
52
|
+
const details = info[name] === undefined ? {} : record(info[name]);
|
|
53
|
+
if (details.filename !== undefined && headerText(details.filename) !== file.filename) fail('inbound_attachment_metadata_mismatch');
|
|
54
|
+
if (details.type !== undefined && contentType(details.type) !== file.contentType) fail('inbound_attachment_metadata_mismatch');
|
|
55
|
+
const identifiers = [file.contentId, contentId(details['content-id']), reverseIds.get(name)].filter(id => id != null);
|
|
56
|
+
if (new Set(identifiers).size > 1) fail('inbound_attachment_metadata_mismatch');
|
|
57
|
+
budget.attachment(file.content); attachments.push({ ...file, contentId: identifiers[0] ?? null });
|
|
58
|
+
}
|
|
59
|
+
return { ...result, text, html, attachments };
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { validateConfig, validateMessage, validateSendResult } from './core/validation.mjs';
|
|
2
|
+
import { fail } from './core/errors.mjs';
|
|
3
|
+
import { sendSendGrid } from './adapters/sendgrid/index.mjs';
|
|
4
|
+
import { sendResend } from './adapters/resend/index.mjs';
|
|
5
|
+
import { sendCloudflare } from './adapters/cloudflare/index.mjs';
|
|
6
|
+
import { sendMessaging, reconcileMessaging, eraseMessaging } from './adapters/messaging/index.mjs';
|
|
7
|
+
import { sendDevelopment } from './development/index.mjs';
|
|
8
|
+
import { applyUnsubscribeHeaders } from './unsubscribe/index.mjs';
|
|
9
|
+
|
|
10
|
+
const transports = { sendgrid: sendSendGrid, resend: sendResend, cloudflare: sendCloudflare, development: sendDevelopment, messaging: sendMessaging };
|
|
11
|
+
|
|
12
|
+
/** Explicit configuration; a missing/failed provider never selects another path. */
|
|
13
|
+
export function createMailSender(config, dependencies = {}) {
|
|
14
|
+
validateConfig(config, dependencies);
|
|
15
|
+
let configured;
|
|
16
|
+
try { configured = Object.freeze(structuredClone(config)); } catch { fail('invalid_config'); }
|
|
17
|
+
const host = Object.freeze({ ...dependencies });
|
|
18
|
+
const transport = transports[configured.provider];
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
...(configured.provider === 'messaging' ? {
|
|
21
|
+
reconcile: deliveryId => reconcileMessaging(configured, deliveryId, host),
|
|
22
|
+
erase: address => eraseMessaging(configured, address, host),
|
|
23
|
+
} : {}),
|
|
24
|
+
async send(message) {
|
|
25
|
+
validateMessage(message, configured.provider);
|
|
26
|
+
let snapshot;
|
|
27
|
+
try { snapshot = structuredClone(message); } catch { fail('invalid_message'); }
|
|
28
|
+
const prepared = applyUnsubscribeHeaders(snapshot);
|
|
29
|
+
validateMessage(prepared, configured.provider);
|
|
30
|
+
return validateSendResult(await transport(configured, prepared, host), configured.provider);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SendGridMessage {
|
|
2
|
+
readonly from: string
|
|
3
|
+
readonly to: readonly string[]
|
|
4
|
+
readonly subject: string
|
|
5
|
+
readonly text: string
|
|
6
|
+
readonly html?: string | null
|
|
7
|
+
readonly replyTo?: string
|
|
8
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
9
|
+
/** `content` is a base64 string. `type` defaults to application/octet-stream. */
|
|
10
|
+
readonly attachments?: readonly { readonly content: string; readonly filename: string; readonly type?: string }[]
|
|
11
|
+
readonly asmGroupId?: number
|
|
12
|
+
readonly subscriptionTracking?: boolean
|
|
13
|
+
readonly customArgs?: Readonly<Record<string, string>>
|
|
14
|
+
/** Durable local command ID for correlation; SendGrid does not deduplicate it. */
|
|
15
|
+
readonly deliveryId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SendGridSendError extends Error {
|
|
19
|
+
readonly outcome: 'rejected' | 'rate_limited' | 'ambiguous'
|
|
20
|
+
readonly status: number | undefined
|
|
21
|
+
constructor(outcome: 'rejected' | 'rate_limited' | 'ambiguous', message: string, status?: number)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Validated wire envelope, also usable as a durable claim fingerprint. */
|
|
25
|
+
export function serializeSendGridMessage(message: SendGridMessage): string
|
|
26
|
+
|
|
27
|
+
/** One attempt only. Acceptance and a provider receipt are distinct evidence. */
|
|
28
|
+
export function sendSendGrid(input: {
|
|
29
|
+
readonly apiKey: string
|
|
30
|
+
readonly message: SendGridMessage
|
|
31
|
+
readonly fetch?: typeof globalThis.fetch
|
|
32
|
+
readonly timeoutMilliseconds?: number
|
|
33
|
+
}): Promise<{ provider: 'sendgrid'; providerMessageId: string | null; state: 'accepted' }>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MailValidationError } from '../core/errors.mjs';
|
|
2
|
+
import { sendSendGrid as submit, serializeSendGridMessage as serialize } from '../adapters/sendgrid/index.mjs';
|
|
3
|
+
|
|
4
|
+
// The API of the former `modules/sendgrid-email` module, kept for existing adopters.
|
|
5
|
+
// It owns no transport: every request is built and sent by the one SendGrid adapter
|
|
6
|
+
// in this module, so both import paths put identical requests on the wire.
|
|
7
|
+
|
|
8
|
+
export class SendGridSendError extends Error {
|
|
9
|
+
constructor(outcome, message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SendGridSendError';
|
|
12
|
+
this.outcome = outcome;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rejected(message) { return new SendGridSendError('rejected', message); }
|
|
18
|
+
|
|
19
|
+
// Diagnostics carry only the fixed validation code, never input values.
|
|
20
|
+
function invalid(error) {
|
|
21
|
+
if (error instanceof MailValidationError) return rejected(`SendGrid message or key is invalid (${error.code}).`);
|
|
22
|
+
return rejected('SendGrid message or key is invalid.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeAttachment(attachment) {
|
|
26
|
+
const content = attachment?.content;
|
|
27
|
+
if (typeof content !== 'string' || !content || !/^[A-Za-z0-9+/\s]*={0,2}\s*$/.test(content)) throw rejected('Attachment content must be base64.');
|
|
28
|
+
let binary;
|
|
29
|
+
try { binary = atob(content); } catch { throw rejected('Attachment content must be base64.'); }
|
|
30
|
+
return {
|
|
31
|
+
filename: attachment.filename,
|
|
32
|
+
contentType: attachment.type ?? 'application/octet-stream',
|
|
33
|
+
content: Uint8Array.from(binary, character => character.charCodeAt(0)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only the former module's fields are read, so unknown fields stay ignored as before.
|
|
38
|
+
function toMailMessage(message) {
|
|
39
|
+
if (!message || typeof message !== 'object') throw rejected('SendGrid message is required.');
|
|
40
|
+
const mapped = { deliveryId: message.deliveryId, from: message.from, to: message.to, subject: message.subject, text: message.text };
|
|
41
|
+
if (message.html) mapped.html = message.html;
|
|
42
|
+
if (message.replyTo) mapped.replyTo = message.replyTo;
|
|
43
|
+
for (const field of ['headers', 'customArgs', 'asmGroupId', 'subscriptionTracking']) {
|
|
44
|
+
if (message[field] !== undefined) mapped[field] = message[field];
|
|
45
|
+
}
|
|
46
|
+
if (message.attachments?.length) mapped.attachments = message.attachments.map(decodeAttachment);
|
|
47
|
+
return mapped;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validated wire envelope, also usable as a durable claim fingerprint. */
|
|
51
|
+
export function serializeSendGridMessage(message) {
|
|
52
|
+
try { return serialize(toMailMessage(message)); } catch (error) {
|
|
53
|
+
throw error instanceof SendGridSendError ? error : invalid(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One attempt only. Acceptance and a provider receipt are distinct evidence. */
|
|
58
|
+
export async function sendSendGrid(input) {
|
|
59
|
+
if (typeof input?.apiKey !== 'string' || !input.apiKey.trim()) throw rejected('SendGrid API key is required.');
|
|
60
|
+
const message = toMailMessage(input.message);
|
|
61
|
+
const baseFetch = input.fetch ?? globalThis.fetch;
|
|
62
|
+
let httpStatus;
|
|
63
|
+
// The adapter result classifies the outcome; the exact HTTP status is kept for callers.
|
|
64
|
+
const observedFetch = async (url, init) => {
|
|
65
|
+
const response = await baseFetch(url, init);
|
|
66
|
+
httpStatus = response?.status;
|
|
67
|
+
return response;
|
|
68
|
+
};
|
|
69
|
+
const dependencies = { fetch: observedFetch };
|
|
70
|
+
if (input.timeoutMilliseconds !== undefined) dependencies.timeoutMilliseconds = input.timeoutMilliseconds;
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
result = await submit({ provider: 'sendgrid', mode: 'production', apiKey: input.apiKey }, message, dependencies);
|
|
74
|
+
} catch (error) { throw invalid(error); }
|
|
75
|
+
switch (result.status) {
|
|
76
|
+
case 'accepted':
|
|
77
|
+
return { provider: 'sendgrid', providerMessageId: result.providerMessageId, state: 'accepted' };
|
|
78
|
+
case 'rate_limited':
|
|
79
|
+
throw new SendGridSendError('rate_limited', 'SendGrid rate limited this submission.', httpStatus);
|
|
80
|
+
case 'rejected':
|
|
81
|
+
throw new SendGridSendError('rejected', `SendGrid rejected this submission (${httpStatus}).`, httpStatus);
|
|
82
|
+
default:
|
|
83
|
+
if (httpStatus === undefined) throw new SendGridSendError('ambiguous', 'SendGrid submission did not return a receipt; reconcile before resending.');
|
|
84
|
+
throw new SendGridSendError('ambiguous', `SendGrid returned an unconfirmed submission status (${httpStatus}); reconcile before resending.`, httpStatus);
|
|
85
|
+
}
|
|
86
|
+
}
|