@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,308 @@
|
|
|
1
|
+
# Base deploy kit
|
|
2
|
+
|
|
3
|
+
Dependency-free ESM provisioning, migration, upload and authenticated health checks for declared Cloudflare Workers targets. Node 22.12+. This module does not read Wrangler login, `.env`, git credentials or provider configuration. Actual hosted qualification remains pending owner inputs; local tests use provider fixtures only under `tests/` and real SQLite for migration/data behavior.
|
|
4
|
+
|
|
5
|
+
## Stage interface: IF-057-STAGE-v1
|
|
6
|
+
|
|
7
|
+
The fixed commands run with Base checkout root as cwd:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
node modules/deploy-kit/prepare.mjs
|
|
11
|
+
node modules/deploy-kit/cli.mjs deploy
|
|
12
|
+
node modules/deploy-kit/cli.mjs health
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`default-stage.mjs` exports these arrays. Prepare runs in a separate ephemeral **credential-free Linux job**. Deploy and health run from a reviewed trusted controller checkout in a fresh job. The project checkout, dependencies, scripts, build process, parent processes and filesystem must never coexist with provider/mail/evidence credentials or controller configuration. Environment clearing alone cannot enforce this. The workflow must use `persist-credentials: false`, have no provider/mail/evidence secrets or OIDC in prepare, not mount controller directories, and not place controller code in the project checkout. The prepare isolation environment marker is a workflow assertion, not a security sandbox.
|
|
16
|
+
|
|
17
|
+
Prepare runs `pnpm install --frozen-lockfile` then `pnpm --dir default-seed build`. It packages the actual Vite `./dist/server` modules/main, `./dist/client` assets and D1 SQL. It verifies clean source before and after, rejects links/special files, and writes one bounded inert JSON artifact outside the source tree. It never reads provider inputs. Unsupported module formats fail explicitly.
|
|
18
|
+
|
|
19
|
+
The packer supports the pinned Cloudflare Vite plugin 1.54.5 legacy `wrangler.json` output: `no_bundle: true` and `rules: [{type: "ESModule", globs: ["**/*.js", "**/*.mjs"]}]`. It follows Wrangler 4.129.1's module discovery for those rules plus its default Text (`.txt`, `.html`, `.sql`), Data (`.bin`) and CompiledWasm (`.wasm`) rules. This retains lazy JavaScript chunks and additional runtime modules. `.vite/manifest.json`, `wrangler.json`, source maps and other files outside those rules are build metadata, not upload modules. The full build tree still undergoes regular-file and size checks. Bundled output, alternate rules/module roots and disabled module discovery are refused; adapting a new build contract is explicit. JSON remains an unsupported artifact module type.
|
|
20
|
+
|
|
21
|
+
Inputs to prepare (allowlist these, do not spread runner environment):
|
|
22
|
+
|
|
23
|
+
- `RER_DEPLOY_SOURCE_SHA`: trusted exact 40-hex checkout SHA.
|
|
24
|
+
- `RER_DEPLOY_RUN_ID`, `RER_DEPLOY_RUN_ATTEMPT`: trusted decimal strings.
|
|
25
|
+
- `RER_DEPLOY_ARTIFACT`: absent absolute output filename outside checkout.
|
|
26
|
+
- `RER_PREPARE_HOME`: absent private directory outside checkout.
|
|
27
|
+
- `RER_PREPARE_ISOLATION=ephemeral-credential-free-job` and PATH.
|
|
28
|
+
|
|
29
|
+
The trusted controller must verify artifact-service run/source custody and obtain the SHA-256 digest independently. An artifact's identity fields or prepare stdout do not attest themselves. Deploy and health accept those same four identity/artifact inputs plus `RER_DEPLOY_ARTIFACT_SHA256`, `RER_DEPLOY_RECEIPT`, and **only** `CF_THROWAWAY_ACCOUNT_ID`/`CF_THROWAWAY_API_TOKEN` as provider inputs. They parse artifact bytes; they do not execute or import candidate code. The evidence token belongs only to a separate fixed persistence step. `NODE_OPTIONS`/`NODE_PATH` must be absent before Node starts, not merely checked afterward.
|
|
30
|
+
|
|
31
|
+
Worker names are `rer-base-stage-<run-id>`, with `-db` D1 and `-jobs` Queue. The exact account's authenticated workers.dev subdomain defines the origin. There is no arbitrary URL/command/name workflow input. Default-seed requires D1, JOBS and its ten-minute Cron only. No unrelated mail credential is required.
|
|
32
|
+
|
|
33
|
+
The maintained default-seed `/health` returns the existing `status: 'ok'` plus this deployment identity after its real D1 readiness check, built by `deploymentHealth(env)` from `foundation/runtime/deployment-health.ts`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
{ ok: true, deployment: {
|
|
37
|
+
sourceSha: env.RER_DEPLOY_SOURCE,
|
|
38
|
+
fingerprint: env.RER_DEPLOY_FINGERPRINT,
|
|
39
|
+
versionId: env.RER_VERSION.id,
|
|
40
|
+
} }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Assets upload with `run_worker_first: true` (`RUN_WORKER_FIRST` in `render.mjs`). Every request reaches the Worker first, so the seed's routing, origin admission, `no-store` document policy and private-path refusals decide what is served, and the Base delivery producer accepts no other setting. A built file therefore reaches the browser only through the Worker: seed entries call `staticAssetResponse(request, env)` from `foundation/runtime/static-assets.ts` first. After the deploy identity passes, health requests one built file (the first artifact asset that is neither an HTML document nor under a dot path) once, without credentials, and fails with `asset-http-failed` unless it answers 200; the result is recorded as `health.asset`. `rer verify` serves the local build with the same setting (`cli/hosted-output.mjs`).
|
|
44
|
+
|
|
45
|
+
The controller supplies the two public variables and Cloudflare version-metadata binding. The parent owns default-seed source/types, package scripts, shared manifests and workflows. Keep the staged-deploy descriptor blocked until the three-stage isolation and actual build/health integration pass; the original deploy-then-health executor cannot safely run installation/build with its credentials.
|
|
46
|
+
|
|
47
|
+
## Reconciliation and repeats
|
|
48
|
+
|
|
49
|
+
`deploy({api,plan,artifact,artifactSha256,previousReceipt,env,fetch})` is the shared implementation for deploy and stage. Plans are controller-owned and validated; do not derive a secret-bearing plan from executable project configuration. `defaultStagePlan` demonstrates the full schema. Optional R2 `{binding,name}`, Queue `{binding,name,consumer?}`, DO `{binding,className,storage:'sqlite'|'classic'}`, and secret `{binding,environment}` declarations are supported. New DOs must use SQLite; existing classic namespaces are reusable. Namespace creation occurs through Worker upload migrations, with old/new migration tags, then authenticated namespace readback. No namespace create endpoint is invented. Cron and queue consumers reconcile after Worker creation. Conflicting consumers and resource identities fail; resources/data are never deleted.
|
|
50
|
+
|
|
51
|
+
Cron reads require the documented `result: {schedules: [...]}` envelope, including valid unique Cron strings; bare arrays and partial/malformed results fail. Before changing an existing Worker, deployment reads its schedules and rejects any Cron outside the requested plan with `cron-drift`, before resource writes, SQL migrations or Worker upload. Trigger reconciliation repeats that read before any trigger write. This applies to ordinary changed-source upgrades with or without a prior receipt. The kit may add requested schedules; it never intentionally removes existing schedules. Removing a Cron from the plan while it still exists remotely requires a separate reviewed removal first. Every changed upload reapplies its nonempty declared Cron set after workers.dev activation, even when the Worker-scoped list already matches. The kit reads the exact schedule set back after the write and checks that the active deployment identity remained stable. A declared Cron also requires the authenticated active version to report a `scheduled` handler; absent detection fails with `missing-scheduled-handler`. These checks establish provider configuration, not proof that a scheduled invocation has executed. Dispatch is the provider's: on the RunEditRun account (11 October 2026) a correctly registered Cron started only at the account scheduler's next refresh, observed 60 to 106 minutes apart, so a first tick can arrive well after Cloudflare's documented 15 minutes; a cleared schedule keeps running, and a deleted Worker keeps being invoked (as `internalError`), until a later refresh. A redeploy keeps the existing dispatch: the first run after it executes the new version. Judge first-deploy Cron by the seed's own scheduled-run record, not by a short watch. Unchanged deployment and health require the exact desired schedule set and perform no repairs. Callers must serialize deployments and out-of-band schedule edits for a Worker: the provider schedule replacement API offers no conditional update in this interface.
|
|
52
|
+
|
|
53
|
+
`workerConsumerScript(consumer)` (also exported from `index.mjs`) decodes Worker consumer identities from either documented `script_name` or the observed GET response string `script`. It returns the exact name using the plan's resource-name grammar, or `null` for missing, malformed, conflicting, or non-Worker identities. If both fields are present, each must be valid and both must agree; a malformed alias is never ignored. Reconciliation checks every consumer on queues with a requested consumer before any trigger write and rejects foreign or duplicate consumers. Writes continue to use the documented `script_name` request field. Read-only health and retained-receipt repeats accept either response variant without repairs.
|
|
54
|
+
|
|
55
|
+
D1/Queue/DO account listings fully paginate with bounded page/request budgets. R2 follows provider cursors. Missing pagination evidence, duplicate names, permission errors, partial success, redirects and transport failures fail visibly. Requests go only to Cloudflare's exact account API. Every read has a 30-second timeout and bounded response size; no request is retried. Asset upload tokens are used only on the documented same-account assets endpoint.
|
|
56
|
+
|
|
57
|
+
The first receipt binds source commit SHA and tree, artifact/config SHA-256, effective deployment fingerprint, account/Worker, actual resource IDs, migration hashes, secret names and active provider deployment/version/etag. It contains no secret value. The CLI creates it exclusively at mode 0600 before the first health request; it is an identity receipt, not a claim that health passed (health attempts are added afterwards, see below). A failed health request still exits nonzero, while the retained identity allows the next explicit invocation to authenticate state without another upload. For the second deployment use the **same artifact, run/attempt and receipt**. Current provider state must match the prior receipt. The kit performs no resource creation, Worker/asset upload, SQL mutation, Cron/consumer update, or secret rewrite when unchanged. Read-only D1 queries use the provider's POST query transport; a POST is not by itself a database mutation. The receipt remains byte-identical on a repeat. Missing resources, drift or stale state fail without repair during this repeat.
|
|
58
|
+
|
|
59
|
+
Opaque existing secrets are preserved by name and strict binding inheritance; values are never fetched or compared. Missing declared secrets are supplied in the upload metadata in memory only. Rotation is a separate explicit operation; changing an input value does not overwrite a deployed secret. No temporary secret files are used.
|
|
60
|
+
|
|
61
|
+
Health independently reads current deployment/version/settings, resources, namespaces, schedules, consumers, secret presence and migration history. It requests only the authenticated account-derived `/health`, without credentials, rejects redirects/malformed/unhealthy/stale identity and reads deployment identity again afterward.
|
|
62
|
+
|
|
63
|
+
`health.mjs` is Base's one health verifier. `waitForDeploymentHealth(url, options)` (formerly `deployment/health.mjs`) gives a newly published route and its bindings one bounded window, 30 seconds by default: only 404, 503 and network failures are retried, every other status and every redirect is terminal, and the final error states the last result. The standalone `health` command probes `/health` once; transient failures are recorded as failures without a retry. The library `verifyHealth` retains its bounded readiness window. After an upload, `deploy` uses the recorded readiness wait described below.
|
|
64
|
+
|
|
65
|
+
## Owner account
|
|
66
|
+
|
|
67
|
+
Owner-account mode deploys a seed to the owner's own Cloudflare account. It needs only two inputs: `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, and the account must already have a workers.dev subdomain (see below). On a first deploy it also needs the secrets the seed declares. It never reads `CF_THROWAWAY_*` or `RER_DEPLOY_*`, Wrangler login or `.env`. The throwaway stage above is unchanged and remains the proof path.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
pnpm build # the seed's own build, from a committed tree
|
|
71
|
+
node <kit>/owner-cli.mjs deploy --declaration deploy.json --environment production [--product <dir>] [--state <file>]
|
|
72
|
+
node <kit>/owner-cli.mjs health --declaration deploy.json --environment production
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The seed declares bindings, never names:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{ "version": 1, "project": "runeditrun/penny", "compatibilityDate": "2026-09-08", "compatibilityFlags": ["nodejs_compat"],
|
|
79
|
+
"d1": [{ "binding": "DB" }], "r2": [{ "binding": "FILES" }], "kv": [{ "binding": "CACHE" }],
|
|
80
|
+
"queues": [{ "binding": "JOBS", "consumer": { "batch_size": 10, "max_retries": 3 }, "deadLetterQueue": "JOBS_DEAD" }, { "binding": "JOBS_DEAD" }],
|
|
81
|
+
"durableObjects": [{ "binding": "ROOM", "className": "Room", "storage": "sqlite" }],
|
|
82
|
+
"secrets": [{ "binding": "AUTH_SECRET", "environment": "AUTH_SECRET" }], "crons": ["* * * * *"],
|
|
83
|
+
"vars": { "MAIL_PROVIDER": "sendgrid" }, "originVar": "APP_ORIGIN" }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Names are deterministic. The Worker is `<last segment of project>-<environment>`, for example `penny-production`. Each D1 database, R2 bucket, KV namespace and Queue is `<worker>-<binding in lower case, _ as ->`, for example `penny-production-db` and `penny-production-jobs-dead`. Durable Object namespaces belong to the Worker and are created by its upload migration. `originVar` receives `https://<worker>.<account subdomain>.workers.dev`. Migrations come from the packaged build's `migrations/core/*.sql` files. A name over 63 characters is refused, not shortened.
|
|
87
|
+
|
|
88
|
+
A declaration may also bind another owner-deployed Worker and may keep its own workers.dev route off. Both keys are optional, and a declaration without them renders and fingerprints exactly as before:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "services": [{ "binding": "PENNY_EGRESS", "project": "runeditrun/egress-service", "entrypoint": "Optional", "allowedHosts": ["hooks.example.com", "*.example.org"] }],
|
|
92
|
+
"workersDev": false }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A service's Worker name is derived from its `project` and this deploy's environment, so the example above binds `egress-service-production`. `allowedHosts` becomes the binding's `props` (`{allowedHosts}`), which the bound Worker reads as `ctx.props`. The binding is uploaded and read back like every other binding, so a changed `props` is reported as `binding-readback-mismatch`. Before any write, deploy reads each bound Worker. If it is absent, deploy refuses with `service-worker-missing` and resource `service:<worker>`; deploy that Worker first. `workersDev: false` keeps the route off: an enabled route is switched off on upload, the receipt's `origin` is `null`, and health uses provider readback only, because there is no public `/health` to request. [modules/egress-service](../egress-service/README.md) is the Worker this exists for.
|
|
96
|
+
|
|
97
|
+
`ownerTarget`, `ownerPlan`, `ownerDeploy` and `ownerHealth` (exported from `index.mjs`) are the same steps for a seed's own script. A seed script can compute vars before calling `ownerPlan`.
|
|
98
|
+
|
|
99
|
+
Ownership lives in a state file, by default `.rer/deploy/<environment>.json` under the checkout (mode 0600). It holds the account, project, environment, the resources this kit created with their ids (for an R2 bucket, its creation date), the pending upload intent and its observed provider identity, and the last receipt. It holds no secret values. Keep it out of Git and keep it: it is the only proof of which resources the kit created. Back it up after each deploy; see "If the state file is lost" below.
|
|
100
|
+
|
|
101
|
+
- **Find or create.** Before any write, the kit lists every declared kind and checks the Worker. A name it created is reused. An absent name is created. A same-named resource it did not create is refused with `resource-name-collision` and the resource name; it is never adopted. Ownership collisions, missing first-deploy secrets and existing Cron drift fail before the first provider write. Provider failures may interrupt later writes; see partial failure and resume below.
|
|
102
|
+
- **Re-run.** The same build writes nothing: no create, upload, migration, trigger or secret write, and the state file stays byte-identical. A later release that declares a new resource creates only that resource.
|
|
103
|
+
- **Partial failure and resume.** The kit records each create as `pending` before the request and `created` with the provider id after it. A failed create leaves `pending`. On the next run an absent pending name is created again, and a present one is refused as `uncertain-resource-create`, because a lost response cannot prove who created it. Inspect it: if it is the empty resource from the interrupted run, delete it and re-run. A Worker upload is recorded as created when the upload succeeds, so a failure after it resumes with one fresh upload. If the response to the very first upload is lost, the Worker exists but is still `pending`. The kit records the build and its fingerprint before sending an upload. With no prior receipt, the same build may recover its pending Worker only when the live fingerprint matches that intent, then resume with one fresh upload. A different build or fingerprint is refused as `uncertain-resource-create`; recover with the original build first. If the uncertain Worker cannot be authenticated, inspect it and delete it only after confirming that it is the kit-created Worker and that its Durable Object data is disposable. Back up any needed data before deleting it. Migration claims keep the rules under Migration safety.
|
|
104
|
+
- **Drift.** A created resource that has gone is refused as `owned-resource-missing`; one replaced under the same name is refused as `owned-resource-replaced`. The kit never deletes or recreates them. This covers D1, KV, Queues and R2 (a bucket is identified by its creation date; a state file written before that was recorded keeps working and is not checked for that bucket). For the Worker, the live deployment (version id, deployment id and etag) must be the last receipt's before any write; someone else's upload under the same name is refused as `owned-resource-replaced` and never overwritten. Immediately before the Worker PUT, the kit rechecks every saved resource identity, including resources omitted from the current plan, then the Worker identity. A replacement during provisioning or asset upload is refused before the upload intent or receipt is saved. An interrupted successful upload may resume only when the live version id, deployment id and etag match the identity saved immediately after that upload. A historical fingerprint alone never overrides a receipt mismatch.
|
|
105
|
+
- **Lost upgrade response.** If an upgrade response is lost and the live identity no longer matches the receipt, the kit refuses rather than guessing ownership. A [Cloudflare rollback](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) creates a new deployment, so it does not restore the recorded identity. Recover with a new environment and a fresh state path, or back up the state and any needed data, confirm ownership, and follow the cleanup under "If the state file is lost" with a fresh `--state` path.
|
|
106
|
+
- **One run at a time.** A deploy takes an exclusive lock file, `<state file>.lock`, for the whole run and removes it at the end, even after a failure. A second run on the same state file is refused as `owner-state-locked` before any provider request. If a run was killed (power loss, `kill -9`) and no deploy is running, delete the lock file by hand.
|
|
107
|
+
|
|
108
|
+
The CLI prints JSON: refusal codes and resource names only, never inputs, tokens or provider bodies.
|
|
109
|
+
|
|
110
|
+
### If the state file is lost
|
|
111
|
+
|
|
112
|
+
A fresh clone or CI runner has no `.rer/` directory, so the kit can no longer tell its own resources from anyone else's and refuses every later deploy as `resource-name-collision`. It is correct to refuse: the kit never adopts what it cannot prove it created. To recover, do one of these.
|
|
113
|
+
|
|
114
|
+
- Restore the state file from a backup (the file is plain JSON; keep a copy outside the checkout, or in secret storage, after each deploy). Put it at the same `--state` path.
|
|
115
|
+
- Or, after confirming ownership, delete all kit-created resources for this project and environment (Worker, D1 databases, R2 buckets, KV namespaces and Queues) in the Cloudflare dashboard, then re-run. Delete only empty or disposable resources; back up any needed data first, including Durable Object data attached to the Worker. The kit creates them again and writes a new state file. A refusal names only the first collision: deleting that name alone may reveal the next one. Do not delete a foreign resource just because its name appears in a refusal.
|
|
116
|
+
- Or deploy a new `--environment`. The old resources stay as they are.
|
|
117
|
+
|
|
118
|
+
Do not deploy from CI unless the state file persists there.
|
|
119
|
+
|
|
120
|
+
### workers.dev subdomain
|
|
121
|
+
|
|
122
|
+
The origin is `https://<worker>.<subdomain>.workers.dev`, so the account needs a workers.dev subdomain. For an account without one, the kit stops at its first request with `workers-subdomain-missing`, before creating anything. Register one first: in the Cloudflare dashboard open Workers & Pages and choose the account's workers.dev subdomain, or call `PUT /accounts/<account id>/workers/subdomain` with `{"subdomain":"<name>"}` using a token with Workers Scripts: Edit. Then re-run.
|
|
123
|
+
|
|
124
|
+
### API token scopes
|
|
125
|
+
|
|
126
|
+
Create an account-scoped API token for the one account, with these account permissions:
|
|
127
|
+
|
|
128
|
+
| Permission | Used for |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Workers Scripts: Edit | Worker upload, settings, versions and deployments, secret names, Cron schedules, workers.dev subdomain, static asset upload, Durable Object namespace listing |
|
|
131
|
+
| D1: Edit | List, create and read databases; run migrations and their ledger queries |
|
|
132
|
+
| Workers R2 Storage: Edit | List, create and read buckets (only when R2 is declared) |
|
|
133
|
+
| Workers KV Storage: Edit | List, create and read namespaces (only when KV is declared) |
|
|
134
|
+
| Queues: Edit | List, create and read queues; reconcile Worker consumers (only when Queues are declared) |
|
|
135
|
+
|
|
136
|
+
When a token lacks a permission, the CLI prints which one instead of only a status code: `token-scope-missing-workers`, `token-scope-missing-d1`, `token-scope-missing-r2`, `token-scope-missing-kv` or `token-scope-missing-queues`, named after the row above. The code is given for HTTP 401 and 403, so an invalid token reports the first scope the kit tried. Edit permission can only be proved by writing, so there is no read-only preflight: a token that can list but not create fails at the first create, and anything the kit already created is kept in the state file and reused on the next run. No zone, user or membership permission is needed. These scopes follow Cloudflare's documented permissions for each endpoint the kit calls; tests use a fake Cloudflare API, so a fresh-account run is still the hosted proof.
|
|
137
|
+
|
|
138
|
+
### Adoption by seeds
|
|
139
|
+
|
|
140
|
+
A seed carries a copy of `modules/deploy-kit/` from a Base pin. Refresh the copy, write a `deploy.json` from the plan the seed already builds, and point `deploy`, and the hosting step of `setup`, at `owner-cli.mjs`. Keep any throwaway controller only for proofs. In `deploy.json` declare:
|
|
141
|
+
|
|
142
|
+
- the bindings the app uses (`DB`, `FILES`, `JOBS` and so on), with each queue's consumer and dead-letter queue and each SQLite Durable Object class;
|
|
143
|
+
- the secrets the app reads, such as its session secret and the mail provider's key;
|
|
144
|
+
- any Cron schedule and the public setup fields the app takes in `vars`;
|
|
145
|
+
- `originVar`, the variable that holds the app's public origin (for example `APP_ORIGIN`).
|
|
146
|
+
|
|
147
|
+
A seed whose setup provisions nothing declares the same bindings and deploys through its own `deploy` verb. Resource names change from `rer-seedproof-*` or a chosen prefix to `<project>-<environment>-*`. The kit refuses rather than adopts an earlier installation's resources.
|
|
148
|
+
|
|
149
|
+
## Migration safety
|
|
150
|
+
|
|
151
|
+
SQL files have ordered unique names and SHA-256 identities. `_rer_deploy_migrations` is permanent controller-owned history. Applied entries must be an unchanged prefix of the desired set. A new migration gets an `applying` claim before its SQL and completion marker are submitted together to the D1 batch query API. Any interrupted, failed or uncertain outcome remains blocked and must be inspected explicitly; the kit never automatically retries, rolls back user data, or marks an unknown outcome successful. Readback checks every final identity.
|
|
152
|
+
|
|
153
|
+
Completion preserves the original SQL bytes and always starts on a new line.
|
|
154
|
+
Comment-only migrations are rejected before any provider request or ledger claim.
|
|
155
|
+
The controller adds a separator only when the final non-comment token is not a
|
|
156
|
+
semicolon symbol. This avoids an empty D1 statement after trailing comments
|
|
157
|
+
without treating quoted semicolons as terminators or changing migration hashes.
|
|
158
|
+
|
|
159
|
+
A lexical guard rejects standalone transaction/resource control statements (`BEGIN`, `COMMIT`, `END`, `ROLLBACK`, `SAVEPOINT`, `RELEASE`, `ATTACH`, `DETACH`, `VACUUM`) at statement boundaries, including after a trigger. Bare `ROLLBACK` is also refused in conflict clauses and `RAISE`, which can roll back a batch without a standalone control statement. SQLite `CREATE [TEMP|TEMPORARY] TRIGGER` bodies and nested `CASE ... END` expressions are supported; quoted text and comments cannot create statement boundaries. Both ledger names are reserved in bare, double-quoted, backtick and bracket identifiers, including schema-qualified references. [SQLite also accepts single-quoted identifiers](https://www.sqlite.org/lang_keywords.html), so exact controller ledger names are reserved in every token, including data strings, relation names, virtual-table options and introspection arguments. Comments and longer descriptive strings remain allowed. This intentionally conservative policy does not attempt to authorize arbitrary SQLite name grammars. Incomplete quotes, block comments, parentheses, CASE expressions or trigger bodies and NUL bytes fail closed; trigger header expressions must close before BEGIN and body expressions before their statement semicolon. This guard protects controller custody; SQLite still validates application SQL syntax.
|
|
160
|
+
|
|
161
|
+
Existing Wrangler `d1_migrations` records lack content hashes. Automatic adoption would fabricate historical certainty, so those installations require separately reviewed history/hash adoption before this kit proceeds. The kit can extend its own previous migrations while retaining existing application rows. Local SQLite tests prove failure rollback in the test transport; the provider's actual failure atomicity, retained data, and two-deploy no-mutation behavior remain live proof obligations. The conservative `applying` claim prevents automatic replay even if a remote batch partially commits.
|
|
162
|
+
|
|
163
|
+
## Local operations and source templates
|
|
164
|
+
|
|
165
|
+
`operations/operations.mjs` retains v1 compatibility and supports declarative v2 `loops`. A required disabled loop fails. Enabled mail loops explicitly select `resend`, `sendgrid` or `cloudflare`; the corresponding nonblank key is required (`RESEND_API_KEY`, `SENDGRID_API_KEY`, or `CLOUDFLARE_EMAIL_API_TOKEN` plus `CLOUDFLARE_EMAIL_ACCOUNT_ID`). A conflicting `MAIL_PROVIDER` fails. Optional disabled features pass. No DNS, mail, build, deployment or provider request occurs in these checks. Existing v1 executable adapters remain the existing reviewed local contract; deploy-kit `check()` refuses executable configuration and accepts declarative v2 only. `mailPlan()` consumes the mail module's `mailDoctor(...,live:false)` once that module is integrated; absence is an explicit import error.
|
|
166
|
+
|
|
167
|
+
`createTemplates({destination,project,sourcePaths,commands,loops})` creates a new directory only, containing `edition.json`, `OPERATIONS.json` v2, **both** operations modules and the real edition-authoring tool modules. Commands/effects and selected paths must describe actual adopter files. Add actual sources, initialize Git and stage them, then call `lockTemplates(root)`: it invokes actual edition-authoring `lock` and `check`, generating real `sources.lock.json` hashes and retaining any recipe-selected origins. Existing destinations and locks are refused. It never invents source hashes or overwrites an owner edition.
|
|
168
|
+
|
|
169
|
+
## Validation and provenance
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
node --test modules/deploy-kit/tests/*.test.mjs operations/*.test.mjs
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The tests cover real SQLite prior data, migration order/hash/failure/replay safety, fixture provider uploads and repeated no mutations across D1/R2/Queue/DO/Cron/secrets, wrong/missing mail inputs, transport boundaries/pagination, stale health/version, artifact links/custody, actual authoring lock/check and unchanged v1 regression tests. Owner-account tests use a fake Cloudflare API (`tests/owner-provider.mjs`, which adds KV, foreign resources and injected failures to the same fixture): first create, byte-identical re-run, partial failure and resume, a lost create response, a Worker upload failure, a lost first-upload response, collisions with resources the kit did not create, a Worker or bucket replaced under the same name, a missing workers.dev subdomain, token-scope failures, the state-file lock, and the CLI over a committed seed build. `pnpm verify:deployment` runs every deploy-kit test in Base verify. They are not live receipts. CF throwaway token/account ID and evidence persistence token remain missing until the owner supplies them. Prepare/build's Actions isolation and real default-seed remote health require the parent integration; no Mac provider login or live call was used.
|
|
176
|
+
|
|
177
|
+
Implementation references read: an earlier public seed release's own deployment scripts, Base `deployment/owner-migrations.mjs`, `operations/` and `edition-authoring/`. The ambient-login/catch-and-continue paths in those examples were not adopted. No private source was copied or merged.
|
|
178
|
+
|
|
179
|
+
Primary protocol references: [Worker upload](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/), [multipart metadata](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/), [asset upload](https://developers.cloudflare.com/workers/static-assets/direct-upload/), [D1 query](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/query/), [Queue consumers](https://developers.cloudflare.com/api/resources/queues/subresources/consumers/), [R2 pagination](https://developers.cloudflare.com/api/resources/r2/subresources/buckets/methods/list/), [DO namespaces](https://developers.cloudflare.com/api/resources/durable_objects/subresources/namespaces/), [Worker versions](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/get/). Checked 6 October 2026; hosted conformance is not inferred from documentation.
|
|
180
|
+
|
|
181
|
+
Cron/build repair references: [Cron GET](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/schedules/methods/get/), [Cron PUT](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/schedules/methods/update/), [Cron replacement behavior](https://developers.cloudflare.com/workers/configuration/cron-triggers/), the published [Vite plugin 1.54.5 source](https://www.npmjs.com/package/@cloudflare/vite-plugin/v/1.54.5) (`src/plugins/output-config.ts`, `src/plugins/additional-modules.ts`) and [Wrangler 4.129.1 source](https://www.npmjs.com/package/wrangler/v/4.129.1) (`src/deployment-bundle/rules.ts`, `find-additional-modules.ts`, `no-bundle-worker.ts`).
|
|
182
|
+
|
|
183
|
+
## Additional adopter contracts
|
|
184
|
+
|
|
185
|
+
`packageBuild({root,source,runIdentity,productDirectory})` shares the inert Vite
|
|
186
|
+
packer with editions whose product is at repository root (`productDirectory:''`).
|
|
187
|
+
The default remains `default-seed`; caller-owned prepare isolation and build
|
|
188
|
+
commands still apply. Queue entries may declare `deadLetterQueue` as the name of
|
|
189
|
+
another declared queue with no consumer. Both resources are provisioned before
|
|
190
|
+
consumer reconciliation; provider `dead_letter_queue` is top-level, never a
|
|
191
|
+
numeric setting. Independent health verifies it and repeat refuses drift.
|
|
192
|
+
|
|
193
|
+
R2 reconciliation selects the explicit shared read-only list strategy
|
|
194
|
+
`api.list('/r2/buckets', {field:'buckets', pagination:'r2-bounded'})`.
|
|
195
|
+
This strategy is restricted to that exact endpoint and field and requests
|
|
196
|
+
`per_page=1000`, the documented maximum. Absent or null `result_info` is terminal
|
|
197
|
+
only when the page has fewer than 1000 buckets (including zero). A full page
|
|
198
|
+
without metadata fails closed. Present metadata must supply a valid positive
|
|
199
|
+
`per_page` no greater than 1000; terminal pages must be shorter than that bound.
|
|
200
|
+
Cursors are encoded, validated and checked for cycles. Any supplied count or
|
|
201
|
+
total count must agree with returned rows and completion, and totals cannot
|
|
202
|
+
change between pages. Request, page, row and response-size budgets still apply.
|
|
203
|
+
No metadata is fabricated and no request is retried. Generic `pagination:'cursor'`
|
|
204
|
+
retains its existing contract: omitted metadata fails, and its R2 null-metadata
|
|
205
|
+
exception accepts only a short page. D1/Queue/DO page pagination is unchanged.
|
|
206
|
+
The bounded strategy is covered with local provider fixtures; this change does
|
|
207
|
+
not itself establish authenticated provider conformance or deployed acceptance.
|
|
208
|
+
Protocol references checked 7 October 2026: [R2 list](https://developers.cloudflare.com/api/resources/r2/subresources/buckets/methods/list/)
|
|
209
|
+
and [queue consumer](https://developers.cloudflare.com/api/resources/queues/subresources/consumers/methods/create/).
|
|
210
|
+
|
|
211
|
+
## First health readiness and evidence
|
|
212
|
+
|
|
213
|
+
After an upload that changed the Worker, `deploy` gives `/health` a total
|
|
214
|
+
90-second readiness budget (`readiness.totalMs`, never above 90000). Retries wait
|
|
215
|
+
1 second, doubling to a 10-second cap, and never less than the 2-second poll of
|
|
216
|
+
the standalone window: 2, 2, 4, 8, 10, 10 ... seconds. Only transport failures,
|
|
217
|
+
404 and 5xx (including Cloudflare 52x) retry. A redirect, any other status, a
|
|
218
|
+
malformed or unhealthy body and a deployment identity mismatch fail at once.
|
|
219
|
+
Each request is capped by both the 30-second request limit and the remaining
|
|
220
|
+
budget, and no request starts at the deadline. An unchanged repeat uses the
|
|
221
|
+
existing library window and does not record first-upload readiness attempts.
|
|
222
|
+
The standalone CLI `health` command sends a single request. Library `verifyHealth`
|
|
223
|
+
retains the existing window, retrying only 404, 503 and network failures for
|
|
224
|
+
30 seconds by default.
|
|
225
|
+
|
|
226
|
+
The returned receipt carries `health: {ok, versionId, attempts}`. Each attempt is
|
|
227
|
+
`{offsetMs, status, cfRay, contentType, body, errorClass}`: `status` is null for
|
|
228
|
+
a transport failure, `body` is the first 256 response bytes decoded as UTF-8 and
|
|
229
|
+
`errorClass` is null for the passing attempt. A failed wait throws an error whose
|
|
230
|
+
message names the class and the last status (for example
|
|
231
|
+
`health-http-failed-last-status-503`) and which carries `attempts`, `status` and
|
|
232
|
+
the identity `receipt` (with `health.ok` false). Provider diagnostics are never
|
|
233
|
+
retained; the endpoint must not return credentials. `readiness: {totalMs, clock,
|
|
234
|
+
sleep}` selects a smaller bound and deterministic time; `healthOptions.now` and
|
|
235
|
+
`healthOptions.sleep` act as `clock` and `sleep` for the same purpose.
|
|
236
|
+
|
|
237
|
+
`onHealth(receipt)` runs after every attempt and at the end. A controller adopts
|
|
238
|
+
`onHealth: receipt => writeHealthReceipt(receiptPath, receipt)`, which atomically
|
|
239
|
+
replaces the mode-0600 receipt and refuses any change to its identity; the default
|
|
240
|
+
CLI and owner-account state do this. A no-op repeat never rewrites the original receipt.
|
|
241
|
+
|
|
242
|
+
## Read-only teardown verification
|
|
243
|
+
|
|
244
|
+
The kit does not perform deletes. After a seed has validated its receipt and
|
|
245
|
+
removed only its authenticated disposable resources, call
|
|
246
|
+
`verifyTeardown({api, plan, receipt})`. The default CLI exposes
|
|
247
|
+
`node modules/deploy-kit/cli.mjs verify-teardown` with the same controller inputs.
|
|
248
|
+
The target must be the exact receipt Worker with a `rer-seedproof-` prefix or
|
|
249
|
+
the default numeric `rer-base-stage-<run-id>` disposable stage name.
|
|
250
|
+
Verification reads the complete account Worker, D1, R2, Queue and Durable Object
|
|
251
|
+
namespace listings, and lists consumers for every surviving Queue. It rejects
|
|
252
|
+
any receipt resource ID/name or any resource/consumer with the disposable
|
|
253
|
+
prefix, including a foreign one; it does not authorize deleting those foreign
|
|
254
|
+
resources. Errors carry `remaining` with names and identities.
|
|
255
|
+
|
|
256
|
+
Deleting moves the provider's `total_count` while a listing is in progress. During
|
|
257
|
+
this post-delete snapshot only, `provider-pagination-changed` and
|
|
258
|
+
`provider-partial-list` discard the entire incomplete snapshot and restart all
|
|
259
|
+
listings. The default is four attempts with 250 ms exponential backoff capped at
|
|
260
|
+
2 seconds; `maxAttempts` is bounded to 1-8. Each attempt records its number,
|
|
261
|
+
offset and error class, and a complete attempt its counts. `onAttempt`, `clock`
|
|
262
|
+
and `sleep` are injectable. Exhaustion throws with every attempt; a partial listing
|
|
263
|
+
never counts as empty. The returned `listing` is the empty owned-scope view of the
|
|
264
|
+
complete snapshot that was read (an empty array for every owned type; foreign rows
|
|
265
|
+
are not copied into evidence, and each attempt's `counts` give the account totals).
|
|
266
|
+
On failure `error.remaining` names the survivors and `error.listing` holds their rows. Ordinary
|
|
267
|
+
`api.list` keeps all its strict pagination assertions. Workers and Queue consumers
|
|
268
|
+
use the explicit `pagination:'single'` strategy, restricted to those two
|
|
269
|
+
unpaginated endpoints; supplied pagination metadata must confirm completion.
|
|
270
|
+
|
|
271
|
+
A seed teardown adopts it by keeping every pre-delete receipt, consumer and
|
|
272
|
+
ownership check and its exact DELETE requests, and replacing its final
|
|
273
|
+
post-delete list-and-assert block with the shared verifier:
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
import { verifyTeardown } from '../../modules/deploy-kit/index.mjs';
|
|
277
|
+
const verification = await verifyTeardown({api, plan, receipt});
|
|
278
|
+
return {ok: true, deleted: true, verification};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Worker deletion removes its schedules and bindings from the API, but not from
|
|
282
|
+
the provider's dispatch: a deleted cron Worker keeps being invoked (failing) until
|
|
283
|
+
the account's next scheduler refresh (see Cron above). A teardown that deletes a
|
|
284
|
+
Worker calls `deleteWorker({api, worker})`, which PUTs an empty schedule set, reads
|
|
285
|
+
the schedules back until the provider reports none (bounded, 60 seconds by default;
|
|
286
|
+
a set that does not settle fails with `teardown-schedules-not-cleared` and leaves
|
|
287
|
+
the Worker in place) and only then deletes. The deploy CLI prints one line after a
|
|
288
|
+
Cron deploy that the first scheduled run can take up to about two hours on some
|
|
289
|
+
accounts. The namespace listing
|
|
290
|
+
independently checks Durable Object removal. Persist the verification (or the
|
|
291
|
+
error's `attempts` and `remaining`) in the seed's teardown evidence. This Base change
|
|
292
|
+
modifies no seed repository. Protocol references:
|
|
293
|
+
[Worker scripts list](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/list/)
|
|
294
|
+
and [Queue consumers list](https://developers.cloudflare.com/api/resources/queues/subresources/consumers/methods/list/).
|
|
295
|
+
|
|
296
|
+
Named egress service grants may be declared as `services[].grant` alongside
|
|
297
|
+
`entrypoint`. The grant carries exact `allowedHosts`, service `sourceSha`,
|
|
298
|
+
`fingerprint` and the service receipt's `provider` identity (`versionId`,
|
|
299
|
+
`deploymentId`, `etag`). These are controller checks, not upload binding fields.
|
|
300
|
+
Before deployment and during health verification, deploy-kit reads the target
|
|
301
|
+
Worker's `EGRESS_GRANTS` plain-text binding and verifies the chosen entrypoint,
|
|
302
|
+
host grant, source and immutable deployment identity. Missing, duplicate or
|
|
303
|
+
widened grants fail closed, as do service/binding/entrypoint/environment drift.
|
|
304
|
+
Only the ordinary named service binding is uploaded. This supports service-owned
|
|
305
|
+
narrowing where Cloudflare omits service-binding `props` from settings/version
|
|
306
|
+
readback. Ordinary `allowedHosts` still maps to `props` and retains strict
|
|
307
|
+
readback; missing props are never silently accepted. See the
|
|
308
|
+
[egress service guide](../egress-service/README.md).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, lstat, readdir } from 'node:fs/promises';
|
|
3
|
+
import { resolve, parse, join, extname } from 'node:path';
|
|
4
|
+
import { assert, hash, localPath } from './validation.mjs';
|
|
5
|
+
export const MAX_ARTIFACT = 48 * 1024 * 1024;
|
|
6
|
+
export async function regularBytes(path, limit = MAX_ARTIFACT) {
|
|
7
|
+
const absolute = resolve(path); let prefix = parse(absolute).root;
|
|
8
|
+
for (const part of absolute.slice(prefix.length).split('/').slice(0,-1)) { prefix = join(prefix,part); assert((await lstat(prefix)).isDirectory(),'nonregular-artifact-parent'); }
|
|
9
|
+
const file = await open(absolute,constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
10
|
+
try { const before = await file.stat(); assert(before.isFile() && before.nlink === 1 && before.size <= limit,'nonregular-or-oversize-artifact');
|
|
11
|
+
const bytes = await file.readFile(); const after = await file.stat(); assert(bytes.length === before.size && before.ino === after.ino && before.mtimeMs === after.mtimeMs,'artifact-changed-during-read'); return bytes;
|
|
12
|
+
} finally { await file.close(); }
|
|
13
|
+
}
|
|
14
|
+
export async function readArtifact(path, expected) {
|
|
15
|
+
assert(/^[a-f0-9]{64}$/.test(expected.sha256 ?? ''),'missing-artifact-sha256');
|
|
16
|
+
const bytes = await regularBytes(path); assert(hash(bytes) === expected.sha256,'artifact-digest-mismatch');
|
|
17
|
+
let artifact; try { artifact = JSON.parse(bytes); } catch { throw new Error('invalid-artifact-json'); }
|
|
18
|
+
validateArtifact(artifact);
|
|
19
|
+
assert(artifact.source.sha === expected.sourceSha && artifact.run.id === expected.runId && artifact.run.attempt === expected.runAttempt,'artifact-custody-mismatch');
|
|
20
|
+
return artifact;
|
|
21
|
+
}
|
|
22
|
+
export function validateArtifact(value) {
|
|
23
|
+
assert(value?.version === 1 && /^[a-f0-9]{40}$/.test(value.source?.sha ?? '') && /^[a-f0-9]{40}$/.test(value.source?.tree ?? '') && /^\d+$/.test(value.run?.id ?? '') && /^\d+$/.test(value.run?.attempt ?? ''),'invalid-artifact-identity');
|
|
24
|
+
assert(Array.isArray(value.modules) && value.modules.length > 0 && Array.isArray(value.assets) && Array.isArray(value.migrations), 'invalid-artifact-inventory');
|
|
25
|
+
localPath(value.main);
|
|
26
|
+
for (const list of [value.modules,value.assets]) {
|
|
27
|
+
assert(list.length <= 10000 && new Set(list.map(x => x.path)).size === list.length,'invalid-artifact-file-count');
|
|
28
|
+
for (const item of list) { localPath(item.path); assert(typeof item.base64 === 'string' && Buffer.from(item.base64,'base64').toString('base64') === item.base64 && hash(Buffer.from(item.base64,'base64')) === item.sha256,'invalid-artifact-file'); }
|
|
29
|
+
}
|
|
30
|
+
assert(value.modules.some(x => x.path === value.main),'artifact-main-missing');
|
|
31
|
+
assert(value.modules.every(x => moduleType(x.path) !== undefined), 'unsupported-module-type');
|
|
32
|
+
}
|
|
33
|
+
export async function collectFiles(root, directory = '', budget = {bytes:0,count:0}) {
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const entry of (await readdir(join(root,directory),{withFileTypes:true})).sort((a,b) => a.name.localeCompare(b.name))) {
|
|
36
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name; localPath(path);
|
|
37
|
+
assert(!entry.isSymbolicLink(),'symlink-in-artifact');
|
|
38
|
+
if (entry.isDirectory()) files.push(...await collectFiles(root,path,budget));
|
|
39
|
+
else { assert(entry.isFile(),'nonregular-artifact'); const bytes = await regularBytes(join(root,path)); budget.bytes += bytes.length; budget.count++; assert(budget.bytes <= MAX_ARTIFACT && budget.count <= 10000,'artifact-inventory-too-large'); files.push({path,sha256:hash(bytes),base64:bytes.toString('base64')}); }
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
/** Built files a deployed Worker must serve: everything for the browser except documents
|
|
44
|
+
* (the app answers those; asset html_handling redirects them) and dot paths (never public). */
|
|
45
|
+
export const publicAssetPaths = paths => paths.filter(path => !path.split('/').some(part => part.startsWith('.')) && extname(path) !== '.html');
|
|
46
|
+
export function moduleType(path) { return ({'.js':'application/javascript+module','.mjs':'application/javascript+module','.wasm':'application/wasm','.txt':'text/plain','.html':'text/plain','.sql':'text/plain','.bin':'application/octet-stream'})[extname(path)]; }
|
|
47
|
+
export function assetType(path) { return ({'.html':'text/html','.css':'text/css','.js':'application/javascript','.mjs':'application/javascript','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.ico':'image/x-icon','.woff2':'font/woff2','.txt':'text/plain'})[extname(path)] ?? 'application/octet-stream'; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { assert, stable } from './validation.mjs';
|
|
2
|
+
import { collectFiles, moduleType } from './artifact.mjs';
|
|
3
|
+
|
|
4
|
+
/** The legacy output contract of the pinned Cloudflare Vite plugin (1.54.5).
|
|
5
|
+
* Wrangler no_bundle discovers modules using these ESModule rules plus its
|
|
6
|
+
* Text/Data/CompiledWasm defaults; it does not upload every file in dist/server.
|
|
7
|
+
* Keep unknown build contracts explicit rather than dropping possible modules.
|
|
8
|
+
*/
|
|
9
|
+
export async function collectBuildModules(root, config, main) {
|
|
10
|
+
const rules = [{type:'ESModule',globs:['**/*.js','**/*.mjs']}];
|
|
11
|
+
assert(config.no_bundle === true && stable(config.rules) === stable(rules)
|
|
12
|
+
&& config.find_additional_modules !== false && config.base_dir === undefined,
|
|
13
|
+
'unsupported-build-module-rules');
|
|
14
|
+
assert(/\.(?:m?js)$/.test(main), 'unsupported-build-main');
|
|
15
|
+
const files = await collectFiles(root);
|
|
16
|
+
// Includes all emitted chunks (also lazy chunks) and supported additional
|
|
17
|
+
// modules. JSON manifests and source maps do not match any upload rule.
|
|
18
|
+
const modules = files.filter(file => moduleType(file.path) !== undefined);
|
|
19
|
+
assert(modules.some(file => file.path === main), 'artifact-main-missing');
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readOperations, inspectOperations } from '../../operations/operations.mjs';
|
|
2
|
+
/** Only for reviewed project descriptors; operations v1's existing adapter contract remains unchanged. */
|
|
3
|
+
export async function check(root, env = {}) {
|
|
4
|
+
const {descriptor} = await readOperations(root);
|
|
5
|
+
if (descriptor.configuration?.module) throw new Error('deploy-check-refuses-executable-configuration');
|
|
6
|
+
if (descriptor.version !== 2) throw new Error('deploy-check-requires-declarative-v2');
|
|
7
|
+
return inspectOperations(root,env);
|
|
8
|
+
}
|
|
9
|
+
export async function mailPlan(options, dependencies) {
|
|
10
|
+
// The mail module's portable interface. Imported only for an explicitly requested offline doctor plan.
|
|
11
|
+
const { mailDoctor } = await import('../mail/index.mjs');
|
|
12
|
+
return mailDoctor({...options,live:false},dependencies);
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { writeFile, lstat } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { assert, hash } from './validation.mjs';
|
|
5
|
+
import { readArtifact, regularBytes } from './artifact.mjs';
|
|
6
|
+
import { cloudflare } from './provider.mjs';
|
|
7
|
+
import { defaultStagePlan } from './default-stage.mjs';
|
|
8
|
+
import { deploy } from './deploy.mjs';
|
|
9
|
+
import { verifyHealth } from './health.mjs';
|
|
10
|
+
import { verifyTeardown } from './teardown.mjs';
|
|
11
|
+
import { writeHealthReceipt } from './receipt.mjs';
|
|
12
|
+
|
|
13
|
+
export async function main(args=process.argv.slice(2),env=process.env) {
|
|
14
|
+
assert(args.length === 1 && ['deploy','health','verify-teardown'].includes(args[0]),'usage-deploy-health-or-verify-teardown');
|
|
15
|
+
assert(!env.NODE_OPTIONS && !env.NODE_PATH && !env.EVIDENCE_WRITE_TOKEN,'unsafe-deploy-environment');
|
|
16
|
+
const required = ['RER_DEPLOY_ARTIFACT','RER_DEPLOY_ARTIFACT_SHA256','RER_DEPLOY_SOURCE_SHA','RER_DEPLOY_RUN_ID','RER_DEPLOY_RUN_ATTEMPT','RER_DEPLOY_RECEIPT','CF_THROWAWAY_ACCOUNT_ID','CF_THROWAWAY_API_TOKEN'];
|
|
17
|
+
const missing = required.filter(key => typeof env[key] !== 'string' || !env[key].trim());
|
|
18
|
+
if (missing.length) { const error = new Error('missing-input'); error.inputs = missing; throw error; }
|
|
19
|
+
for (const key of ['RER_DEPLOY_ARTIFACT','RER_DEPLOY_RECEIPT']) assert(env[key] === resolve(env[key]),'input-path-must-be-absolute');
|
|
20
|
+
const expected = {sha256:env.RER_DEPLOY_ARTIFACT_SHA256,sourceSha:env.RER_DEPLOY_SOURCE_SHA,runId:env.RER_DEPLOY_RUN_ID,runAttempt:env.RER_DEPLOY_RUN_ATTEMPT};
|
|
21
|
+
const artifact = await readArtifact(env.RER_DEPLOY_ARTIFACT,expected);
|
|
22
|
+
const plan = defaultStagePlan(artifact);
|
|
23
|
+
const api = cloudflare({accountId:env.CF_THROWAWAY_ACCOUNT_ID,token:env.CF_THROWAWAY_API_TOKEN});
|
|
24
|
+
let previousReceipt;
|
|
25
|
+
try {
|
|
26
|
+
const stat = await lstat(env.RER_DEPLOY_RECEIPT); assert((stat.mode & 0o077) === 0,'receipt-permissions');
|
|
27
|
+
previousReceipt = JSON.parse(await regularBytes(env.RER_DEPLOY_RECEIPT,1024*1024));
|
|
28
|
+
assert(previousReceipt.run?.id === artifact.run.id && previousReceipt.run?.attempt === artifact.run.attempt && previousReceipt.source?.sha === artifact.source.sha && previousReceipt.artifactSha256 === expected.sha256,'receipt-custody-mismatch');
|
|
29
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
30
|
+
if (args[0] === 'verify-teardown') {
|
|
31
|
+
const result = await verifyTeardown({api,plan,receipt:previousReceipt});
|
|
32
|
+
console.log(JSON.stringify(result));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
if (args[0] === 'health') {
|
|
36
|
+
assert(previousReceipt && hash(previousReceipt.config) === previousReceipt.configSha256,'missing-or-invalid-receipt');
|
|
37
|
+
await verifyHealth({api,plan,receipt:previousReceipt,readiness:{single:true,totalMs:30000}});
|
|
38
|
+
} else {
|
|
39
|
+
const receipt = await deploy({api,plan,artifact,artifactSha256:expected.sha256,previousReceipt,repeatOnly:Boolean(previousReceipt),
|
|
40
|
+
onHealth: async candidate => { if (!previousReceipt) await writeHealthReceipt(env.RER_DEPLOY_RECEIPT,candidate); },
|
|
41
|
+
onReceipt: async candidate => {
|
|
42
|
+
if (!previousReceipt) await writeFile(env.RER_DEPLOY_RECEIPT,JSON.stringify(candidate,null,2)+'\n',{flag:'wx',mode:0o600});
|
|
43
|
+
}});
|
|
44
|
+
// A true repeat retains the original receipt byte-for-byte. A changed build needs a new receipt path/run.
|
|
45
|
+
if (previousReceipt) assert(receipt.changed === false,'existing-receipt-cannot-be-overwritten');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
49
|
+
try { await main(); } catch (error) { console.error(JSON.stringify(error.message === 'missing-input' ? {ok:false,code:'missing-input',inputs:error.inputs} : {ok:false,code:/^(health-|malformed-response|response-too-large|teardown-)/.test(error.message) ? error.message : 'deploy-kit-command-failed',...(error.attempts ? {attempts:error.attempts} : {}),...(error.remaining ? {remaining:error.remaining} : {})})); process.exitCode=1; }
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
|
|
3
|
+
export function ingressBindings(value = {}) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['address', 'httpPort', 'httpsPort'].includes(key))) throw Error('Invalid container ingress bindings');
|
|
5
|
+
const result = { address: '0.0.0.0', httpPort: 80, httpsPort: 443, ...value };
|
|
6
|
+
if (isIP(result.address) !== 4 || ![result.httpPort, result.httpsPort].every(port => Number.isInteger(port) && port >= 1 && port <= 65535) || result.httpPort === result.httpsPort) throw Error('Invalid container ingress bindings');
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { assert } from './validation.mjs';
|
|
2
|
+
/** Controller-owned target. No arbitrary Worker name, URL, command or resource declarations. */
|
|
3
|
+
export function defaultStagePlan(artifact) {
|
|
4
|
+
assert(/^\d{1,20}$/.test(artifact.run.id),'invalid-run-id');
|
|
5
|
+
const worker = `rer-base-stage-${artifact.run.id}`;
|
|
6
|
+
assert(artifact.migrations.every(x => x.binding === 'DB'),'unexpected-migration-binding');
|
|
7
|
+
return {version:1,worker,compatibilityDate:'2026-09-08',compatibilityFlags:['nodejs_compat'],
|
|
8
|
+
d1:[{binding:'DB',name:`${worker}-db`,migrations:artifact.migrations.map(({binding,...rest}) => rest)}],
|
|
9
|
+
r2:[],queues:[{binding:'JOBS',name:`${worker}-jobs`,consumer:{batch_size:10,max_retries:3}}],durableObjects:[],secrets:[],crons:['*/10 * * * *'],vars:{}};
|
|
10
|
+
}
|
|
11
|
+
export const stage = Object.freeze({cwd:'.',blocked:null,
|
|
12
|
+
prepare:['node','modules/deploy-kit/prepare.mjs'],
|
|
13
|
+
deploy:['node','modules/deploy-kit/cli.mjs','deploy'],
|
|
14
|
+
health:['node','modules/deploy-kit/cli.mjs','health']});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { verifyServiceGrants } from './service-grants.mjs';
|
|
2
|
+
import { assert, hash, validatePlan } from './validation.mjs';
|
|
3
|
+
import { validateArtifact, publicAssetPaths } from './artifact.mjs';
|
|
4
|
+
import { validateMigrations, migrate } from './migrations.mjs';
|
|
5
|
+
import { reconcileResources, reconcileTriggers, checkCronOwnership } from './resources.mjs';
|
|
6
|
+
import { activeWorker, secretBindings, uploadWorker, durableNamespaces, durableMigration, workerOrigin, verifyScheduledHandler } from './worker.mjs';
|
|
7
|
+
import { renderConfig, bindingsFor, verifyBindings } from './render.mjs';
|
|
8
|
+
import { verifyHealth } from './health.mjs';
|
|
9
|
+
|
|
10
|
+
/** Controller-owned plan, inert validated artifact bytes; no project imports or subprocesses.
|
|
11
|
+
* `createResources` defaults to creating only without a prior receipt. Owner-account
|
|
12
|
+
* mode creates owned resources itself first, then passes false. `uploadWithoutReceipt`
|
|
13
|
+
* re-uploads an identical build when no receipt survived, instead of refusing the no-op. */
|
|
14
|
+
export async function deploy({api,plan,artifact,artifactSha256,previousReceipt,repeatOnly=false,createResources=!previousReceipt,uploadWithoutReceipt=false,onReceipt,onHealth,readiness={},env={},fetch=globalThis.fetch,healthOptions={}}) {
|
|
15
|
+
validatePlan(plan); validateArtifact(artifact);
|
|
16
|
+
await verifyServiceGrants(api, plan);
|
|
17
|
+
for (const item of plan.d1) validateMigrations(item.migrations);
|
|
18
|
+
const existing = await activeWorker(api,plan.worker);
|
|
19
|
+
// An ordinary upgrade must not publish code or apply migrations before
|
|
20
|
+
// detecting schedules outside this plan. Check again at trigger reconciliation.
|
|
21
|
+
if (existing) await checkCronOwnership(api,plan);
|
|
22
|
+
// Resolve missing secrets before resource writes. Existing opaque values are retained unchanged.
|
|
23
|
+
const secrets = await secretBindings(api,plan.worker,plan,env,Boolean(existing));
|
|
24
|
+
const resources = await reconcileResources(api,plan,{create:createResources});
|
|
25
|
+
const baseConfig = renderConfig(plan,resources,api.accountId,artifact.source.sha,'',artifact.main,artifact.assets.length > 0);
|
|
26
|
+
const fingerprint = hash({plan,config:baseConfig,source:artifact.source,modules:artifact.modules.map(({path,sha256}) => ({path,sha256})),assets:artifact.assets.map(({path,sha256}) => ({path,sha256}))});
|
|
27
|
+
const config = renderConfig(plan,resources,api.accountId,artifact.source.sha,fingerprint,artifact.main,artifact.assets.length > 0);
|
|
28
|
+
const bindings = bindingsFor(config,resources,plan);
|
|
29
|
+
const namespaces = await durableNamespaces(api,plan);
|
|
30
|
+
const migration = durableMigration(namespaces,existing);
|
|
31
|
+
const priorFingerprint = existing?.settings.bindings.find(x => x.type === 'plain_text' && x.name === 'RER_DEPLOY_FINGERPRINT')?.text;
|
|
32
|
+
const authenticated = previousReceipt?.fingerprint === fingerprint && previousReceipt.accountId === api.accountId && previousReceipt.worker === plan.worker && previousReceipt.provider?.versionId === existing?.versionId && previousReceipt.provider?.etag === existing?.etag && previousReceipt.provider?.deploymentId === existing?.deploymentId;
|
|
33
|
+
const unchanged = priorFingerprint === fingerprint && secrets.missing.length === 0 && !migration && !(uploadWithoutReceipt && !authenticated);
|
|
34
|
+
if (repeatOnly) assert(unchanged && previousReceipt?.fingerprint === fingerprint && previousReceipt?.provider?.versionId === existing?.versionId && previousReceipt?.provider?.etag === existing?.etag, 'repeat-deployment-drift');
|
|
35
|
+
const migrations = [];
|
|
36
|
+
for (const item of plan.d1) migrations.push({binding:item.binding,...await migrate(api,resources.d1.find(x => x.binding === item.binding).id,item.migrations,{apply:!unchanged})});
|
|
37
|
+
if (unchanged) {
|
|
38
|
+
assert(previousReceipt?.fingerprint === fingerprint && previousReceipt.accountId === api.accountId && previousReceipt.worker === plan.worker && previousReceipt.provider?.versionId === existing.versionId && previousReceipt.provider?.etag === existing.etag && previousReceipt.provider?.deploymentId === existing.deploymentId, 'no-op-requires-authenticated-prior-receipt');
|
|
39
|
+
verifyBindings(existing.settings.bindings,bindings);
|
|
40
|
+
}
|
|
41
|
+
else await uploadWorker(api,{plan,artifact,bindings,secrets,migration,fingerprint});
|
|
42
|
+
const published = unchanged ? existing : await activeWorker(api,plan.worker); assert(published,'missing-deployed-worker');
|
|
43
|
+
verifyBindings(published.settings.bindings,bindings);
|
|
44
|
+
verifyScheduledHandler(published,plan);
|
|
45
|
+
// Finish routing activation before applying schedules to this deployment.
|
|
46
|
+
const origin = await workerOrigin(api,plan.worker,{enable:!unchanged,workersDev:plan.workersDev !== false});
|
|
47
|
+
await reconcileTriggers(api,plan,resources,{mutate:!unchanged,reapplyCrons:!unchanged});
|
|
48
|
+
const active = await activeWorker(api,plan.worker); assert(active,'missing-deployed-worker');
|
|
49
|
+
assert(active.versionId === published.versionId && active.deploymentId === published.deploymentId && active.etag === published.etag,'deployment-changed-during-triggers');
|
|
50
|
+
verifyBindings(active.settings.bindings,bindings);
|
|
51
|
+
verifyScheduledHandler(active,plan);
|
|
52
|
+
const receipt = {version:1,accountId:api.accountId,worker:plan.worker,origin,source:artifact.source,run:artifact.run,artifactSha256,configSha256:hash(config),fingerprint,config,resources,
|
|
53
|
+
durableObjects:await durableNamespaces(api,plan,{requireAll:true}),secrets:plan.secrets.map(x => x.binding),migrations,
|
|
54
|
+
provider:{versionId:active.versionId,deploymentId:active.deploymentId,etag:active.etag},changed:!unchanged};
|
|
55
|
+
if (onReceipt) await onReceipt(receipt);
|
|
56
|
+
// One built file proves the Worker hands static assets to the browser (assets run behind it).
|
|
57
|
+
const asset = publicAssetPaths(artifact.assets.map(x => x.path))[0];
|
|
58
|
+
if (unchanged) await verifyHealth({...healthOptions,api,plan,receipt,fetch,asset});
|
|
59
|
+
else await firstHealth({api,plan,receipt,fetch,healthOptions,readiness,onHealth,asset});
|
|
60
|
+
return receipt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** After an upload the route may take a while to appear: wait for /health with a bound and
|
|
64
|
+
* record every attempt in `receipt.health`. `onHealth(receipt)` runs after each attempt and at the end. */
|
|
65
|
+
async function firstHealth({api,plan,receipt,fetch,healthOptions,readiness,onHealth,asset}) {
|
|
66
|
+
receipt.health = {ok:false,attempts:[]};
|
|
67
|
+
const {now,sleep,...options} = healthOptions;
|
|
68
|
+
const wait = {...(now ? {clock:now} : {}),...(sleep ? {sleep} : {}),...readiness,onAttempt:async (_attempt,attempts) => {
|
|
69
|
+
receipt.health.attempts = [...attempts];
|
|
70
|
+
if (onHealth) await onHealth(receipt);
|
|
71
|
+
}};
|
|
72
|
+
try { receipt.health = await verifyHealth({...options,api,plan,receipt,fetch,readiness:wait,asset}); }
|
|
73
|
+
catch (error) {
|
|
74
|
+
receipt.health.ok = false; receipt.health.errorClass = error.message;
|
|
75
|
+
if (onHealth) await onHealth(receipt);
|
|
76
|
+
error.receipt = receipt;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (onHealth) await onHealth(receipt);
|
|
80
|
+
}
|