@runeditrun/rer 0.0.0-stage → 0.1.1
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 +157 -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 +182 -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 +158 -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 +117 -0
- package/global/launch/facts.mjs +94 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +63 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +106 -0
- package/global/launch/steps.mjs +90 -0
- package/global/launch/stop.mjs +89 -0
- package/global/launch/worker.mjs +62 -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,47 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { encodeBase64, parseAddress, validateConfig, validateMessage } from '../../core/validation.mjs';
|
|
3
|
+
import { classifyHttpStatus, requestProvider, sendResult } from '../../core/transport.mjs';
|
|
4
|
+
import { validateCloudflareHeaders } from './headers.mjs';
|
|
5
|
+
import { cloudflareResult, readCloudflareResponse } from './response.mjs';
|
|
6
|
+
|
|
7
|
+
function address(value) {
|
|
8
|
+
const { email, name } = parseAddress(value);
|
|
9
|
+
return name === undefined ? email : { address: email, name };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Cloudflare Email Sending REST API, independent of Email Routing send bindings. */
|
|
13
|
+
export async function sendCloudflare(config, message, dependencies = {}) {
|
|
14
|
+
validateConfig(config, dependencies);
|
|
15
|
+
if (config.provider !== 'cloudflare') fail('cloudflare_provider_required');
|
|
16
|
+
if (message && Object.hasOwn(message, 'unsubscribe')) fail('unsubscribe_requires_facade');
|
|
17
|
+
validateMessage(message, 'cloudflare');
|
|
18
|
+
validateCloudflareHeaders(message.headers);
|
|
19
|
+
const body = { from: address(message.from), to: message.to.map(address), subject: message.subject };
|
|
20
|
+
for (const field of ['text', 'html', 'headers']) if (message[field] !== undefined) body[field] = message[field];
|
|
21
|
+
for (const field of ['cc', 'bcc']) if (message[field] !== undefined) body[field] = message[field].map(address);
|
|
22
|
+
if (message.replyTo !== undefined) body.reply_to = address(message.replyTo);
|
|
23
|
+
if (message.attachments !== undefined) body.attachments = message.attachments.map(attachment => {
|
|
24
|
+
// The structured REST schema permits content_id only on inline attachments.
|
|
25
|
+
if (attachment.contentId !== undefined && attachment.disposition !== 'inline') fail('cloudflare_unsupported_attachment_content_id');
|
|
26
|
+
return {
|
|
27
|
+
filename: attachment.filename, type: attachment.contentType,
|
|
28
|
+
content: encodeBase64(attachment.content), disposition: attachment.disposition ?? 'attachment',
|
|
29
|
+
...(attachment.contentId === undefined ? {} : { content_id: attachment.contentId }),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const serialized = JSON.stringify(body);
|
|
33
|
+
if (new TextEncoder().encode(serialized).byteLength > 5 * 1024 * 1024) fail('cloudflare_message_too_large');
|
|
34
|
+
const recipients = [...message.to, ...(message.cc ?? []), ...(message.bcc ?? [])].map(value => parseAddress(value).email);
|
|
35
|
+
const response = await requestProvider(
|
|
36
|
+
`https://api.cloudflare.com/client/v4/accounts/${config.accountId}/email/sending/send`,
|
|
37
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, body: serialized },
|
|
38
|
+
dependencies,
|
|
39
|
+
);
|
|
40
|
+
if (response === null) return sendResult('cloudflare', 'uncertain', null, 'cloudflare_transport_failed');
|
|
41
|
+
const status = classifyHttpStatus(response.status);
|
|
42
|
+
if (status !== null) {
|
|
43
|
+
void response.body?.cancel().catch(() => {});
|
|
44
|
+
return sendResult('cloudflare', status, null, 'cloudflare_http_error');
|
|
45
|
+
}
|
|
46
|
+
return cloudflareResult(await readCloudflareResponse(response), recipients);
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { safeProviderMessageId, sendResult } from '../../core/transport.mjs';
|
|
2
|
+
import { parseAddress } from '../../core/validation.mjs';
|
|
3
|
+
|
|
4
|
+
const provider = 'cloudflare';
|
|
5
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
// The domain is case-insensitive; do not collapse distinct case-sensitive local parts.
|
|
7
|
+
export function mailboxKey(email) {
|
|
8
|
+
const at = email.lastIndexOf('@');
|
|
9
|
+
return email.slice(0, at + 1) + email.slice(at + 1).toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function cloudflareResult(body, recipients) {
|
|
13
|
+
const unknown = () => sendResult(provider, 'uncertain', null, 'cloudflare_invalid_response');
|
|
14
|
+
if (!record(body) || typeof body.success !== 'boolean' || !Array.isArray(body.errors)) return unknown();
|
|
15
|
+
if (body.success === false) {
|
|
16
|
+
// A contradictory result may reflect a send that already took effect.
|
|
17
|
+
if (body.result !== null && body.result !== undefined) return unknown();
|
|
18
|
+
const codes = body.errors.map(error => error?.code);
|
|
19
|
+
const status = codes.some(code => [10002, 10003, 10100].includes(code)) ? 'uncertain'
|
|
20
|
+
: codes.includes(10004) ? 'rate_limited' : 'rejected';
|
|
21
|
+
return sendResult(provider, status, null, 'cloudflare_provider_error');
|
|
22
|
+
}
|
|
23
|
+
if (body.errors.length || !record(body.result)) return unknown();
|
|
24
|
+
const result = body.result;
|
|
25
|
+
const id = safeProviderMessageId(result.message_id);
|
|
26
|
+
if (result.message_id !== undefined && result.message_id !== null && id === null) return unknown();
|
|
27
|
+
const groups = [
|
|
28
|
+
['delivered', 'accepted'], ['queued', 'accepted'],
|
|
29
|
+
['permanent_bounces', 'rejected'], ['suppressed_recipients', 'rejected'],
|
|
30
|
+
];
|
|
31
|
+
const outcomes = new Map();
|
|
32
|
+
const requested = new Map(recipients.map(recipient => [mailboxKey(recipient), recipient]));
|
|
33
|
+
let malformed = false;
|
|
34
|
+
for (const [field, status] of groups) {
|
|
35
|
+
const list = result[field];
|
|
36
|
+
if (field === 'suppressed_recipients' && list === undefined) continue;
|
|
37
|
+
if (!Array.isArray(list)) { malformed = true; continue; }
|
|
38
|
+
for (const value of list) {
|
|
39
|
+
let email;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = parseAddress(value);
|
|
42
|
+
if (parsed.name || parsed.email !== value) { malformed = true; continue; }
|
|
43
|
+
email = mailboxKey(parsed.email);
|
|
44
|
+
} catch { malformed = true; continue; }
|
|
45
|
+
if (!requested.has(email)) { malformed = true; continue; }
|
|
46
|
+
if (outcomes.has(email)) {
|
|
47
|
+
// Even repeated identical groups have ambiguous recipient cardinality.
|
|
48
|
+
outcomes.set(email, 'uncertain');
|
|
49
|
+
malformed = true;
|
|
50
|
+
} else outcomes.set(email, status);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const recipientResults = [...requested].map(([key, recipient]) => ({
|
|
54
|
+
recipient, status: outcomes.get(key) ?? 'uncertain',
|
|
55
|
+
}));
|
|
56
|
+
if (malformed || recipientResults.some(item => item.status === 'uncertain')) {
|
|
57
|
+
return sendResult(provider, 'uncertain', id, 'cloudflare_incomplete_recipient_results', recipientResults);
|
|
58
|
+
}
|
|
59
|
+
const accepted = recipientResults.filter(item => item.status === 'accepted').length;
|
|
60
|
+
if (accepted === recipientResults.length) return sendResult(provider, 'accepted', id, undefined, recipientResults);
|
|
61
|
+
if (accepted === 0) return sendResult(provider, 'rejected', id, 'cloudflare_recipients_rejected', recipientResults);
|
|
62
|
+
// The common result union has no partial status. Never encourage retrying the batch.
|
|
63
|
+
return sendResult(provider, 'uncertain', id, 'cloudflare_partial_recipient_failure', recipientResults);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readCloudflareResponse(response) {
|
|
67
|
+
// Bound both bytes and time after response headers; errors never expose the body.
|
|
68
|
+
let reader;
|
|
69
|
+
try { reader = response.body?.getReader(); } catch { return null; }
|
|
70
|
+
if (!reader) return null;
|
|
71
|
+
let timer;
|
|
72
|
+
let finished = false;
|
|
73
|
+
const read = async () => {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let length = 0;
|
|
76
|
+
while (true) {
|
|
77
|
+
const { done, value } = await reader.read();
|
|
78
|
+
if (finished) return null;
|
|
79
|
+
if (done) break;
|
|
80
|
+
length += value.byteLength;
|
|
81
|
+
if (length > 1024 * 1024) return null;
|
|
82
|
+
chunks.push(value);
|
|
83
|
+
}
|
|
84
|
+
const bytes = new Uint8Array(length);
|
|
85
|
+
let offset = 0;
|
|
86
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
87
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
return await Promise.race([
|
|
91
|
+
read().catch(() => null),
|
|
92
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), 15_000); }),
|
|
93
|
+
]);
|
|
94
|
+
} finally {
|
|
95
|
+
finished = true;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
void reader.cancel().catch(() => {});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Messaging v1 wire and Base client
|
|
2
|
+
|
|
3
|
+
Normative phase-1 wire choices for independent implementations and the concurrent
|
|
4
|
+
Messaging core. Linking, production credentials and deployment are separate.
|
|
5
|
+
|
|
6
|
+
`createMailSender({provider:'messaging', mode, serviceOrigin, installId, kid,
|
|
7
|
+
signingKey}, {fetch?, serviceBinding?, clock?, timeoutMilliseconds?})` exposes
|
|
8
|
+
`send(message)`, `reconcile(deliveryId)`, and `erase(address)`. `signingKey` is a
|
|
9
|
+
private ES256 P-256 JWK; no API key or direct provider settings are allowed.
|
|
10
|
+
Configuration and each message are snapshotted before asynchronous work.
|
|
11
|
+
`serviceOrigin` is an exact HTTPS origin (HTTP loopback only in test/development).
|
|
12
|
+
A `serviceBinding` is a Worker Fetcher with `fetch`; it replaces the HTTP edge,
|
|
13
|
+
never signature authentication. Do not configure both fetch and serviceBinding.
|
|
14
|
+
|
|
15
|
+
Every request has `RER-Signature: <compact JWS>`. Protected header is
|
|
16
|
+
`{alg:'ES256',kid,typ:'JWT'}`. Claims are `{iss:installId,aud:serviceOrigin,
|
|
17
|
+
iat:<integer epoch seconds>,method,path,bodySha256:<lowercase SHA-256 hex>}`.
|
|
18
|
+
The signature is the JOSE 64-byte P1363 ECDSA signature. Hash the exact UTF-8
|
|
19
|
+
request bytes; GET hashes zero bytes. Path is the URL's encoded pathname,
|
|
20
|
+
including `encodeURIComponent(deliveryId)` for reconciliation. Request verification
|
|
21
|
+
permits iat within +/-60 seconds. Never follow redirects or retry automatically.
|
|
22
|
+
|
|
23
|
+
`POST /v1/mail`: JSON fields `deliveryId`, required `kind` (`auth`,
|
|
24
|
+
`transactional`, `bulk`), `from`, `to` (ONE address string, not array), `subject`,
|
|
25
|
+
optional `replyTo`, `text`, `html`, `headers`, `attachments`, `expiresAt`.
|
|
26
|
+
Base's public message still uses a one-element `to` array. `cc`/`bcc` are refused.
|
|
27
|
+
Messaging delivery IDs retain Base's 512-character limit and must have no
|
|
28
|
+
surrounding whitespace, so sends can be reconciled and correlated with events.
|
|
29
|
+
Internal spaces, Unicode, slashes, query and fragment characters are preserved;
|
|
30
|
+
`.` and `..` and strings that cannot be URL-encoded are refused before dispatch.
|
|
31
|
+
Address strings preserve display names and local-part case. `expiresAt` is an
|
|
32
|
+
RFC3339 timestamp; Base carries it only for Messaging (other transports cannot
|
|
33
|
+
promise expiry). Attachments are `{filename,contentType,content:<canonical padded
|
|
34
|
+
base64>,disposition?,contentId?}`; no byte conversion through text. Text plus HTML
|
|
35
|
+
is at most 1 MiB UTF-8; decoded attachments total at most 5 MiB; entire JSON body
|
|
36
|
+
at most 8 MiB. Headers are Base's allowed subset of `List-*`, `Auto-Submitted`,
|
|
37
|
+
`In-Reply-To`, `References`, `Message-ID`, `X-*`; `replyTo` supplies Reply-To.
|
|
38
|
+
Base applies its unsubscribe assembly before signing.
|
|
39
|
+
|
|
40
|
+
The send response is the JSON result itself, without an envelope:
|
|
41
|
+
`{provider:'messaging',status,providerMessageId,code?,suppression?,retryAfter?}`.
|
|
42
|
+
Accepted has a nonempty Messaging message id and no code. Development has null
|
|
43
|
+
providerMessageId and no code. All other states have null providerMessageId.
|
|
44
|
+
Rejected codes: suppressed, kind_not_granted, sender_not_granted, app_paused,
|
|
45
|
+
invalid, unsubscribe_missing, expired, provider_rejected_recipient, key_reused.
|
|
46
|
+
Only suppressed requires `suppression: hard_bounce|complaint|provider`.
|
|
47
|
+
Rate-limited codes: app_cap, recipient_limit, provider_rate_limited,
|
|
48
|
+
providers_unavailable; these require `retryAfter`, an integer epoch timestamp in
|
|
49
|
+
seconds (schedule at this time, not a duration). Uncertain codes: ambiguous,
|
|
50
|
+
in_progress, orphaned; in_progress requires retryAfter, other uncertain codes
|
|
51
|
+
may carry it. No other fields or status/code combinations are accepted.
|
|
52
|
+
Application responses are HTTP 200. HTTP errors, malformed/overlong response
|
|
53
|
+
bodies, timeouts and network failures map to uncertain/ambiguous without echoing
|
|
54
|
+
response fields or issuing a second request. The default client deadline is 15s,
|
|
55
|
+
including body reading; Messaging's own send budget is 10s.
|
|
56
|
+
|
|
57
|
+
`GET /v1/messages/{deliveryId}` sends no body. HTTP 200 returns
|
|
58
|
+
`{deliveryId,messageId,state,result,attempts,lastEvent}`; state is sending or a
|
|
59
|
+
send-result status, result is null while sending and otherwise the above result.
|
|
60
|
+
Attempts are `{provider,outcome,httpStatus,code,durationMs}`: provider is resend,
|
|
61
|
+
sendgrid, cloudflare or development; outcome is started, accepted, rejected,
|
|
62
|
+
rate_limited or uncertain; HTTP status and code may be null; durationMs is a
|
|
63
|
+
nonnegative integer or null. `lastEvent` is null or the event object below.
|
|
64
|
+
An authenticated 404 returns null. Other failures throw a fixed redacted
|
|
65
|
+
MailValidationError; they do not imply a send outcome.
|
|
66
|
+
|
|
67
|
+
`POST /v1/erase` sends exactly `{address:<bare mailbox preserving case>}`;
|
|
68
|
+
HTTP 200 returns `{erased:true}`. Erasure is idempotent and scoped by signed iss.
|
|
69
|
+
Failures throw a fixed redacted error; a timeout does not prove erasure failed.
|
|
70
|
+
|
|
71
|
+
## Event wire
|
|
72
|
+
|
|
73
|
+
The raw POST body is the compact JWS itself (UTF-8), not JSON wrapping it.
|
|
74
|
+
Protected header is `{alg:'ES256',kid,typ:'JWT'}`. Payload is
|
|
75
|
+
`{iss:serviceOrigin,aud:installId,iat:<integer epoch seconds>,events:[...]}`.
|
|
76
|
+
Each event is `{id,messageId,deliveryId,recipient,occurredAt,type,detail}`.
|
|
77
|
+
Recipient is a bare mailbox with local-part case preserved; occurredAt is RFC3339.
|
|
78
|
+
Types: accepted, delivered, deferred, bounced, complained, failed, suppressed.
|
|
79
|
+
`detail` is a closed object, with optional `bounce: 'hard'|'soft'`,
|
|
80
|
+
`suppression: 'hard_bounce'|'complaint'|'provider'`, and `code` (a bounded machine
|
|
81
|
+
code matching `[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}`). Bounce classification is allowed
|
|
82
|
+
only on bounced; suppression only on suppressed. No free-form provider text.
|
|
83
|
+
|
|
84
|
+
Intake uses `messagingSource:{serviceOrigin,installId,jwks:{keys:[publicJwk,...]}}`
|
|
85
|
+
pinned at link time. No remote key lookup, jku, x5u, embedded jwk, algorithm
|
|
86
|
+
negotiation or unsigned JSON ingestion. Key rotation replaces the configured
|
|
87
|
+
pinned set. Signature, issuer, audience and +/-60s iat are checked before events
|
|
88
|
+
have any effect. Each normalized record carries `eventId:id`,
|
|
89
|
+
`providerMessageId:messageId`, `deliveryId`, `recipients:[recipient]`, `detail`,
|
|
90
|
+
and `status:type` except suppressed maps to rejected. Stable receipts fingerprint
|
|
91
|
+
the validated event fields in fixed order, independent of batch order, envelope
|
|
92
|
+
clock, signing key rotation or JSON property order. A changed field conflicts.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
|
|
3
|
+
export function closed(value, fields, code = 'invalid_messaging_response') {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
5
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
|
|
6
|
+
Reflect.ownKeys(value).some(key => typeof key !== 'string' || !fields.includes(key))) fail(code);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function identity(value, max = 512, code = 'invalid_messaging_identity') {
|
|
10
|
+
if (typeof value !== 'string' || !value.length || value.trim() !== value || value.length > max || /[\x00-\x1f\x7f]/.test(value)) fail(code);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function serviceOrigin(value, mode = 'production') {
|
|
14
|
+
let url;
|
|
15
|
+
try { url = new URL(value); } catch { fail('invalid_messaging_origin'); }
|
|
16
|
+
const loopback = url.hostname === 'localhost' || url.hostname === '[::1]' || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
17
|
+
if (url.origin !== value || url.username || url.password ||
|
|
18
|
+
!(url.protocol === 'https:' || (mode !== 'production' && url.protocol === 'http:' && loopback))) fail('invalid_messaging_origin');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function rfc3339(value, code = 'invalid_messaging_timestamp') {
|
|
22
|
+
if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(value)) fail(code);
|
|
23
|
+
const day = new Date(`${value.slice(0, 10)}T00:00:00.000Z`);
|
|
24
|
+
const time = Date.parse(value);
|
|
25
|
+
if (!Number.isSafeInteger(time) || time < 0 || !Number.isFinite(day.getTime()) ||
|
|
26
|
+
day.toISOString().slice(0, 10) !== value.slice(0, 10) ||
|
|
27
|
+
Number(value.slice(11, 13)) > 23 || Number(value.slice(14, 16)) > 59 || Number(value.slice(17, 19)) > 59 ||
|
|
28
|
+
(value.endsWith('Z') === false && (Number(value.slice(-5, -3)) > 23 || Number(value.slice(-2)) > 59))) fail(code);
|
|
29
|
+
return time;
|
|
30
|
+
}
|
|
31
|
+
export function validateMessagingConfig(config, dependencies) {
|
|
32
|
+
serviceOrigin(config.serviceOrigin, config.mode);
|
|
33
|
+
identity(config.installId, 256, 'invalid_messaging_install');
|
|
34
|
+
identity(config.kid, 256, 'invalid_messaging_key');
|
|
35
|
+
if (config.apiKey !== undefined || config.accountId !== undefined) fail('unsupported_messaging_config');
|
|
36
|
+
const jwk = config.signingKey;
|
|
37
|
+
if (!jwk || jwk.kty !== 'EC' || jwk.crv !== 'P-256' ||
|
|
38
|
+
!['x', 'y', 'd'].every(field => typeof jwk[field] === 'string' && /^[A-Za-z0-9_-]{43}$/.test(jwk[field])) ||
|
|
39
|
+
(jwk.alg !== undefined && jwk.alg !== 'ES256') || (jwk.use !== undefined && jwk.use !== 'sig') ||
|
|
40
|
+
(jwk.key_ops !== undefined && (!Array.isArray(jwk.key_ops) || jwk.key_ops.length !== 1 || jwk.key_ops[0] !== 'sign')) ||
|
|
41
|
+
(jwk.kid !== undefined && jwk.kid !== config.kid)) fail('invalid_messaging_key');
|
|
42
|
+
if (dependencies.serviceBinding !== undefined && (!dependencies.serviceBinding || typeof dependencies.serviceBinding.fetch !== 'function')) fail('invalid_service_binding');
|
|
43
|
+
if (dependencies.serviceBinding !== undefined && dependencies.fetch !== undefined) fail('ambiguous_messaging_transport');
|
|
44
|
+
}
|
|
45
|
+
const codes = {
|
|
46
|
+
rejected: ['suppressed', 'kind_not_granted', 'sender_not_granted', 'app_paused', 'invalid', 'unsubscribe_missing', 'expired', 'provider_rejected_recipient', 'key_reused'],
|
|
47
|
+
rate_limited: ['app_cap', 'recipient_limit', 'provider_rate_limited', 'providers_unavailable'],
|
|
48
|
+
uncertain: ['ambiguous', 'in_progress', 'orphaned'],
|
|
49
|
+
};
|
|
50
|
+
export function validateMessagingResult(result) {
|
|
51
|
+
const invalid = 'invalid_send_result';
|
|
52
|
+
closed(result, ['provider', 'status', 'providerMessageId', 'code', 'suppression', 'retryAfter'], invalid);
|
|
53
|
+
if (result.provider !== 'messaging' || typeof result.status !== 'string') fail(invalid);
|
|
54
|
+
if (result.status === 'accepted') identity(result.providerMessageId, 1024, invalid);
|
|
55
|
+
else if (result.providerMessageId !== null) fail(invalid);
|
|
56
|
+
if (['accepted', 'development'].includes(result.status)) {
|
|
57
|
+
if (Object.hasOwn(result, 'code')) fail(invalid);
|
|
58
|
+
} else if (!(Object.hasOwn(codes, result.status) && codes[result.status].includes(result.code))) fail(invalid);
|
|
59
|
+
if (result.code === 'suppressed') {
|
|
60
|
+
if (!['hard_bounce', 'complaint', 'provider'].includes(result.suppression)) fail(invalid);
|
|
61
|
+
} else if (Object.hasOwn(result, 'suppression')) fail(invalid);
|
|
62
|
+
const retryable = ['rate_limited', 'uncertain'].includes(result.status);
|
|
63
|
+
if (Object.hasOwn(result, 'retryAfter') && (!retryable || !Number.isSafeInteger(result.retryAfter) || result.retryAfter < 0)) fail(invalid);
|
|
64
|
+
if ((result.status === 'rate_limited' || result.code === 'in_progress') && result.retryAfter === undefined) fail(invalid);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { parseAddress } from '../../core/validation.mjs';
|
|
3
|
+
import { closed, identity, rfc3339 } from './contract.mjs';
|
|
4
|
+
|
|
5
|
+
export function messagingEvent(value, now) {
|
|
6
|
+
closed(value, ['id', 'messageId', 'deliveryId', 'recipient', 'occurredAt', 'type', 'detail'], 'invalid_event_schema');
|
|
7
|
+
const id = identity(value.id), messageId = identity(value.messageId, 1024), deliveryId = identity(value.deliveryId);
|
|
8
|
+
const parsed = parseAddress(value.recipient);
|
|
9
|
+
if (parsed.name || parsed.email !== value.recipient) fail('invalid_event_recipient');
|
|
10
|
+
const occurredAt = rfc3339(value.occurredAt, 'invalid_event_timestamp');
|
|
11
|
+
if (occurredAt > now + 300_000) fail('invalid_event_timestamp');
|
|
12
|
+
if (!['accepted', 'delivered', 'deferred', 'bounced', 'complained', 'failed', 'suppressed'].includes(value.type)) fail('unsupported_delivery_event');
|
|
13
|
+
closed(value.detail, ['bounce', 'suppression', 'code'], 'invalid_event_detail');
|
|
14
|
+
const detail = {};
|
|
15
|
+
if (Object.hasOwn(value.detail, 'bounce')) {
|
|
16
|
+
if (value.type !== 'bounced' || !['hard', 'soft'].includes(value.detail.bounce)) fail('invalid_event_detail');
|
|
17
|
+
detail.bounce = value.detail.bounce;
|
|
18
|
+
}
|
|
19
|
+
if (Object.hasOwn(value.detail, 'suppression')) {
|
|
20
|
+
if (value.type !== 'suppressed' || !['hard_bounce', 'complaint', 'provider'].includes(value.detail.suppression)) fail('invalid_event_detail');
|
|
21
|
+
detail.suppression = value.detail.suppression;
|
|
22
|
+
}
|
|
23
|
+
if (Object.hasOwn(value.detail, 'code')) {
|
|
24
|
+
if (typeof value.detail.code !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(value.detail.code)) fail('invalid_event_detail');
|
|
25
|
+
detail.code = value.detail.code;
|
|
26
|
+
}
|
|
27
|
+
return { id, messageId, deliveryId, recipient: value.recipient, occurredAt: value.occurredAt, type: value.type, detail };
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { parseAddress } from '../../core/validation.mjs';
|
|
3
|
+
import { closed, identity, validateMessagingResult } from './contract.mjs';
|
|
4
|
+
import { serializeMessagingMessage } from './serialize.mjs';
|
|
5
|
+
import { messagingRequest } from './request.mjs';
|
|
6
|
+
import { messagingEvent } from './event-schema.mjs';
|
|
7
|
+
|
|
8
|
+
const ambiguous = () => ({ provider: 'messaging', status: 'uncertain', providerMessageId: null, code: 'ambiguous' });
|
|
9
|
+
export async function sendMessaging(config, message, dependencies) {
|
|
10
|
+
const bytes = serializeMessagingMessage(message);
|
|
11
|
+
const response = await messagingRequest(config, dependencies, 'POST', '/v1/mail', bytes);
|
|
12
|
+
if (response?.status !== 200) return ambiguous();
|
|
13
|
+
try { return validateMessagingResult(response.value); } catch { return ambiguous(); }
|
|
14
|
+
}
|
|
15
|
+
export async function reconcileMessaging(config, deliveryId, dependencies) {
|
|
16
|
+
identity(deliveryId, 512, 'invalid_delivery_id');
|
|
17
|
+
// Dot-only segments are normalized by URL/fetch even when percent encoded.
|
|
18
|
+
if (deliveryId === '.' || deliveryId === '..') fail('invalid_delivery_id');
|
|
19
|
+
let segment;
|
|
20
|
+
try { segment = encodeURIComponent(deliveryId); } catch { fail('invalid_delivery_id'); }
|
|
21
|
+
const response = await messagingRequest(config, dependencies, 'GET', `/v1/messages/${segment}`);
|
|
22
|
+
if (response?.status === 404) return null;
|
|
23
|
+
try {
|
|
24
|
+
if (response?.status !== 200) throw new Error();
|
|
25
|
+
const value = closed(response.value, ['deliveryId', 'messageId', 'state', 'result', 'attempts', 'lastEvent']);
|
|
26
|
+
if (value.deliveryId !== deliveryId) throw new Error();
|
|
27
|
+
identity(value.messageId, 1024);
|
|
28
|
+
if (value.state === 'sending') { if (value.result !== null) throw new Error(); }
|
|
29
|
+
else {
|
|
30
|
+
validateMessagingResult(value.result);
|
|
31
|
+
if (value.state !== value.result.status || (value.result.status === 'accepted' && value.result.providerMessageId !== value.messageId)) throw new Error();
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(value.attempts) || value.attempts.length > 1000) throw new Error();
|
|
34
|
+
for (const attempt of value.attempts) {
|
|
35
|
+
closed(attempt, ['provider', 'outcome', 'httpStatus', 'code', 'durationMs']);
|
|
36
|
+
if (!['resend', 'sendgrid', 'cloudflare', 'development'].includes(attempt.provider) ||
|
|
37
|
+
!['started', 'accepted', 'rejected', 'rate_limited', 'uncertain'].includes(attempt.outcome) ||
|
|
38
|
+
!(attempt.httpStatus === null || (Number.isInteger(attempt.httpStatus) && attempt.httpStatus >= 100 && attempt.httpStatus <= 599)) ||
|
|
39
|
+
!(attempt.code === null || (typeof attempt.code === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(attempt.code))) ||
|
|
40
|
+
!(attempt.durationMs === null || (Number.isSafeInteger(attempt.durationMs) && attempt.durationMs >= 0))) throw new Error();
|
|
41
|
+
}
|
|
42
|
+
if (value.lastEvent !== null) {
|
|
43
|
+
const event = messagingEvent(value.lastEvent, (dependencies.clock ?? Date.now)());
|
|
44
|
+
if (event.messageId !== value.messageId || event.deliveryId !== deliveryId) throw new Error();
|
|
45
|
+
value.lastEvent = event;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
} catch { fail('messaging_reconciliation_failed'); }
|
|
49
|
+
}
|
|
50
|
+
export async function eraseMessaging(config, address, dependencies) {
|
|
51
|
+
const parsed = parseAddress(address);
|
|
52
|
+
if (parsed.name || parsed.email !== address) fail('invalid_erase_address');
|
|
53
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ address }));
|
|
54
|
+
const response = await messagingRequest(config, dependencies, 'POST', '/v1/erase', bytes);
|
|
55
|
+
try {
|
|
56
|
+
if (response?.status !== 200) throw new Error();
|
|
57
|
+
const value = closed(response.value, ['erased']);
|
|
58
|
+
if (value.erased !== true) throw new Error();
|
|
59
|
+
return value;
|
|
60
|
+
} catch { fail('messaging_erasure_failed'); }
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { identity, closed } from './contract.mjs';
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
export function base64url(bytes) {
|
|
6
|
+
let binary = '';
|
|
7
|
+
for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
8
|
+
return btoa(binary).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
9
|
+
}
|
|
10
|
+
function decode(value) {
|
|
11
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error();
|
|
12
|
+
const bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
|
13
|
+
if (base64url(bytes) !== value) throw new Error();
|
|
14
|
+
return bytes;
|
|
15
|
+
}
|
|
16
|
+
function json(value) { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(decode(value))); }
|
|
17
|
+
export async function bodyDigest(bytes) {
|
|
18
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), value => value.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
}
|
|
20
|
+
export async function signRequest(config, method, path, bytes, now) {
|
|
21
|
+
try {
|
|
22
|
+
if (!Number.isSafeInteger(now) || now < 0) fail('invalid_messaging_clock');
|
|
23
|
+
const protectedPart = base64url(encoder.encode(JSON.stringify({ alg: 'ES256', kid: config.kid, typ: 'JWT' })));
|
|
24
|
+
const claims = { iss: config.installId, aud: config.serviceOrigin, iat: Math.floor(now / 1000), method, path, bodySha256: await bodyDigest(bytes) };
|
|
25
|
+
const payload = base64url(encoder.encode(JSON.stringify(claims)));
|
|
26
|
+
const key = await crypto.subtle.importKey('jwk', config.signingKey, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
27
|
+
const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, encoder.encode(`${protectedPart}.${payload}`));
|
|
28
|
+
return `${protectedPart}.${payload}.${base64url(new Uint8Array(signature))}`;
|
|
29
|
+
} catch { fail('messaging_signing_failed'); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function pinnedKeys(jwks) {
|
|
33
|
+
closed(jwks, ['keys'], 'invalid_event_config');
|
|
34
|
+
if (!Array.isArray(jwks.keys) || !jwks.keys.length || jwks.keys.length > 16) fail('invalid_event_config');
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const key of jwks.keys) {
|
|
37
|
+
closed(key, ['kty', 'crv', 'x', 'y', 'kid', 'alg', 'use', 'key_ops', 'ext'], 'invalid_event_config');
|
|
38
|
+
identity(key.kid, 256, 'invalid_event_config');
|
|
39
|
+
if (seen.has(key.kid) || key.kty !== 'EC' || key.crv !== 'P-256' ||
|
|
40
|
+
!['x', 'y'].every(field => typeof key[field] === 'string' && /^[A-Za-z0-9_-]{43}$/.test(key[field])) ||
|
|
41
|
+
(key.alg !== undefined && key.alg !== 'ES256') || (key.use !== undefined && key.use !== 'sig') ||
|
|
42
|
+
(key.key_ops !== undefined && (!Array.isArray(key.key_ops) || key.key_ops.length !== 1 || key.key_ops[0] !== 'verify'))) fail('invalid_event_config');
|
|
43
|
+
seen.add(key.kid);
|
|
44
|
+
}
|
|
45
|
+
return structuredClone(jwks);
|
|
46
|
+
}
|
|
47
|
+
/** No remote key lookup. Parse only the header until the signature is verified. */
|
|
48
|
+
export async function verifyEventJws(rawBody, source, now) {
|
|
49
|
+
try {
|
|
50
|
+
const compact = new TextDecoder('utf-8', { fatal: true }).decode(rawBody);
|
|
51
|
+
const parts = compact.split('.');
|
|
52
|
+
if (parts.length !== 3) throw new Error();
|
|
53
|
+
const header = json(parts[0]);
|
|
54
|
+
closed(header, ['alg', 'kid', 'typ'], 'event_signature_invalid');
|
|
55
|
+
if (header.alg !== 'ES256' || (header.typ !== undefined && header.typ !== 'JWT')) throw new Error();
|
|
56
|
+
const jwk = source.jwks.keys.find(key => key.kid === header.kid);
|
|
57
|
+
if (!jwk) throw new Error();
|
|
58
|
+
const signature = decode(parts[2]);
|
|
59
|
+
if (signature.length !== 64) throw new Error();
|
|
60
|
+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
|
|
61
|
+
if (!await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, signature, encoder.encode(`${parts[0]}.${parts[1]}`))) throw new Error();
|
|
62
|
+
const payload = json(parts[1]);
|
|
63
|
+
closed(payload, ['iss', 'aud', 'iat', 'events'], 'event_signature_invalid');
|
|
64
|
+
if (payload.iss !== source.serviceOrigin || payload.aud !== source.installId ||
|
|
65
|
+
!Number.isSafeInteger(payload.iat) || payload.iat < 0 || Math.abs(now - payload.iat * 1000) > 60_000) throw new Error();
|
|
66
|
+
return payload.events;
|
|
67
|
+
} catch { fail('event_signature_invalid'); }
|
|
68
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { signRequest } from './jws.mjs';
|
|
3
|
+
|
|
4
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
5
|
+
async function jsonBody(response, signal) {
|
|
6
|
+
if (!response.body) throw new Error();
|
|
7
|
+
const reader = response.body.getReader();
|
|
8
|
+
const cancel = () => { void reader.cancel().catch(() => {}); };
|
|
9
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let size = 0;
|
|
12
|
+
try {
|
|
13
|
+
for (;;) {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done) break;
|
|
16
|
+
size += value.length;
|
|
17
|
+
if (size > MAX_RESPONSE_BYTES) throw new Error();
|
|
18
|
+
chunks.push(value);
|
|
19
|
+
}
|
|
20
|
+
const bytes = new Uint8Array(size);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
|
|
23
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
24
|
+
} finally { signal.removeEventListener('abort', cancel); cancel(); }
|
|
25
|
+
}
|
|
26
|
+
/** Exactly one signed exchange, bounded through response-body consumption. */
|
|
27
|
+
export async function messagingRequest(config, dependencies, method, path, body = new Uint8Array()) {
|
|
28
|
+
let now;
|
|
29
|
+
try { now = (dependencies.clock ?? Date.now)(); } catch { fail('invalid_messaging_clock'); }
|
|
30
|
+
const signature = await signRequest(config, method, path, body, now);
|
|
31
|
+
const timeout = dependencies.timeoutMilliseconds ?? 15_000;
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const fetcher = dependencies.serviceBinding
|
|
34
|
+
? dependencies.serviceBinding.fetch.bind(dependencies.serviceBinding)
|
|
35
|
+
: dependencies.fetch ?? globalThis.fetch;
|
|
36
|
+
let timer;
|
|
37
|
+
try {
|
|
38
|
+
return await Promise.race([
|
|
39
|
+
Promise.resolve().then(async () => {
|
|
40
|
+
const response = await fetcher(`${config.serviceOrigin}${path}`, {
|
|
41
|
+
method, redirect: 'manual', signal: controller.signal,
|
|
42
|
+
headers: { 'RER-Signature': signature, ...(method === 'GET' ? {} : { 'Content-Type': 'application/json' }) },
|
|
43
|
+
...(method === 'GET' ? {} : { body }),
|
|
44
|
+
});
|
|
45
|
+
if (!(response instanceof Response)) return null;
|
|
46
|
+
if (response.status !== 200) { void response.body?.cancel().catch(() => {}); return { status: response.status, value: null }; }
|
|
47
|
+
return { status: 200, value: await jsonBody(response, controller.signal) };
|
|
48
|
+
}).catch(() => null),
|
|
49
|
+
new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(null); }, timeout); }),
|
|
50
|
+
]);
|
|
51
|
+
} finally { clearTimeout(timer); controller.abort(); }
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fail } from '../../core/errors.mjs';
|
|
2
|
+
import { validateMessage, encodeBase64 } from '../../core/validation.mjs';
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
export function serializeMessagingMessage(message) {
|
|
6
|
+
validateMessage(message, 'messaging');
|
|
7
|
+
if (message.unsubscribe !== undefined) fail('unconsumed_unsubscribe');
|
|
8
|
+
if (encoder.encode(message.text ?? '').length + encoder.encode(message.html ?? '').length > 1024 * 1024) fail('messaging_body_too_large');
|
|
9
|
+
let attachmentBytes = 0;
|
|
10
|
+
for (const attachment of message.attachments ?? []) attachmentBytes += attachment.content.byteLength;
|
|
11
|
+
if (attachmentBytes > 5 * 1024 * 1024) fail('messaging_attachments_too_large');
|
|
12
|
+
for (const name of Object.keys(message.headers ?? {})) {
|
|
13
|
+
if (!/^(?:list-[a-z0-9-]+|x-[a-z0-9-]+|auto-submitted|in-reply-to|references|message-id)$/i.test(name)) fail('unsupported_messaging_header');
|
|
14
|
+
}
|
|
15
|
+
const { to, attachments, ...rest } = message;
|
|
16
|
+
const body = encoder.encode(JSON.stringify({ ...rest, to: to[0], ...(attachments === undefined ? {} : {
|
|
17
|
+
attachments: attachments.map(attachment => ({ ...attachment, content: encodeBase64(attachment.content) })),
|
|
18
|
+
}) }));
|
|
19
|
+
if (body.length > 8 * 1024 * 1024) fail('messaging_request_too_large');
|
|
20
|
+
return body;
|
|
21
|
+
}
|