@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,65 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir, cp, mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { parseJsonc } from '../../cli/jsonc.mjs';
|
|
7
|
+
import { portabilityPlan } from './portability-schema.mjs';
|
|
8
|
+
import { checkBoundary } from './boundary.mjs';
|
|
9
|
+
const hash = source => createHash('sha256').update(source).digest('hex');
|
|
10
|
+
export async function buildContainer(root, options={}) {
|
|
11
|
+
const previous=process.cwd();process.chdir(root);
|
|
12
|
+
try{return await buildImage(root,options);}finally{process.chdir(previous);}
|
|
13
|
+
}
|
|
14
|
+
async function buildImage(root, { output = join(root,'.rer/container/image'), engine, cacheContract, runner, declaration } = {}) {
|
|
15
|
+
const require = createRequire(join(root,'package.json'));
|
|
16
|
+
declaration ??= JSON.parse(await readFile(join(root,'deploy.json'),'utf8'));
|
|
17
|
+
engine ??= declaration.container?.engine ?? 'workerd'; cacheContract ??= declaration.container?.cacheContract; runner ??= declaration.container?.runner;
|
|
18
|
+
if (!['workerd','node'].includes(engine)) throw new Error('Unsupported container engine');
|
|
19
|
+
await checkBoundary(root,{engine,cacheContract});
|
|
20
|
+
const vite = await import(pathToFileURL(require.resolve('vite'))), loaded = await vite.loadConfigFromFile({ command:'build',mode:'production' },undefined,root);
|
|
21
|
+
if (!loaded) throw new Error('Vite configuration missing');
|
|
22
|
+
const config = loaded.config;
|
|
23
|
+
if (engine === 'node') {
|
|
24
|
+
const plugins = (await Promise.all(config.plugins.flat(Infinity))).filter(plugin => plugin && !plugin.name.startsWith('vite-plugin-cloudflare') && plugin.name !== 'tanstack-start-core:post-build');
|
|
25
|
+
const bindingShim = resolve(import.meta.dirname,'bindings.mjs');
|
|
26
|
+
config.plugins = [{ name:'rer-container-bindings', enforce:'pre',
|
|
27
|
+
resolveId(id) { if (id === 'cloudflare:workers') return bindingShim; },
|
|
28
|
+
transform(source,id) { if (cacheContract === 'lamp-retained-v1' && id === join(root,'src/server.ts')) return source.replace(/export\s*\{\s*SnapshotCache\s*\}\s*from\s*['"][^'"]+['"];?/,''); },
|
|
29
|
+
},...plugins];
|
|
30
|
+
config.ssr = { ...config.ssr,noExternal:true }; config.environments = { ...config.environments,ssr:{ ...config.environments?.ssr,resolve:{ noExternal:true },build:{ ...config.environments?.ssr?.build,rolldownOptions:{ output:{ entryFileNames:'index.mjs' } } } } };
|
|
31
|
+
}
|
|
32
|
+
const builder = await vite.createBuilder({ ...config,root,configFile:false }); await builder.buildApp();
|
|
33
|
+
// Apply the same local-only bundle guard before packaging any container image.
|
|
34
|
+
await (await import('../../cli/dev-identity.mjs')).refuseDevLayerOutput(root);
|
|
35
|
+
let workerConfig;
|
|
36
|
+
if (engine === 'workerd') workerConfig = JSON.parse(await readFile(join(root,'dist/server/wrangler.json'),'utf8'));
|
|
37
|
+
else workerConfig = parseJsonc(await readFile(join(root,'wrangler.jsonc'),'utf8'));
|
|
38
|
+
const { unstable_splitSqlQuery:split } = require('wrangler');
|
|
39
|
+
const migrations = {};
|
|
40
|
+
for (const db of workerConfig.d1_databases ?? []) {
|
|
41
|
+
let directory = resolve(root,db.migrations_dir ?? 'migrations');
|
|
42
|
+
try { await readdir(directory); } catch { directory = resolve(root,'dist/server',db.migrations_dir); }
|
|
43
|
+
migrations[db.binding] = [];
|
|
44
|
+
for (const name of (await readdir(directory)).filter(name => name.endsWith('.sql')).sort()) { const sql = await readFile(join(directory,name),'utf8'); migrations[db.binding].push({ name,sql,sha256:hash(sql),statements:split(sql) }); }
|
|
45
|
+
}
|
|
46
|
+
const entry = engine === 'workerd' ? 'dist/server/'+workerConfig.main : 'dist/server/index.mjs';
|
|
47
|
+
await readFile(join(root,entry));
|
|
48
|
+
if (engine === 'node' && workerConfig.kv_namespaces?.length) throw new Error('Node container does not support KV');
|
|
49
|
+
for (const secret of declaration.secrets ?? []) if (Object.hasOwn(workerConfig.vars ?? {},secret.binding)) throw new Error('Secret declared as an image variable: '+secret.binding);
|
|
50
|
+
const manifest = { version:1,engine,entry,cacheContract,runner,config:workerConfig,migrations,portabilityMigrations:await portabilityPlan(split),maxRequestBytes:25*1024*1024 };
|
|
51
|
+
await rm(output,{recursive:true,force:true}); await mkdir(join(output,'app'),{recursive:true});
|
|
52
|
+
await cp(join(root,'dist'),join(output,'app/dist'),{recursive:true}); await writeFile(join(output,'app/container.json'),JSON.stringify(manifest,null,2)+'\n');
|
|
53
|
+
if (runner) {
|
|
54
|
+
const within = path => { if (typeof path !== 'string' || path.startsWith('/') || path.split('/').some(part => ['..','.',''].includes(part))) throw new Error('Invalid packaged runner path'); return join(root,path); };
|
|
55
|
+
await readFile(within(runner.entry));
|
|
56
|
+
for (const path of runner.files) await cp(within(path),join(output,'app/runner',path),{recursive:true,filter:path => !path.includes('/tests/') && !path.endsWith('/synthetic.mjs')});
|
|
57
|
+
}
|
|
58
|
+
const base = resolve(import.meta.dirname,'../..');
|
|
59
|
+
for (const path of ['modules/container','modules/portability','modules/deploy-kit','cli/scheduled.mjs','cli/jsonc.mjs','modules/integrations/core/public-address.ts']) { await mkdir(join(output,'runtime',path,'..'),{recursive:true}); await cp(join(base,path),join(output,'runtime',path),{recursive:true,filter:path => !path.includes('/tests/')}); }
|
|
60
|
+
const dependencies = engine === 'workerd' ? { miniflare:'5.20260907.0-alpha' } : {};
|
|
61
|
+
await writeFile(join(output,'runtime/package.json'),JSON.stringify({private:true,type:'module',dependencies}));
|
|
62
|
+
await writeFile(join(output,'Dockerfile'),`FROM node:26.8.1-bookworm-slim\nRUN apt-get update && apt-get install -y --no-install-recommends util-linux ca-certificates && apt-get clean\nRUN mkdir -p /private /archive\nWORKDIR /runtime\nCOPY runtime/package.json ./package.json\nRUN npm install --omit=dev --no-audit --no-fund\nCOPY runtime/ /runtime/\nCOPY app/ /app/\nENV NODE_ENV=production PORT=8080 WRANGLER_SEND_METRICS=false\nEXPOSE 8080\nVOLUME /data\nENTRYPOINT ["sh", "/runtime/modules/container/entrypoint.sh"]\n`);
|
|
63
|
+
return { output,manifest };
|
|
64
|
+
}
|
|
65
|
+
if (process.argv[1] === import.meta.filename) { try { const result=await buildContainer(resolve(process.argv[2])); console.log(JSON.stringify({output:result.output,engine:result.manifest.engine})); } catch(cause) { console.error(cause.message); if(cause.failures)console.error(JSON.stringify(cause.failures));process.exitCode=1; } }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { open, readFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { refuseLocalModels } from '../deploy-kit/local-only.mjs';
|
|
5
|
+
import { LOCAL_ONLY_KEYS, localOnlyBinding } from '../deploy-kit/owner.mjs';
|
|
6
|
+
export async function deployInputs({root,envFile,localOnlyKeys=[]}) {
|
|
7
|
+
await refuseLocalModels(root);
|
|
8
|
+
const declaration=JSON.parse(await readFile(join(root,'deploy.json'),'utf8'));
|
|
9
|
+
if(localOnlyBinding(declaration,localOnlyKeys))throw new Error('Deploy declaration contains a local-only binding');
|
|
10
|
+
if(!envFile)throw new Error('Container deploy requires --env-file with private production settings');
|
|
11
|
+
const handle=await open(resolve(root,envFile),constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
12
|
+
let encoded;
|
|
13
|
+
try{const status=await handle.stat();if(!status.isFile()||status.nlink!==1||(status.mode&0o077)!==0||(process.getuid&&status.uid!==process.getuid()))throw new Error('Deploy env file must be an owned private regular file (0600)');encoded=await handle.readFile('utf8');}finally{await handle.close();}
|
|
14
|
+
const settings={};for(const line of encoded.split(/\r?\n/)){if(!line.trim()||line.trim().startsWith('#'))continue;const match=/^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);if(!match)throw new Error('Invalid deploy env file');if(Object.hasOwn(settings,match[1]))throw new Error('Repeated deploy env key');settings[match[1]]=match[2].replace(/^"(.*)"$/,'$1');}
|
|
15
|
+
if([...LOCAL_ONLY_KEYS,...localOnlyKeys].some(key=>Object.hasOwn(settings,key)))throw new Error('Local-only keys are refused for deploy');
|
|
16
|
+
return{declaration,settings,encoded:Object.entries(settings).map(([key,value])=>key+'='+value).join('\n')+'\n'};
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { recordVolumeOwner } from './lock.mjs';
|
|
5
|
+
import { incoming, outgoing } from './ingress.mjs';
|
|
6
|
+
import { parseCron, cronMatches } from '../../cli/scheduled.mjs';
|
|
7
|
+
import { superviseRunner } from './runner.mjs';
|
|
8
|
+
export async function startHost({ root = '/app', data = '/data', settings = process.env, port = Number(process.env.PORT ?? 8080), edge, engineVersion = process.version, automaticTicks=true, runnerEnabled=true } = {}) {
|
|
9
|
+
await mkdir(data, { recursive:true });
|
|
10
|
+
const manifest = JSON.parse(await readFile(join(root, 'container.json'), 'utf8'));
|
|
11
|
+
const release = await recordVolumeOwner(data);
|
|
12
|
+
let engine, server, timer, idleTimer, runner, closing, lastMinute = -1, lastRequest=Date.now();
|
|
13
|
+
try {
|
|
14
|
+
const create = manifest.engine === 'node' ? (await import('./node-engine.mjs')).nodeEngine : (await import('./worker-engine.mjs')).workerEngine;
|
|
15
|
+
engine = await create({ root,data,manifest,settings,edge });
|
|
16
|
+
const schedules = (manifest.config.triggers?.crons ?? []).map(cron => [cron,parseCron(cron)]);
|
|
17
|
+
const tick = async time => { for (const [cron, expression] of schedules) if (cronMatches(expression, new Date(time))) await engine.scheduled({ cron,scheduledTime:time }); await engine.drain(); };
|
|
18
|
+
server = createServer(async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
lastRequest=Date.now();
|
|
21
|
+
const request = await incoming(req, { trustedProxy:settings.TRUSTED_PROXY, maxRequestBytes:manifest.maxRequestBytes });
|
|
22
|
+
let response = await engine.fetch(request);
|
|
23
|
+
if (new URL(request.url).pathname === '/health' && response.ok) {
|
|
24
|
+
const body = await response.json(); const deadJobs = engine.dead();
|
|
25
|
+
response = Response.json({ ...body, platform:'container', deployment:{ ...body.deployment, platform:'container', versionId:settings.RER_IMAGE_DIGEST ?? null }, container:{ engine:manifest.engine,engineVersion,deadJobs,runner:manifest.runner ? (runner?.running() ? 'running' : 'stopped') : null } }, { status:deadJobs || (manifest.runner && runnerEnabled && !runner?.running()) ? 503 : response.status,headers:response.headers });
|
|
26
|
+
}
|
|
27
|
+
outgoing(response, res);
|
|
28
|
+
} catch (cause) { res.statusCode = cause.status ?? 500; res.end('Container request failed'); console.error('Container request failed:', cause.name); }
|
|
29
|
+
});
|
|
30
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port,'0.0.0.0',resolve); });
|
|
31
|
+
let ticking=false;
|
|
32
|
+
if(automaticTicks){timer = setInterval(async () => { if(ticking)return;ticking=true;const minute = Math.floor(Date.now()/60000); try { if (minute !== lastMinute) { lastMinute = minute; await tick(minute * 60000); } else await engine.drain(); } catch { console.error('Container scheduled event failed'); }finally{ticking=false;} },1000); timer.unref();}
|
|
33
|
+
const close = () => closing ??= (async () => { clearInterval(timer); clearInterval(idleTimer); await runner?.close(); await new Promise(resolve => server.close(resolve)); await engine.close(); await release(); })();
|
|
34
|
+
const idle=Number(settings.RER_CONTAINER_IDLE_SECONDS);if(Number.isFinite(idle)&&idle>0){idleTimer=setInterval(()=>{if(Date.now()-lastRequest>=idle*1000)void close();},Math.min(1000,idle*1000));idleTimer.unref();}
|
|
35
|
+
if(runnerEnabled)runner=superviseRunner({root,manifest,settings,failed(){console.error('Required container runner stopped');void close().finally(()=>{process.exitCode=1;});}});
|
|
36
|
+
return { engine,manifest,server,tick,close };
|
|
37
|
+
} catch (cause) { clearInterval(timer); clearInterval(idleTimer); await runner?.close(); server?.close(); await engine?.close(); await release(); throw cause; }
|
|
38
|
+
}
|
|
39
|
+
if (process.argv[1] === import.meta.filename) {
|
|
40
|
+
let host;
|
|
41
|
+
try { host = await startHost(); console.log('RER_CONTAINER_READY'); } catch (cause) { console.error('Container startup failed:', cause.message?.match(/^[a-z-]+$/)?.[0] ?? cause.name); process.exitCode=1; }
|
|
42
|
+
if (host) for (const signal of ['SIGINT','SIGTERM']) process.once(signal,async () => { await host.close(); process.exit(0); });
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { lookup } from 'node:dns/promises';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
export async function incoming(req, { trustedProxy, maxRequestBytes = 25 * 1024 * 1024 } = {}) {
|
|
5
|
+
const headers = new Headers(); for (const [name, value] of Object.entries(req.headers)) if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(',') : value);
|
|
6
|
+
const peer = req.socket.remoteAddress?.replace('::ffff:', '') ?? '';
|
|
7
|
+
const trusted = trustedProxy && (isIP(trustedProxy) ? peer===trustedProxy : (await lookup(trustedProxy,{all:true})).some(item=>item.address.replace('::ffff:','')===peer));
|
|
8
|
+
let host = req.headers.host, proto = 'http', address = peer;
|
|
9
|
+
if (trusted) { host = req.headers['x-forwarded-host'] ?? host; proto = req.headers['x-forwarded-proto'] ?? proto; address = req.headers['x-forwarded-for']?.split(',').at(-1)?.trim() ?? peer; }
|
|
10
|
+
for (const name of ['cf-connecting-ip','x-forwarded-host','x-forwarded-proto','x-forwarded-for','forwarded','x-rer-original-url']) headers.delete(name);
|
|
11
|
+
if (!['http', 'https'].includes(proto) || typeof host !== 'string' || !/^(?:[a-zA-Z0-9.-]+|\[[0-9a-fA-F:]+\])(?::[0-9]+)?$/.test(host)) throw new Error('Invalid ingress origin');
|
|
12
|
+
headers.set('cf-connecting-ip', address); headers.set('host', host);
|
|
13
|
+
const url = `${proto}://${host}${req.url}`;
|
|
14
|
+
const chunks = []; let size = 0;
|
|
15
|
+
for await (const chunk of req) { size += chunk.length; if (size > maxRequestBytes) { const error = new Error('Request too large'); error.status = 413; throw error; } chunks.push(chunk); }
|
|
16
|
+
return new Request(url, { method: req.method, headers, ...(!['GET','HEAD'].includes(req.method) ? { body: Buffer.concat(chunks) } : {}) });
|
|
17
|
+
}
|
|
18
|
+
export function outgoing(response, res) {
|
|
19
|
+
res.statusCode = response.status;
|
|
20
|
+
for (const [name, value] of response.headers) if (!['set-cookie','content-length','transfer-encoding'].includes(name)) res.setHeader(name, value);
|
|
21
|
+
const cookies = response.headers.getSetCookie(); if (cookies.length) res.setHeader('set-cookie', cookies);
|
|
22
|
+
if (response.body) { const stream = Readable.fromWeb(response.body); res.once('close', () => stream.destroy()); stream.once('error', () => res.destroy()); stream.pipe(res); } else res.end();
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// entrypoint.sh holds a kernel flock on this inode for the entire process lifetime.
|
|
4
|
+
// It works across container PID namespaces and releases automatically on SIGKILL.
|
|
5
|
+
// Never unlink the lock inode: a new inode would permit a second live holder.
|
|
6
|
+
export async function recordVolumeOwner(directory) {
|
|
7
|
+
const file = await open(join(directory, 'host.lock'), 'r+', 0o600);
|
|
8
|
+
const write = async () => { const bytes = Buffer.from(JSON.stringify({ pid: process.pid, heartbeat: new Date().toISOString() })); await file.truncate(0); await file.write(bytes, 0, bytes.length, 0); await file.sync(); };
|
|
9
|
+
await write(); const timer = setInterval(() => void write().catch(() => { process.exitCode = 1; }), 10000); timer.unref();
|
|
10
|
+
return async () => { clearInterval(timer); await file.close(); };
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { validateMigrations } from '../deploy-kit/migrations.mjs';
|
|
2
|
+
export async function migrate(db, migrations, { ledger = '_rer_deploy_migrations' } = {}) {
|
|
3
|
+
if (!['_rer_deploy_migrations', '_rer_portability_migrations'].includes(ledger)) throw new Error('Invalid migration ledger');
|
|
4
|
+
validateMigrations(migrations);
|
|
5
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS ${ledger}(name TEXT PRIMARY KEY,sha256 TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('applying','applied')))`).run();
|
|
6
|
+
const history = (await db.prepare(`SELECT name,sha256,state FROM ${ledger} ORDER BY name`).all()).results;
|
|
7
|
+
if (history.some((row, i) => row.name !== migrations[i]?.name || row.sha256 !== migrations[i]?.sha256 || row.state !== 'applied')) throw new Error('changed-or-uncertain-migration-history');
|
|
8
|
+
const legacy = await db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='d1_migrations'").first();
|
|
9
|
+
if (ledger === '_rer_deploy_migrations' && legacy && (await db.prepare('SELECT name FROM d1_migrations ORDER BY id').all()).results.some(row => !history.some(prior => prior.name === row.name))) throw new Error('legacy-migration-history-needs-reviewed-hash-adoption');
|
|
10
|
+
for (const migration of migrations.slice(history.length)) {
|
|
11
|
+
await db.prepare(`INSERT INTO ${ledger} VALUES(?,?,'applying')`).bind(migration.name, migration.sha256).run();
|
|
12
|
+
// A claim commits separately. Application and completion share one transaction.
|
|
13
|
+
await db.batch([...migration.statements.map(sql => db.prepare(sql)), db.prepare(`UPDATE ${ledger} SET state='applied' WHERE name=? AND sha256=? AND state='applying'`).bind(migration.name, migration.sha256)]);
|
|
14
|
+
}
|
|
15
|
+
return { applied: migrations.length, pending: 0 };
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { D1 } from './adapters/d1.mjs';
|
|
5
|
+
import { Jobs } from './adapters/jobs.mjs';
|
|
6
|
+
import { Objects } from './adapters/objects.mjs';
|
|
7
|
+
import { retainedCache } from './adapters/retained.mjs';
|
|
8
|
+
import { publicFetch } from './adapters/egress.mjs';
|
|
9
|
+
import { assets } from './assets.mjs';
|
|
10
|
+
import { ensurePortabilitySchema } from './portability-schema.mjs';
|
|
11
|
+
import { migrate } from './migrations.mjs';
|
|
12
|
+
export async function nodeEngine({ root, data, manifest, settings, edge }) {
|
|
13
|
+
const platform = new DatabaseSync(join(data, 'platform.sqlite')); platform.exec('PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON');
|
|
14
|
+
const env = { ...manifest.config.vars, ...settings, ASSETS: assets(join(root, 'dist/client')) }, databases = {}, queues = [];
|
|
15
|
+
for (const [binding, plan] of Object.entries(manifest.migrations)) { const db = new D1(join(data, binding + '.sqlite')); databases[binding] = db; env[binding] = db; await migrate(db, plan); await ensurePortabilitySchema(db, manifest.portabilityMigrations); }
|
|
16
|
+
const objects = {};
|
|
17
|
+
for (const bucket of manifest.config.r2_buckets ?? []) { const store = new Objects(platform, join(data, 'objects'), bucket.binding); objects[bucket.binding] = store; env[bucket.binding] = store; }
|
|
18
|
+
await Object.values(objects)[0]?.recover();
|
|
19
|
+
for (const producer of manifest.config.queues?.producers ?? []) { const consumer = manifest.config.queues.consumers.find(item => item.queue === producer.queue); const jobs = new Jobs(platform, producer.queue, { maxRetries: consumer?.max_retries, batchSize: consumer?.max_batch_size }); env[producer.binding] = jobs; queues.push(jobs); }
|
|
20
|
+
if (manifest.config.durable_objects?.bindings?.length) {
|
|
21
|
+
for (const binding of manifest.config.durable_objects.bindings) { if (manifest.cacheContract !== 'lamp-retained-v1' || binding.class_name !== 'SnapshotCache') throw new Error('Unsupported Node binding'); env[binding.name] = retainedCache(platform); }
|
|
22
|
+
}
|
|
23
|
+
globalThis[Symbol.for('rer.container.bindings')] = env;
|
|
24
|
+
const previousFetch = globalThis.fetch; globalThis.fetch = edge ? (input, init) => edge(new Request(input, init), publicFetch) : publicFetch;
|
|
25
|
+
// Lamp uses its persistent retained contract; the optional Cache API is deliberately unsupported.
|
|
26
|
+
globalThis.caches = { open: async () => { throw new Error('Cache API unavailable on container Node'); } };
|
|
27
|
+
const app = (await import(pathToFileURL(join(root, manifest.entry)))).default;
|
|
28
|
+
const pending = new Set(), execution = { waitUntil(promise) { pending.add(promise); promise.catch(() => console.error('Background effect failed')).finally(() => pending.delete(promise)); } };
|
|
29
|
+
return { env, databases, objects, app, execution,
|
|
30
|
+
async fetch(request) {
|
|
31
|
+
if(['GET','HEAD'].includes(request.method)&&!manifest.config.assets?.run_worker_first){const response=await env.ASSETS.fetch(request);if(response.status!==404)return response;}
|
|
32
|
+
return app.fetch(request,env,execution);
|
|
33
|
+
},
|
|
34
|
+
scheduled: event => app.scheduled?.(event, env, execution),
|
|
35
|
+
async drain() { if (app.queue) for (const jobs of queues) await jobs.drain(app.queue.bind(app), env, execution); },
|
|
36
|
+
dead: () => queues.reduce((sum, jobs) => sum + jobs.dead(), 0),
|
|
37
|
+
async close() { await Promise.allSettled([...pending]); await Promise.all(Object.values(databases).map(db => db.close())); platform.close(); globalThis.fetch = previousFetch; },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { migrate } from './migrations.mjs';
|
|
4
|
+
export async function portabilityPlan(split) {
|
|
5
|
+
const sql = await readFile(new URL('../portability/schema/portability.sql', import.meta.url), 'utf8');
|
|
6
|
+
return [{ name:'0001_portability.sql', sql, sha256:createHash('sha256').update(sql).digest('hex'), statements:split(sql) }];
|
|
7
|
+
}
|
|
8
|
+
// Existing product-owned bookkeeping remains under its original migrations.
|
|
9
|
+
// Independently installed bookkeeping cannot interleave later product migrations.
|
|
10
|
+
export async function ensurePortabilitySchema(db, plan) {
|
|
11
|
+
const owned = await db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='_rer_portability_migrations'").first();
|
|
12
|
+
if (!owned && await db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='rer_portability_state'").first()) return;
|
|
13
|
+
await migrate(db, plan, { ledger:'_rer_portability_migrations' });
|
|
14
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def deploy():
|
|
5
|
+
payload = json.load(sys.stdin)
|
|
6
|
+
plan = payload['plan']
|
|
7
|
+
install, lock, path, chain = open_install(plan)
|
|
8
|
+
stage = generation_fd = None
|
|
9
|
+
previous, pending = {}, {}
|
|
10
|
+
try:
|
|
11
|
+
# Retain cleanup identities before Docker/other asynchronous work can
|
|
12
|
+
# give another process an opportunity to substitute private paths.
|
|
13
|
+
for entry in os.listdir(install):
|
|
14
|
+
if re.fullmatch(r'(generation|stage)-[0-9a-f]{32}', entry):
|
|
15
|
+
previous[entry] = retain_generation(install, entry)
|
|
16
|
+
elif re.fullmatch(r'current-[0-9a-f]{32}', entry):
|
|
17
|
+
pending[entry] = file_info(install, entry)
|
|
18
|
+
if pending[entry] is None:
|
|
19
|
+
refuse('Host deployment path changed')
|
|
20
|
+
manifest = retain_file(install, 'current.json')
|
|
21
|
+
def validate_previous():
|
|
22
|
+
for entry, (fd, entries) in previous.items():
|
|
23
|
+
validate_generation_entries(install, entry, fd, entries)
|
|
24
|
+
for entry, created in pending.items():
|
|
25
|
+
same_inode(created, file_info(install, entry))
|
|
26
|
+
same_file(install, 'current.json', manifest)
|
|
27
|
+
owned, containers = inventory(plan)
|
|
28
|
+
admit_ports(plan, owned, containers)
|
|
29
|
+
if payload.get('phase') == 'check':
|
|
30
|
+
print('{}')
|
|
31
|
+
return
|
|
32
|
+
stage, generation, generation_fd, records = stage_files(install, payload['files'])
|
|
33
|
+
caddy = prepare_resources(plan, owned)
|
|
34
|
+
# Recheck destinations and all resource ownership immediately before
|
|
35
|
+
# committing a complete generation and before the first shutdown.
|
|
36
|
+
inventory(plan)
|
|
37
|
+
validate_install(chain, lock, plan)
|
|
38
|
+
validate_previous()
|
|
39
|
+
validate_generation(install, stage, generation_fd, records)
|
|
40
|
+
publish_generation(install, stage, generation)
|
|
41
|
+
stage = generation
|
|
42
|
+
manifest = commit_manifest(install, generation, manifest)
|
|
43
|
+
stage = None
|
|
44
|
+
def validate():
|
|
45
|
+
validate_install(chain, lock, plan)
|
|
46
|
+
validate_previous()
|
|
47
|
+
validate_generation(install, generation, generation_fd, records)
|
|
48
|
+
replace_containers(plan, validate, payload['files'], caddy)
|
|
49
|
+
validate()
|
|
50
|
+
for entry, (fd, entries) in previous.items():
|
|
51
|
+
remove_generation(install, entry, fd, entries)
|
|
52
|
+
for entry, created in pending.items():
|
|
53
|
+
same_inode(created, file_info(install, entry))
|
|
54
|
+
os.unlink(entry, dir_fd=install)
|
|
55
|
+
print(json.dumps({'installation': path, 'generation': generation}))
|
|
56
|
+
finally:
|
|
57
|
+
try:
|
|
58
|
+
if stage:
|
|
59
|
+
remove_generation(install, stage, generation_fd, {filename: created for filename, (_, created) in records.items()})
|
|
60
|
+
finally:
|
|
61
|
+
if generation_fd is not None:
|
|
62
|
+
os.close(generation_fd)
|
|
63
|
+
for fd, _ in previous.values():
|
|
64
|
+
os.close(fd)
|
|
65
|
+
for _, fd, _ in reversed(chain):
|
|
66
|
+
os.close(fd)
|
|
67
|
+
os.close(lock)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
deploy()
|
|
72
|
+
except BaseException as error:
|
|
73
|
+
# No exceptions from parsers or subprocesses may echo private inputs.
|
|
74
|
+
message = str(error) if isinstance(error, RuntimeError) else 'Host deployment refused: unsafe path or failed operation'
|
|
75
|
+
print(message, file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Private dirfd custody, including verification of the live pathname chain."""
|
|
2
|
+
import os, stat, json, pwd, uuid, fcntl, re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def refuse(message):
|
|
6
|
+
raise RuntimeError(message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def metadata(info, directory=False, ancestor=False):
|
|
10
|
+
kind = stat.S_ISDIR if directory else stat.S_ISREG
|
|
11
|
+
owners = {0, os.getuid()} if ancestor else {os.getuid()}
|
|
12
|
+
mask = 0o022 if ancestor else 0o077
|
|
13
|
+
if not kind(info.st_mode) or info.st_uid not in owners or info.st_mode & mask or (not directory and info.st_nlink != 1):
|
|
14
|
+
refuse('Unsafe host path ownership, permissions or type')
|
|
15
|
+
return info
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def checked(fd, directory=False, ancestor=False):
|
|
19
|
+
return metadata(os.fstat(fd), directory, ancestor)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def same_inode(left, right):
|
|
23
|
+
if right is None or (left.st_dev, left.st_ino) != (right.st_dev, right.st_ino):
|
|
24
|
+
refuse('Host deployment path changed')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def directory(parent, name, create=False, ancestor=False):
|
|
28
|
+
if create:
|
|
29
|
+
try:
|
|
30
|
+
os.mkdir(name, 0o700, dir_fd=parent)
|
|
31
|
+
except FileExistsError:
|
|
32
|
+
pass
|
|
33
|
+
fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent)
|
|
34
|
+
try:
|
|
35
|
+
checked(fd, True, ancestor)
|
|
36
|
+
return fd
|
|
37
|
+
except BaseException:
|
|
38
|
+
os.close(fd)
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def file_info(parent, name):
|
|
43
|
+
try:
|
|
44
|
+
metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
|
|
45
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return checked(fd)
|
|
50
|
+
finally:
|
|
51
|
+
os.close(fd)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_json(parent, name):
|
|
55
|
+
metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
|
|
56
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
|
|
57
|
+
try:
|
|
58
|
+
checked(fd)
|
|
59
|
+
with os.fdopen(os.dup(fd)) as stream:
|
|
60
|
+
return json.load(stream)
|
|
61
|
+
finally:
|
|
62
|
+
os.close(fd)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def exclusive(parent, name, content):
|
|
66
|
+
fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=parent)
|
|
67
|
+
try:
|
|
68
|
+
checked(fd)
|
|
69
|
+
with os.fdopen(os.dup(fd), 'wb') as stream:
|
|
70
|
+
stream.write(content.encode())
|
|
71
|
+
stream.flush()
|
|
72
|
+
os.fsync(stream.fileno())
|
|
73
|
+
return checked(fd)
|
|
74
|
+
finally:
|
|
75
|
+
os.close(fd)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def retain_file(parent, name):
|
|
79
|
+
# Identity plus exact bytes: a same-UID file transplanted from another
|
|
80
|
+
# installation passes every ownership and permission check.
|
|
81
|
+
try:
|
|
82
|
+
metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
|
|
83
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
info = checked(fd)
|
|
88
|
+
with os.fdopen(os.dup(fd), 'rb') as stream:
|
|
89
|
+
return info, stream.read(65537)
|
|
90
|
+
finally:
|
|
91
|
+
os.close(fd)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def same_file(parent, name, retained):
|
|
95
|
+
current = retain_file(parent, name)
|
|
96
|
+
if retained is None or current is None:
|
|
97
|
+
if retained is not current:
|
|
98
|
+
refuse('Host deployment path changed')
|
|
99
|
+
return
|
|
100
|
+
same_inode(retained[0], current[0])
|
|
101
|
+
if current[1] != retained[1]:
|
|
102
|
+
refuse('Host deployment file changed')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def open_install(plan):
|
|
106
|
+
# Do not resolve symlinks before checking them. System ancestors may belong
|
|
107
|
+
# to root; the account home and private deployment tree must belong to uid.
|
|
108
|
+
home = pwd.getpwuid(os.getuid()).pw_dir
|
|
109
|
+
if not home.startswith('/') or any(part in ('.', '..') for part in home.split('/')):
|
|
110
|
+
refuse('Unsafe account home')
|
|
111
|
+
fd = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
112
|
+
checked(fd, True, True)
|
|
113
|
+
chain = [('/', fd, True)]
|
|
114
|
+
for part in filter(None, home.split('/')):
|
|
115
|
+
nxt = directory(fd, part, ancestor=True)
|
|
116
|
+
chain.append((part, nxt, True))
|
|
117
|
+
fd = nxt
|
|
118
|
+
info = os.fstat(fd)
|
|
119
|
+
if info.st_uid != os.getuid():
|
|
120
|
+
refuse('Account home is not owned by login user')
|
|
121
|
+
base = directory(fd, '.rer-host', create=True)
|
|
122
|
+
chain.append(('.rer-host', base, False))
|
|
123
|
+
# One host-account lock also serializes creation and binding admission for
|
|
124
|
+
# cooperating installs. The pathname chain is checked independently of it.
|
|
125
|
+
file_info(base, 'deploy.lock')
|
|
126
|
+
lock = os.open('deploy.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600, dir_fd=base)
|
|
127
|
+
checked(lock)
|
|
128
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
129
|
+
created = False
|
|
130
|
+
try:
|
|
131
|
+
os.mkdir(plan['name'], 0o700, dir_fd=base)
|
|
132
|
+
created = True
|
|
133
|
+
except FileExistsError:
|
|
134
|
+
pass
|
|
135
|
+
install = directory(base, plan['name'])
|
|
136
|
+
chain.append((plan['name'], install, False))
|
|
137
|
+
if created:
|
|
138
|
+
exclusive(install, 'owner.json', json.dumps(plan['labels']))
|
|
139
|
+
if read_json(install, 'owner.json') != plan['labels']:
|
|
140
|
+
refuse('Installation ownership mismatch')
|
|
141
|
+
validate_install(chain, lock, plan)
|
|
142
|
+
return install, lock, home + '/.rer-host/' + plan['name'], chain
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def validate_install(chain, lock, plan):
|
|
146
|
+
# A retained dirfd remains usable after rename. Check each original edge
|
|
147
|
+
# from / as well, so it cannot silently become a detached installation.
|
|
148
|
+
parent = None
|
|
149
|
+
for name, fd, ancestor in chain:
|
|
150
|
+
info = checked(fd, True, ancestor)
|
|
151
|
+
if parent is not None:
|
|
152
|
+
linked = metadata(os.stat(name, dir_fd=parent, follow_symlinks=False), True, ancestor)
|
|
153
|
+
same_inode(info, linked)
|
|
154
|
+
parent = fd
|
|
155
|
+
if os.fstat(chain[-3][1]).st_uid != os.getuid():
|
|
156
|
+
refuse('Account home is not owned by login user')
|
|
157
|
+
same_inode(checked(lock), file_info(chain[-2][1], 'deploy.lock'))
|
|
158
|
+
install = chain[-1][1]
|
|
159
|
+
if read_json(install, 'owner.json') != plan['labels']:
|
|
160
|
+
refuse('Installation ownership mismatch')
|
|
161
|
+
validate_tree(install)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def validate_tree(install):
|
|
165
|
+
checked(install, True)
|
|
166
|
+
for entry in os.listdir(install):
|
|
167
|
+
if entry in ('owner.json', 'current.json'):
|
|
168
|
+
file_info(install, entry)
|
|
169
|
+
elif re.fullmatch(r'current-[0-9a-f]{32}', entry):
|
|
170
|
+
file_info(install, entry)
|
|
171
|
+
elif re.fullmatch(r'(generation|stage)-[0-9a-f]{32}', entry):
|
|
172
|
+
child = directory(install, entry)
|
|
173
|
+
try:
|
|
174
|
+
for name in os.listdir(child):
|
|
175
|
+
if name not in ('settings.env', 'compose.yaml', 'Caddyfile'):
|
|
176
|
+
refuse('Unexpected deployment file')
|
|
177
|
+
file_info(child, name)
|
|
178
|
+
finally:
|
|
179
|
+
os.close(child)
|
|
180
|
+
else:
|
|
181
|
+
refuse('Unexpected installation path')
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def stage_files(install, files):
|
|
185
|
+
token = uuid.uuid4().hex
|
|
186
|
+
stage = 'stage-' + token
|
|
187
|
+
os.mkdir(stage, 0o700, dir_fd=install)
|
|
188
|
+
fd = directory(install, stage)
|
|
189
|
+
try:
|
|
190
|
+
records = {}
|
|
191
|
+
for name, contents in files.items():
|
|
192
|
+
records[name] = (contents, exclusive(fd, name, contents))
|
|
193
|
+
os.fsync(fd)
|
|
194
|
+
return stage, 'generation-' + token, fd, records
|
|
195
|
+
except BaseException:
|
|
196
|
+
os.close(fd)
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def validate_generation(install, name, fd, records):
|
|
201
|
+
validate_generation_entries(install, name, fd, {filename: created for filename, (_, created) in records.items()})
|
|
202
|
+
for filename, (contents, created) in records.items():
|
|
203
|
+
file = os.open(filename, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=fd)
|
|
204
|
+
try:
|
|
205
|
+
same_inode(created, checked(file))
|
|
206
|
+
with os.fdopen(os.dup(file), 'rb') as stream:
|
|
207
|
+
if stream.read(len(contents.encode()) + 1) != contents.encode():
|
|
208
|
+
refuse('Host deployment file changed')
|
|
209
|
+
finally:
|
|
210
|
+
os.close(file)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_generation_entries(install, name, fd, entries):
|
|
214
|
+
same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
|
|
215
|
+
if set(os.listdir(fd)) != set(entries):
|
|
216
|
+
refuse('Unexpected deployment file')
|
|
217
|
+
for filename, created in entries.items():
|
|
218
|
+
same_inode(created, file_info(fd, filename))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def retain_generation(install, name):
|
|
222
|
+
fd = directory(install, name)
|
|
223
|
+
try:
|
|
224
|
+
entries = {}
|
|
225
|
+
for filename in os.listdir(fd):
|
|
226
|
+
if filename not in ('settings.env', 'compose.yaml', 'Caddyfile'):
|
|
227
|
+
refuse('Unexpected deployment file')
|
|
228
|
+
info = file_info(fd, filename)
|
|
229
|
+
if info is None:
|
|
230
|
+
refuse('Host deployment path changed')
|
|
231
|
+
entries[filename] = info
|
|
232
|
+
validate_generation_entries(install, name, fd, entries)
|
|
233
|
+
return fd, entries
|
|
234
|
+
except BaseException:
|
|
235
|
+
os.close(fd)
|
|
236
|
+
raise
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def publish_generation(install, stage, generation):
|
|
240
|
+
validate_tree(install)
|
|
241
|
+
os.rename(stage, generation, src_dir_fd=install, dst_dir_fd=install)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def commit_manifest(install, generation, current):
|
|
245
|
+
# A single atomic commit points to a complete generation; interrupted
|
|
246
|
+
# transfers cannot make an existing installation run partial configuration.
|
|
247
|
+
# Only the manifest retained before asynchronous work may be replaced.
|
|
248
|
+
content = json.dumps({'generation': generation})
|
|
249
|
+
pending = 'current-' + uuid.uuid4().hex
|
|
250
|
+
committed = (exclusive(install, pending, content), content.encode())
|
|
251
|
+
try:
|
|
252
|
+
same_file(install, pending, committed)
|
|
253
|
+
same_file(install, 'current.json', current)
|
|
254
|
+
os.replace(pending, 'current.json', src_dir_fd=install, dst_dir_fd=install)
|
|
255
|
+
os.fsync(install)
|
|
256
|
+
same_file(install, 'current.json', committed)
|
|
257
|
+
return committed
|
|
258
|
+
finally:
|
|
259
|
+
remaining = file_info(install, pending)
|
|
260
|
+
if remaining is not None:
|
|
261
|
+
same_inode(committed[0], remaining)
|
|
262
|
+
os.unlink(pending, dir_fd=install)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def remove_generation(install, name, fd, entries):
|
|
266
|
+
# A private path can still have been replaced by the same login UID.
|
|
267
|
+
# Validate the whole retained generation before removing any member, then
|
|
268
|
+
# require the captured identity again immediately before each deletion.
|
|
269
|
+
validate_generation_entries(install, name, fd, entries)
|
|
270
|
+
for entry, created in entries.items():
|
|
271
|
+
same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
|
|
272
|
+
same_inode(created, file_info(fd, entry))
|
|
273
|
+
os.unlink(entry, dir_fd=fd)
|
|
274
|
+
same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
|
|
275
|
+
os.rmdir(name, dir_fd=install)
|