@runeditrun/rer 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/bin/rer.mjs +3 -0
- package/cli/README.md +243 -0
- package/cli/adapter.mjs +10 -0
- package/cli/ai-local.mjs +65 -0
- package/cli/archive-path.mjs +52 -0
- package/cli/archive-vite.mjs +55 -0
- package/cli/archive.mjs +8 -0
- package/cli/auth-copy.mjs +80 -0
- package/cli/bounded.mjs +70 -0
- package/cli/cache-doctor.mjs +63 -0
- package/cli/container-transfer.mjs +35 -0
- package/cli/container.mjs +62 -0
- package/cli/contract.mjs +25 -0
- package/cli/dependencies.mjs +45 -0
- package/cli/dependency-approval.mjs +33 -0
- package/cli/dependency-doctor.mjs +34 -0
- package/cli/dependency-input.mjs +43 -0
- package/cli/deploy-config.mjs +118 -0
- package/cli/deploy.mjs +37 -0
- package/cli/dev-identity.mjs +80 -0
- package/cli/errors.mjs +14 -0
- package/cli/export.mjs +42 -0
- package/cli/font-cache.mjs +29 -0
- package/cli/hosted-output.mjs +110 -0
- package/cli/independent.mjs +29 -0
- package/cli/inspect.mjs +174 -0
- package/cli/jsonc.mjs +111 -0
- package/cli/lifecycle.mjs +148 -0
- package/cli/local.mjs +100 -0
- package/cli/main.mjs +57 -0
- package/cli/managed-server.mjs +143 -0
- package/cli/node-runtime-cache.mjs +68 -0
- package/cli/node-runtime-discovery.mjs +56 -0
- package/cli/node-runtime-download.mjs +59 -0
- package/cli/node-runtime.mjs +50 -0
- package/cli/output.mjs +22 -0
- package/cli/parse.mjs +77 -0
- package/cli/port.mjs +52 -0
- package/cli/portability-worker.mjs +56 -0
- package/cli/portability.mjs +66 -0
- package/cli/preview-cache.mjs +99 -0
- package/cli/project.mjs +48 -0
- package/cli/release.mjs +24 -0
- package/cli/runtime-front-door.mjs +46 -0
- package/cli/scheduled.mjs +81 -0
- package/cli/server-activity.mjs +12 -0
- package/cli/server-proxy.mjs +93 -0
- package/cli/server-registry.mjs +182 -0
- package/cli/server-resources.mjs +22 -0
- package/cli/settings.mjs +15 -0
- package/cli/setup-answers.mjs +29 -0
- package/cli/setup-input.mjs +113 -0
- package/cli/shared.mjs +7 -0
- package/cli/transfers.mjs +33 -0
- package/cli/version.mjs +36 -0
- package/composition/git-source.mjs +111 -0
- package/edition-authoring/files.mjs +81 -0
- package/edition-authoring/manifest.mjs +65 -0
- package/global/README.md +158 -0
- package/global/checkout.mjs +72 -0
- package/global/cli/command.mjs +13 -0
- package/global/cli/install.mjs +75 -0
- package/global/cli/layout.mjs +45 -0
- package/global/cli/status.mjs +54 -0
- package/global/cli/uninstall.mjs +36 -0
- package/global/delegate.mjs +13 -0
- package/global/launch/capacity.mjs +72 -0
- package/global/launch/dev.mjs +117 -0
- package/global/launch/facts.mjs +94 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +63 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +106 -0
- package/global/launch/steps.mjs +90 -0
- package/global/launch/stop.mjs +89 -0
- package/global/launch/worker.mjs +62 -0
- package/global/list.mjs +59 -0
- package/global/main.mjs +53 -0
- package/global/package-info.mjs +38 -0
- package/global/registry.mjs +24 -0
- package/global/rer.mjs +29 -0
- package/global/update/archive.mjs +89 -0
- package/global/update/command.mjs +90 -0
- package/global/update/latest.mjs +6 -0
- package/global/update/notice.mjs +58 -0
- package/global/update/registry.mjs +47 -0
- package/global/version.mjs +29 -0
- package/install.sh +144 -0
- package/modules/ai/LICENSE +21 -0
- package/modules/ai/README.md +90 -0
- package/modules/ai/client.mjs +71 -0
- package/modules/ai/config.mjs +39 -0
- package/modules/ai/dev/index.d.mts +6 -0
- package/modules/ai/dev/index.mjs +64 -0
- package/modules/ai/dev/outbox.mjs +33 -0
- package/modules/ai/errors.mjs +28 -0
- package/modules/ai/fake/index.d.mts +3 -0
- package/modules/ai/fake/index.mjs +82 -0
- package/modules/ai/index.d.mts +10 -0
- package/modules/ai/index.mjs +3 -0
- package/modules/ai/package.json +6 -0
- package/modules/container/README.md +156 -0
- package/modules/container/adapters/d1.mjs +34 -0
- package/modules/container/adapters/egress.mjs +40 -0
- package/modules/container/adapters/jobs.mjs +47 -0
- package/modules/container/adapters/objects.mjs +84 -0
- package/modules/container/adapters/retained.mjs +14 -0
- package/modules/container/adapters/sqlite-error.mjs +7 -0
- package/modules/container/adapters/sqlite-worker.mjs +26 -0
- package/modules/container/assets.mjs +14 -0
- package/modules/container/bindings.mjs +1 -0
- package/modules/container/boundary.mjs +38 -0
- package/modules/container/build.mjs +65 -0
- package/modules/container/deploy-input.mjs +17 -0
- package/modules/container/entrypoint.sh +5 -0
- package/modules/container/host.mjs +43 -0
- package/modules/container/ingress.mjs +23 -0
- package/modules/container/lock.mjs +11 -0
- package/modules/container/migrations.mjs +16 -0
- package/modules/container/node-engine.mjs +39 -0
- package/modules/container/portability-schema.mjs +14 -0
- package/modules/container/remote/deploy.py +76 -0
- package/modules/container/remote/files.py +275 -0
- package/modules/container/remote/resources.py +157 -0
- package/modules/container/runner.mjs +10 -0
- package/modules/container/ssh-plan.mjs +44 -0
- package/modules/container/ssh.mjs +63 -0
- package/modules/container/transfer.mjs +27 -0
- package/modules/container/worker-engine.mjs +32 -0
- package/modules/dependencies/README.md +211 -0
- package/modules/dependencies/approval.mjs +51 -0
- package/modules/dependencies/auth.mjs +40 -0
- package/modules/dependencies/authority.mjs +14 -0
- package/modules/dependencies/crypto.mjs +13 -0
- package/modules/dependencies/index.d.mts +23 -0
- package/modules/dependencies/index.mjs +113 -0
- package/modules/dependencies/logout.mjs +42 -0
- package/modules/dependencies/native.mjs +9 -0
- package/modules/dependencies/protocol.mjs +23 -0
- package/modules/dependencies/schema/approval-attempt.sql +3 -0
- package/modules/dependencies/schema/dependencies.sql +15 -0
- package/modules/dependencies/store.mjs +19 -0
- package/modules/deploy-kit/README.md +308 -0
- package/modules/deploy-kit/artifact.mjs +47 -0
- package/modules/deploy-kit/build-modules.mjs +21 -0
- package/modules/deploy-kit/check.mjs +13 -0
- package/modules/deploy-kit/cli.mjs +50 -0
- package/modules/deploy-kit/container-config.mjs +8 -0
- package/modules/deploy-kit/default-stage.mjs +14 -0
- package/modules/deploy-kit/deploy.mjs +80 -0
- package/modules/deploy-kit/health-request.mjs +86 -0
- package/modules/deploy-kit/health.mjs +99 -0
- package/modules/deploy-kit/index.mjs +12 -0
- package/modules/deploy-kit/local-only.mjs +10 -0
- package/modules/deploy-kit/migrations.mjs +48 -0
- package/modules/deploy-kit/owner-cli.mjs +63 -0
- package/modules/deploy-kit/owner.mjs +297 -0
- package/modules/deploy-kit/prepare.mjs +62 -0
- package/modules/deploy-kit/provider.mjs +99 -0
- package/modules/deploy-kit/queue-consumer.mjs +10 -0
- package/modules/deploy-kit/receipt.mjs +16 -0
- package/modules/deploy-kit/render.mjs +47 -0
- package/modules/deploy-kit/resources.mjs +106 -0
- package/modules/deploy-kit/service-grants.mjs +25 -0
- package/modules/deploy-kit/sql-safety.mjs +94 -0
- package/modules/deploy-kit/sql-tokens.mjs +49 -0
- package/modules/deploy-kit/teardown.mjs +100 -0
- package/modules/deploy-kit/templates.mjs +31 -0
- package/modules/deploy-kit/validation.mjs +58 -0
- package/modules/deploy-kit/worker.mjs +105 -0
- package/modules/erasure/CONTRACT.md +90 -0
- package/modules/erasure/README.md +221 -0
- package/modules/erasure/audit.mjs +23 -0
- package/modules/erasure/blobs.mjs +45 -0
- package/modules/erasure/cascade.mjs +91 -0
- package/modules/erasure/completion.mjs +62 -0
- package/modules/erasure/declaration.mjs +51 -0
- package/modules/erasure/fence.mjs +19 -0
- package/modules/erasure/index.d.mts +62 -0
- package/modules/erasure/index.mjs +85 -0
- package/modules/erasure/order.mjs +56 -0
- package/modules/erasure/plan.mjs +121 -0
- package/modules/erasure/rate.mjs +57 -0
- package/modules/erasure/receipt.mjs +32 -0
- package/modules/erasure/root.mjs +21 -0
- package/modules/erasure/schema/erasure-rates.sql +5 -0
- package/modules/erasure/schema/erasure.sql +27 -0
- package/modules/erasure/scope.mjs +29 -0
- package/modules/erasure/sql.mjs +23 -0
- package/modules/erasure/verification.mjs +69 -0
- package/modules/integrations/core/CONTRACT.md +50 -0
- package/modules/integrations/core/README.md +27 -0
- package/modules/integrations/core/crypto.ts +19 -0
- package/modules/integrations/core/database.ts +21 -0
- package/modules/integrations/core/delivery.ts +97 -0
- package/modules/integrations/core/destination.ts +13 -0
- package/modules/integrations/core/index.ts +7 -0
- package/modules/integrations/core/node-transport.ts +72 -0
- package/modules/integrations/core/public-address.ts +57 -0
- package/modules/integrations/core/repository.ts +74 -0
- package/modules/integrations/core/schema.sql +36 -0
- package/modules/integrations/core/sqlite.ts +20 -0
- package/modules/integrations/core/timestamped-body-signature.ts +38 -0
- package/modules/integrations/core/tokens.ts +71 -0
- package/modules/integrations/core/types.ts +52 -0
- package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
- package/modules/integrations/core/webhook.ts +73 -0
- package/modules/job-queue/README.md +173 -0
- package/modules/job-queue/cli.mjs +64 -0
- package/modules/job-queue/command.mjs +64 -0
- package/modules/job-queue/config.mjs +48 -0
- package/modules/job-queue/identity.mjs +48 -0
- package/modules/job-queue/index.mjs +135 -0
- package/modules/job-queue/membership.mjs +30 -0
- package/modules/job-queue/module.json +10 -0
- package/modules/job-queue/policy.mjs +29 -0
- package/modules/job-queue/processes.mjs +80 -0
- package/modules/job-queue/resources.mjs +37 -0
- package/modules/job-queue/storage.mjs +64 -0
- package/modules/job-queue/supervisor.mjs +30 -0
- package/modules/job-queue/waiting.mjs +16 -0
- package/modules/mail/LICENSE +21 -0
- package/modules/mail/README.md +336 -0
- package/modules/mail/doctor.mjs +81 -0
- package/modules/mail/index.mjs +8 -0
- package/modules/mail/live-proof/README.md +113 -0
- package/modules/mail/live-proof/run.mjs +55 -0
- package/modules/mail/package.json +19 -0
- package/modules/mail/pnpm-lock.yaml +537 -0
- package/modules/mail/src/adapters/cloudflare/README.md +84 -0
- package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
- package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
- package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
- package/modules/mail/src/adapters/messaging/README.md +92 -0
- package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
- package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
- package/modules/mail/src/adapters/messaging/index.mjs +61 -0
- package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
- package/modules/mail/src/adapters/messaging/request.mjs +52 -0
- package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
- package/modules/mail/src/adapters/resend/README.md +120 -0
- package/modules/mail/src/adapters/resend/index.mjs +22 -0
- package/modules/mail/src/adapters/resend/response.mjs +54 -0
- package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
- package/modules/mail/src/adapters/resend/size.mjs +99 -0
- package/modules/mail/src/adapters/sendgrid/README.md +75 -0
- package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
- package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/core/transport.mjs +33 -0
- package/modules/mail/src/core/validation.mjs +172 -0
- package/modules/mail/src/development/index.d.mts +37 -0
- package/modules/mail/src/development/index.mjs +35 -0
- package/modules/mail/src/development/outbox.mjs +78 -0
- package/modules/mail/src/development/schema.sql +9 -0
- package/modules/mail/src/development/viewer-html.mjs +42 -0
- package/modules/mail/src/development/viewer.mjs +65 -0
- package/modules/mail/src/doctor/README.md +107 -0
- package/modules/mail/src/doctor/common.mjs +64 -0
- package/modules/mail/src/doctor/dns.mjs +102 -0
- package/modules/mail/src/doctor/index.mjs +28 -0
- package/modules/mail/src/doctor/probe.mjs +66 -0
- package/modules/mail/src/doctor/receive.mjs +81 -0
- package/modules/mail/src/doctor/records.mjs +45 -0
- package/modules/mail/src/events/README.md +268 -0
- package/modules/mail/src/events/d1-schema.sql +25 -0
- package/modules/mail/src/events/d1-storage.mjs +76 -0
- package/modules/mail/src/events/index.mjs +3 -0
- package/modules/mail/src/events/intake.mjs +201 -0
- package/modules/mail/src/events/storage.mjs +52 -0
- package/modules/mail/src/events/verify.mjs +92 -0
- package/modules/mail/src/inbound/README.md +137 -0
- package/modules/mail/src/inbound/bytes.mjs +91 -0
- package/modules/mail/src/inbound/headers.mjs +143 -0
- package/modules/mail/src/inbound/index.mjs +29 -0
- package/modules/mail/src/inbound/mime.mjs +36 -0
- package/modules/mail/src/inbound/multipart.mjs +27 -0
- package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
- package/modules/mail/src/inbound/resend.mjs +54 -0
- package/modules/mail/src/inbound/sendgrid.mjs +60 -0
- package/modules/mail/src/sender.mjs +33 -0
- package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
- package/modules/mail/src/sendgrid-email/index.mjs +86 -0
- package/modules/mail/src/unsubscribe/index.mjs +142 -0
- package/modules/operator-auth/README.md +599 -0
- package/modules/operator-auth/accounts/adapter.mjs +41 -0
- package/modules/operator-auth/accounts/admin.mjs +159 -0
- package/modules/operator-auth/accounts/credentials.mjs +27 -0
- package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
- package/modules/operator-auth/accounts/index.d.mts +9 -0
- package/modules/operator-auth/accounts/index.mjs +9 -0
- package/modules/operator-auth/accounts/native.d.mts +38 -0
- package/modules/operator-auth/accounts/native.mjs +171 -0
- package/modules/operator-auth/accounts/oa.mjs +90 -0
- package/modules/operator-auth/accounts/provider-control.mjs +50 -0
- package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
- package/modules/operator-auth/accounts/roles.mjs +26 -0
- package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
- package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
- package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
- package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
- package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
- package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
- package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
- package/modules/operator-auth/accounts/settings-page.mjs +4 -0
- package/modules/operator-auth/accounts/store.mjs +246 -0
- package/modules/operator-auth/branding.mjs +66 -0
- package/modules/operator-auth/config.mjs +35 -0
- package/modules/operator-auth/conformance/README.md +81 -0
- package/modules/operator-auth/conformance/index.d.mts +43 -0
- package/modules/operator-auth/conformance/index.mjs +300 -0
- package/modules/operator-auth/conformance/presentation.mjs +57 -0
- package/modules/operator-auth/crypto.mjs +17 -0
- package/modules/operator-auth/dev/badge.mjs +62 -0
- package/modules/operator-auth/dev/index.d.mts +14 -0
- package/modules/operator-auth/dev/index.mjs +137 -0
- package/modules/operator-auth/dev/personas.mjs +17 -0
- package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
- package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
- package/modules/operator-auth/dev-admission.d.mts +4 -0
- package/modules/operator-auth/dev-admission.mjs +10 -0
- package/modules/operator-auth/dev-gate.mjs +86 -0
- package/modules/operator-auth/docs/README.md +78 -0
- package/modules/operator-auth/docs/native-accounts.md +313 -0
- package/modules/operator-auth/docs/tanstack-start.md +218 -0
- package/modules/operator-auth/docs/vite-spa.md +144 -0
- package/modules/operator-auth/errors.mjs +9 -0
- package/modules/operator-auth/http.mjs +42 -0
- package/modules/operator-auth/identity/admission.mjs +105 -0
- package/modules/operator-auth/identity/after-confirm.mjs +18 -0
- package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
- package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
- package/modules/operator-auth/identity/email-link/index.mjs +128 -0
- package/modules/operator-auth/identity/email-link/message.mjs +16 -0
- package/modules/operator-auth/identity/index.d.mts +7 -0
- package/modules/operator-auth/identity/index.mjs +5 -0
- package/modules/operator-auth/identity/oidc/README.md +171 -0
- package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
- package/modules/operator-auth/identity/oidc/config.mjs +102 -0
- package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
- package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
- package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
- package/modules/operator-auth/identity/oidc/index.mjs +287 -0
- package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
- package/modules/operator-auth/identity/oidc/network.mjs +93 -0
- package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
- package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
- package/modules/operator-auth/identity/oidc/store.mjs +68 -0
- package/modules/operator-auth/identity/principal.mjs +28 -0
- package/modules/operator-auth/index.d.mts +223 -0
- package/modules/operator-auth/index.mjs +11 -0
- package/modules/operator-auth/input.mjs +55 -0
- package/modules/operator-auth/local-cli.mjs +16 -0
- package/modules/operator-auth/page.mjs +34 -0
- package/modules/operator-auth/policy.mjs +20 -0
- package/modules/operator-auth/routes.mjs +91 -0
- package/modules/operator-auth/service.mjs +152 -0
- package/modules/operator-auth/signin/index.mjs +67 -0
- package/modules/owner-claim/README.md +137 -0
- package/modules/owner-claim/idp.mjs +40 -0
- package/modules/owner-claim/index.d.mts +27 -0
- package/modules/owner-claim/index.mjs +39 -0
- package/modules/owner-claim/page.mjs +11 -0
- package/modules/owner-claim/schema/idp.sql +4 -0
- package/modules/owner-claim/schema/owner-claim.sql +18 -0
- package/modules/owner-claim/transaction.mjs +87 -0
- package/modules/owner-claim/verify.mjs +80 -0
- package/modules/portability/README.md +444 -0
- package/modules/portability/bridge.mjs +102 -0
- package/modules/portability/catalogue.mjs +118 -0
- package/modules/portability/cli-options.mjs +38 -0
- package/modules/portability/cli.mjs +56 -0
- package/modules/portability/copies.mjs +68 -0
- package/modules/portability/dev-data.mjs +32 -0
- package/modules/portability/diff.mjs +33 -0
- package/modules/portability/export.mjs +129 -0
- package/modules/portability/files.mjs +65 -0
- package/modules/portability/format.mjs +122 -0
- package/modules/portability/generated.mjs +60 -0
- package/modules/portability/hash.mjs +79 -0
- package/modules/portability/http.mjs +42 -0
- package/modules/portability/import.mjs +185 -0
- package/modules/portability/index.d.mts +58 -0
- package/modules/portability/index.mjs +22 -0
- package/modules/portability/journal.mjs +49 -0
- package/modules/portability/legacy-files.mjs +43 -0
- package/modules/portability/legacy-safety.mjs +27 -0
- package/modules/portability/lines.mjs +36 -0
- package/modules/portability/migration-history.mjs +18 -0
- package/modules/portability/migration-statements.mjs +23 -0
- package/modules/portability/payloads.mjs +52 -0
- package/modules/portability/provider-data.mjs +25 -0
- package/modules/portability/restore-safety.mjs +30 -0
- package/modules/portability/schema/portability.sql +25 -0
- package/modules/portability/storage.mjs +49 -0
- package/modules/release/README.md +212 -0
- package/modules/release/catalogue.mjs +79 -0
- package/modules/release/export.mjs +51 -0
- package/modules/release/produce.mjs +136 -0
- package/modules/release/public-child-transport-state.cjs +29 -0
- package/modules/release/public-child.mjs +71 -0
- package/modules/release/public-git.mjs +21 -0
- package/modules/release/publication-scan.mjs +22 -0
- package/modules/release/publish-handoff.mjs +37 -0
- package/modules/release/source.mjs +71 -0
- package/modules/release/test-summary.mjs +50 -0
- package/onboarding/README.md +163 -0
- package/onboarding/activity.mjs +31 -0
- package/onboarding/adapter-auth.mjs +88 -0
- package/onboarding/adapter-hosting.mjs +35 -0
- package/onboarding/adapter-labels.mjs +13 -0
- package/onboarding/adapter-settings.mjs +129 -0
- package/onboarding/adapter.mjs +238 -0
- package/onboarding/agent-handoff.mjs +9 -0
- package/onboarding/app-processes.mjs +21 -0
- package/onboarding/archive-output.d.mts +3 -0
- package/onboarding/archive-output.mjs +54 -0
- package/onboarding/command-custody.mjs +24 -0
- package/onboarding/command-recovery.mjs +8 -0
- package/onboarding/commands.mjs +166 -0
- package/onboarding/dependency-approval.mjs +36 -0
- package/onboarding/dev-identity.mjs +23 -0
- package/onboarding/email.mjs +24 -0
- package/onboarding/failures.mjs +42 -0
- package/onboarding/input-limits.mjs +7 -0
- package/onboarding/installer-inputs.mjs +24 -0
- package/onboarding/launcher.mjs +34 -0
- package/onboarding/local/build-files.mjs +64 -0
- package/onboarding/local/build.mjs +49 -0
- package/onboarding/local/environment.mjs +30 -0
- package/onboarding/local/files.mjs +35 -0
- package/onboarding/local/operation.mjs +166 -0
- package/onboarding/local/owner.mjs +34 -0
- package/onboarding/local/process-table.mjs +114 -0
- package/onboarding/local/recovery.mjs +79 -0
- package/onboarding/local/state.mjs +31 -0
- package/onboarding/metadata.mjs +7 -0
- package/onboarding/operator-install.mjs +45 -0
- package/onboarding/operator-installer.mjs +39 -0
- package/onboarding/operator-prerequisites.mjs +27 -0
- package/onboarding/package.json +6 -0
- package/onboarding/prefill.mjs +16 -0
- package/onboarding/private-files.mjs +97 -0
- package/onboarding/private.mjs +35 -0
- package/onboarding/process-cleanup.mjs +134 -0
- package/onboarding/process-diagnostics.mjs +91 -0
- package/onboarding/process-scope.mjs +118 -0
- package/onboarding/process-table.mjs +118 -0
- package/onboarding/process-tree.mjs +36 -0
- package/onboarding/production-modules.mjs +22 -0
- package/onboarding/production-options.mjs +40 -0
- package/onboarding/production-plan.mjs +8 -0
- package/onboarding/production-runtime.mjs +30 -0
- package/onboarding/project-vite.mjs +14 -0
- package/onboarding/providers.mjs +289 -0
- package/onboarding/queue.mjs +53 -0
- package/onboarding/queued-entry.mjs +34 -0
- package/onboarding/readiness.mjs +59 -0
- package/onboarding/redaction-jwt.mjs +32 -0
- package/onboarding/redaction.mjs +298 -0
- package/onboarding/schema/SETUP.schema.json +514 -0
- package/onboarding/schema/dependencies.mjs +35 -0
- package/onboarding/schema/json-schema.mjs +202 -0
- package/onboarding/schema/setup-validation.mjs +325 -0
- package/onboarding/secrets.mjs +21 -0
- package/onboarding/server.mjs +188 -0
- package/onboarding/setup-manifest.mjs +116 -0
- package/onboarding/state.mjs +46 -0
- package/onboarding/theme-files.mjs +37 -0
- package/onboarding/theme.mjs +118 -0
- package/onboarding/ui/activity.js +98 -0
- package/onboarding/ui/address-client.mjs +33 -0
- package/onboarding/ui/app.js +377 -0
- package/onboarding/ui/index.html +1 -0
- package/onboarding/ui/model-core.mjs +127 -0
- package/onboarding/ui/model.mjs +5 -0
- package/onboarding/ui/style.css +63 -0
- package/onboarding/ui/theme.css +37 -0
- package/onboarding/ui/workspace-art.js +30 -0
- package/onboarding/wrangler-auth.mjs +43 -0
- package/operations/README.md +246 -0
- package/operations/operations.mjs +168 -0
- package/operations/project-operations.mjs +157 -0
- package/package.json +11 -4
- package/scripts/publish-check/README.md +72 -0
- package/scripts/publish-check/checksum_maps.py +52 -0
- package/scripts/publish-check/cli.py +34 -0
- package/scripts/publish-check/command_arguments.py +51 -0
- package/scripts/publish-check/command_shell_quotes.py +41 -0
- package/scripts/publish-check/command_source_quotes.py +120 -0
- package/scripts/publish-check/content_identities.py +250 -0
- package/scripts/publish-check/copy_identities.py +41 -0
- package/scripts/publish-check/der.py +61 -0
- package/scripts/publish-check/destination.py +117 -0
- package/scripts/publish-check/env_values.py +74 -0
- package/scripts/publish-check/font_licence.py +23 -0
- package/scripts/publish-check/git_history.py +115 -0
- package/scripts/publish-check/github_refs.py +217 -0
- package/scripts/publish-check/gitleaks.py +72 -0
- package/scripts/publish-check/gitleaks.toml +40 -0
- package/scripts/publish-check/history_scan.py +176 -0
- package/scripts/publish-check/hit_location.py +18 -0
- package/scripts/publish-check/hygiene_config.py +42 -0
- package/scripts/publish-check/hygiene_text.py +11 -0
- package/scripts/publish-check/identity-recipe.mjs +54 -0
- package/scripts/publish-check/json_identity.py +79 -0
- package/scripts/publish-check/license-exceptions.json +11 -0
- package/scripts/publish-check/licenses.py +49 -0
- package/scripts/publish-check/policy.py +18 -0
- package/scripts/publish-check/project_identity.py +89 -0
- package/scripts/publish-check/provenance.py +91 -0
- package/scripts/publish-check/provenance_pins.py +33 -0
- package/scripts/publish-check/public_base.py +63 -0
- package/scripts/publish-check/public_hygiene.py +80 -0
- package/scripts/publish-check/publication-config.json +12 -0
- package/scripts/publish-check/publication_config.py +34 -0
- package/scripts/publish-check/report.py +52 -0
- package/scripts/publish-check/repository_commands.py +75 -0
- package/scripts/publish-check/repository_intent.py +52 -0
- package/scripts/publish-check/reviewed.py +41 -0
- package/scripts/publish-check/source_hashes.mjs +6 -0
- package/scripts/publish-check/tree_scan.py +88 -0
- package/scripts/publish-check/trust_bundle.py +87 -0
- package/scripts/publish-check.mjs +58 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { trafficProxy } from './server-proxy.mjs';
|
|
6
|
+
|
|
7
|
+
// All public serving, including setup's bounded runtime, uses this listener.
|
|
8
|
+
// The adapter receives a separate loopback port and keeps the public origin
|
|
9
|
+
// for application URLs, cookies and sign-in links.
|
|
10
|
+
export async function runtimeFrontDoor({ host, port, activity }) {
|
|
11
|
+
const runtimePort = await runtimeLoopbackPort(port);
|
|
12
|
+
const observer = activity ?? { begin: () => () => {}, touch: () => {} };
|
|
13
|
+
const proxy = await trafficProxy({ host, port, target: runtimePort, activity: observer });
|
|
14
|
+
return { ...proxy, runtimePort };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The runtime binds this port strictly, seconds after it is chosen. A port the
|
|
18
|
+
// OS assigned to listen(0) comes from its ephemeral range, which meanwhile hands
|
|
19
|
+
// ports to every other socket (the runtime's own listeners, outbound requests),
|
|
20
|
+
// so the runtime's bind could find it taken. Choose a free port the OS never
|
|
21
|
+
// assigns on its own instead.
|
|
22
|
+
async function runtimeLoopbackPort(publicPort) {
|
|
23
|
+
const [first, last] = await ephemeralRange();
|
|
24
|
+
const candidates = [[10000, first - 1], [last + 1, 65535]].filter(([low, high]) => low <= high);
|
|
25
|
+
if (!candidates.length) throw Error(`The OS assigns every local port from 10000 (ephemeral range ${first}-${last}); leave part of that space outside the range for the local runtime.`);
|
|
26
|
+
const size = candidates.reduce((total, [low, high]) => total + high - low + 1, 0);
|
|
27
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
28
|
+
let offset = Math.floor(Math.random() * size), candidate;
|
|
29
|
+
for (const [low, high] of candidates) { if (offset <= high - low) { candidate = low + offset; break; } offset -= high - low + 1; }
|
|
30
|
+
if (candidate !== publicPort && await free(candidate)) return candidate;
|
|
31
|
+
}
|
|
32
|
+
throw Error('No free loopback port outside the ephemeral range was found for the local runtime.');
|
|
33
|
+
}
|
|
34
|
+
async function ephemeralRange() {
|
|
35
|
+
if (process.platform === 'linux') return (await readFile('/proc/sys/net/ipv4/ip_local_port_range', 'utf8')).trim().split(/\s+/).map(Number);
|
|
36
|
+
if (process.platform === 'darwin') return (await promisify(execFile)('/usr/sbin/sysctl', ['-n', 'net.inet.ip.portrange.first', 'net.inet.ip.portrange.last'])).stdout.trim().split(/\s+/).map(Number);
|
|
37
|
+
// Windows' default dynamic range, the IANA range.
|
|
38
|
+
return [49152, 65535];
|
|
39
|
+
}
|
|
40
|
+
function free(port) {
|
|
41
|
+
const probe = createServer();
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
probe.once('error', () => resolve(false));
|
|
44
|
+
probe.listen(port, '127.0.0.1', () => probe.close(() => resolve(true)));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { declaredCronSources } from './deploy-config.mjs';
|
|
2
|
+
|
|
3
|
+
// `rer dev` runs the project's declared Cron Triggers (wrangler `triggers.crons`, from wrangler.jsonc, wrangler.toml or a deploy template)
|
|
4
|
+
// on their schedule by calling the local runtime's scheduled test endpoint (the Vite plugin's under dev, Miniflare's own under preview), so
|
|
5
|
+
// alerts, retries and checks behave on the owner's computer as they do deployed.
|
|
6
|
+
|
|
7
|
+
const fields = [[0, 59], [0, 23], [1, 31], [1, 12], [1, 7]];
|
|
8
|
+
|
|
9
|
+
function field(text, [low, high]) {
|
|
10
|
+
const values = new Set();
|
|
11
|
+
for (const part of text.split(',')) {
|
|
12
|
+
const match = /^(?:(\*)|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(part);
|
|
13
|
+
if (!match) throw new Error(`Unsupported cron field "${text}"`);
|
|
14
|
+
const step = match[4] === undefined ? 1 : Number(match[4]);
|
|
15
|
+
const start = match[1] ? low : Number(match[2]), end = match[1] ? high : match[3] === undefined ? (match[4] === undefined ? start : high) : Number(match[3]);
|
|
16
|
+
if (!step || start < low || end > high || start > end) throw new Error(`Cron field "${text}" is out of range`);
|
|
17
|
+
for (let value = start; value <= end; value += step) values.add(value);
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Same grammar the deploy kit accepts: five numeric fields with lists, ranges and
|
|
23
|
+
// steps, in UTC. Day of week counts 1 (Sunday) to 7 (Saturday), as Cloudflare does.
|
|
24
|
+
export function parseCron(expression) {
|
|
25
|
+
const parts = expression.trim().split(/\s+/);
|
|
26
|
+
if (parts.length !== 5) throw new Error(`Cron "${expression}" needs five fields`);
|
|
27
|
+
const [minute, hour, day, month, weekday] = parts.map((part, index) => field(part, fields[index]));
|
|
28
|
+
return { minute, hour, day, month, weekday, dayRestricted: parts[2] !== '*', weekdayRestricted: parts[4] !== '*' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function cronMatches(cron, date) {
|
|
32
|
+
const parsed = typeof cron === 'string' ? parseCron(cron) : cron;
|
|
33
|
+
const day = parsed.day.has(date.getUTCDate()), weekday = parsed.weekday.has(date.getUTCDay() + 1);
|
|
34
|
+
const days = parsed.dayRestricted && parsed.weekdayRestricted ? day || weekday : day && weekday;
|
|
35
|
+
return parsed.minute.has(date.getUTCMinutes()) && parsed.hour.has(date.getUTCHours()) && parsed.month.has(date.getUTCMonth() + 1) && days;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The Cron Triggers a project declares, read from the same files `rer check` reads
|
|
39
|
+
// for the deployed Worker (deploy-config.mjs). Two files that both declare
|
|
40
|
+
// triggers.crons must declare the same ones: otherwise `rer dev` would run a
|
|
41
|
+
// schedule the deployed Worker does not have.
|
|
42
|
+
export async function declaredCrons(root, adapter) {
|
|
43
|
+
const sources = await declaredCronSources(root, adapter);
|
|
44
|
+
for (const { path, crons } of sources) for (const cron of crons) parseCron(cron);
|
|
45
|
+
const [first, ...others] = sources;
|
|
46
|
+
const clash = others.find(({ crons }) => crons.length !== first.crons.length || crons.some(cron => !first.crons.includes(cron)));
|
|
47
|
+
if (clash) throw new Error(`${first.path} declares ${JSON.stringify(first.crons)} but ${clash.path} declares ${JSON.stringify(clash.crons)}; make them the same.`);
|
|
48
|
+
return first?.crons ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fire each matching cron at the start of its minute until `signal` aborts.
|
|
52
|
+
// `report` receives one record per run: { cron, ok, status?, ms, error? }.
|
|
53
|
+
export function runScheduled({ crons, url, path = '/cdn-cgi/handler/scheduled', signal, report, now = () => new Date(), fetchImpl = fetch, runTimeout = 300000 }) {
|
|
54
|
+
const parsed = crons.map(cron => [cron, parseCron(cron)]), running = new Set();
|
|
55
|
+
let timer;
|
|
56
|
+
const settled = new Promise(resolve => {
|
|
57
|
+
const finish = () => { clearTimeout(timer); resolve(); };
|
|
58
|
+
if (signal.aborted) return finish();
|
|
59
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
60
|
+
const wait = () => { timer = setTimeout(tick, 60000 - (now().getTime() % 60000) + 250); };
|
|
61
|
+
const tick = () => {
|
|
62
|
+
const minute = now();
|
|
63
|
+
for (const [cron, expression] of parsed) if (cronMatches(expression, minute)) void run(cron);
|
|
64
|
+
wait();
|
|
65
|
+
};
|
|
66
|
+
async function run(cron) {
|
|
67
|
+
if (running.has(cron)) { report({ cron, ok: false, skipped: true, error: 'previous run still in progress' }); return; }
|
|
68
|
+
running.add(cron);
|
|
69
|
+
const started = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetchImpl(`${url}${path}?cron=${encodeURIComponent(cron)}`, { signal: AbortSignal.any([signal, AbortSignal.timeout(runTimeout)]) });
|
|
72
|
+
await response.body?.cancel();
|
|
73
|
+
report({ cron, ok: response.ok, status: response.status, ms: Date.now() - started });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!signal.aborted) report({ cron, ok: false, ms: Date.now() - started, error: error.name === 'TimeoutError' ? 'timed out' : 'the runtime did not answer' });
|
|
76
|
+
} finally { running.delete(cron); }
|
|
77
|
+
}
|
|
78
|
+
wait();
|
|
79
|
+
});
|
|
80
|
+
return settled;
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Inject the monotonic clock and scheduling together: tests advance logical time,
|
|
2
|
+
// never assert the operating system schedules a callback in a wall-clock window.
|
|
3
|
+
export function activityClock({seconds,onIdle,now=()=>Number(process.hrtime.bigint()/1000000n),schedule=setTimeout,cancel=clearTimeout,onChange=()=>{}}={}) {
|
|
4
|
+
let ready=false,active=0,last=now(),timer,closed=false;
|
|
5
|
+
function arm(){cancel(timer);if(ready&&!closed&&!active)timer=schedule(()=>{if(now()-last>=seconds*1000)onIdle();else arm();},Math.min(2**31-1,Math.max(1,seconds*1000-(now()-last))));}
|
|
6
|
+
function touch(){last=now();onChange({lastActivity:last,active});arm();}
|
|
7
|
+
return {ready(){ready=true;touch();},touch,
|
|
8
|
+
begin(){active++;touch();let ended=false;return()=>{if(ended)return;ended=true;active--;touch();};},
|
|
9
|
+
snapshot(){return {ready,active,lastActivity:last,idleSeconds:(now()-last)/1000};},
|
|
10
|
+
close(){closed=true;cancel(timer);}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { posix } from 'node:path';
|
|
5
|
+
import { refuse } from './errors.mjs';
|
|
6
|
+
import { fontResponseHeaders } from './font-cache.mjs';
|
|
7
|
+
|
|
8
|
+
// The public listener observes transport lifetime without buffering or replacing
|
|
9
|
+
// application responses. Preserve Host, Origin, cookies, status and streaming.
|
|
10
|
+
// Each request opens its own loopback connection: a pooled keep-alive socket
|
|
11
|
+
// that the runtime has already dropped would otherwise fail a fresh request.
|
|
12
|
+
export async function trafficProxy({host,port,target,activity}) {
|
|
13
|
+
const probe=randomUUID(),sockets=new Set();
|
|
14
|
+
const server=http.createServer((req,res)=>{
|
|
15
|
+
if(!publicPath(req.url)){req.resume();notFound(res);return;}
|
|
16
|
+
const internal=req.headers['x-rer-probe']===probe;delete req.headers['x-rer-probe'];
|
|
17
|
+
const finish=internal?()=>{}:activity.begin();
|
|
18
|
+
let requestEnded=req.complete,responseEnded=false;
|
|
19
|
+
const completed=()=>{if(requestEnded&&responseEnded)finish();};
|
|
20
|
+
req.once('end',()=>{requestEnded=true;completed();});req.once('aborted',finish);
|
|
21
|
+
res.once('close',()=>{responseEnded=true;if(req.destroyed)finish();else completed();});res.once('finish',()=>{responseEnded=true;completed();});
|
|
22
|
+
const upstream=http.request({host:'127.0.0.1',port:target,path:req.url,method:req.method,headers:req.headers,agent:false},reply=>{
|
|
23
|
+
res.writeHead(reply.statusCode,fontResponseHeaders(req.url,reply.statusCode,reply.rawHeaders,req.headers));reply.pipe(res);
|
|
24
|
+
});
|
|
25
|
+
req.once('aborted',()=>upstream.destroy());res.once('close',()=>upstream.destroy());
|
|
26
|
+
upstream.on('error',()=>{if(!res.headersSent)notFound(res);else res.destroy();});req.pipe(upstream);
|
|
27
|
+
});
|
|
28
|
+
server.on('connection',socket=>{sockets.add(socket);socket.on('close',()=>sockets.delete(socket));});
|
|
29
|
+
server.on('upgrade',(req,socket,head)=>{
|
|
30
|
+
if(!publicPath(req.url)){socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');return;}
|
|
31
|
+
const hmr=String(req.headers['sec-websocket-protocol']??'').split(',').map(s=>s.trim()).includes('vite-hmr');
|
|
32
|
+
const upstream=net.connect(target,'127.0.0.1',()=>{
|
|
33
|
+
upstream.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n${req.rawHeaders.reduce((lines,value,i)=>i%2?lines:lines+value+': '+req.rawHeaders[i+1]+'\r\n','')}\r\n`);
|
|
34
|
+
if(head.length)upstream.write(head);socket.pipe(upstream);upstream.pipe(socket);
|
|
35
|
+
});
|
|
36
|
+
if(!hmr){
|
|
37
|
+
const observeSafely=observer=>chunk=>{try{observer(chunk);}catch{socket.destroy();upstream.destroy();}};
|
|
38
|
+
const finish=activity.begin(),observe=observeSafely(frameObserver(()=>activity.touch())),replyFrames=observeSafely(frameObserver(()=>activity.touch()));
|
|
39
|
+
let handshake=Buffer.alloc(0),upgraded=false;
|
|
40
|
+
if(head.length)observe(head);socket.on('data',observe);socket.once('close',finish);
|
|
41
|
+
upstream.on('data',chunk=>{
|
|
42
|
+
if(upgraded){replyFrames(chunk);return;}
|
|
43
|
+
handshake=Buffer.concat([handshake,chunk]);const end=handshake.indexOf('\r\n\r\n');
|
|
44
|
+
if(end<0){if(handshake.length>65536)upstream.destroy();return;}
|
|
45
|
+
upgraded=/^HTTP\/1\.[01] 101 /.test(handshake.toString('latin1',0,end));
|
|
46
|
+
if(upgraded)replyFrames(handshake.subarray(end+4));else finish();handshake=Buffer.alloc(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
upstream.on('error',()=>socket.destroy());socket.on('error',()=>upstream.destroy());socket.on('close',()=>upstream.destroy());upstream.on('close',()=>socket.destroy());
|
|
50
|
+
});
|
|
51
|
+
try{await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(port,host,resolve);});}
|
|
52
|
+
catch(error){
|
|
53
|
+
if(['EADDRINUSE','EACCES'].includes(error.code))throw refuse(`Requested port ${port} is unavailable. Stop its owner or choose an explicit free port; rer does not drift.`);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
return {probe,port:server.address().port,async close(){for(const socket of sockets)socket.destroy();await new Promise(resolve=>server.close(resolve));}};
|
|
57
|
+
}
|
|
58
|
+
function notFound(res){res.writeHead(404,{'content-type':'text/plain; charset=utf-8'});res.end('Not found');}
|
|
59
|
+
// The runtime's built-in routes (/cdn-cgi/*: Local Explorer, trigger handlers,
|
|
60
|
+
// R2 and platform proxies), the checkout's private files and export archives
|
|
61
|
+
// (*.rer and their in-progress .partial files) never answer on the public
|
|
62
|
+
// listener; rer's scheduler calls the runtime's internal port. Decode as often as
|
|
63
|
+
// a server might and test every segment, so encoded, dotted or backslashed
|
|
64
|
+
// spellings of either are refused too (APFS ignores case). The documented
|
|
65
|
+
// exports/ folder is refused where Vite serves the checkout (the root and its
|
|
66
|
+
// /@fs, /@id prefixes), so an app's own /api/.../exports routes still answer.
|
|
67
|
+
const privateSegment=/^(?:cdn-cgi|\.rer|\.seed|\.wrangler|\.git|\.dev\.vars.*|\.env.*|.*\.rer(?:\.[\w-]+\.partial)?)$/i;
|
|
68
|
+
export function publicPath(url){
|
|
69
|
+
let path=String(url).split(/[?#]/)[0];
|
|
70
|
+
for(let round=0;round<4;round++){let decoded;try{decoded=decodeURIComponent(path);}catch{return false;}if(decoded===path)break;path=decoded;}
|
|
71
|
+
const segments=path.replace(/\\/g,'/').split('/').map(segment=>segment.trim());
|
|
72
|
+
if(segments.some(segment=>privateSegment.test(segment)))return false;
|
|
73
|
+
const served=posix.normalize('/'+segments.join('/')).split('/').filter(Boolean),exports=segment=>/^exports$/i.test(segment);
|
|
74
|
+
return !exports(served[0])&&!(served[0]?.startsWith('@')&&served.some(exports));
|
|
75
|
+
}
|
|
76
|
+
// Count application data frames, not protocol ping/pong or an open control
|
|
77
|
+
// socket. Parse lengths incrementally without retaining application payloads.
|
|
78
|
+
export function frameObserver(touch) {
|
|
79
|
+
let buffer=Buffer.alloc(0),remaining=0;
|
|
80
|
+
return chunk=>{
|
|
81
|
+
buffer=Buffer.concat([buffer,chunk]);
|
|
82
|
+
for(;;){
|
|
83
|
+
if(remaining){const count=Math.min(remaining,buffer.length);remaining-=count;buffer=buffer.subarray(count);if(remaining)return;}
|
|
84
|
+
if(buffer.length<2)return;
|
|
85
|
+
const opcode=buffer[0]&15,masked=Boolean(buffer[1]&128);let length=buffer[1]&127,offset=2;
|
|
86
|
+
if(length===126){if(buffer.length<4)return;length=buffer.readUInt16BE(2);offset=4;}
|
|
87
|
+
else if(length===127){if(buffer.length<10)return;const big=buffer.readBigUInt64BE(2);if(big>BigInt(Number.MAX_SAFE_INTEGER))throw Error('WebSocket frame too large');length=Number(big);offset=10;}
|
|
88
|
+
if(masked)offset+=4;if(buffer.length<offset)return;
|
|
89
|
+
if(opcode<=2)touch();buffer=buffer.subarray(offset);remaining=length;
|
|
90
|
+
if(!remaining&&!buffer.length)return;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createConnection, createServer } from 'node:net';
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
3
|
+
import { userInfo, tmpdir } from 'node:os';
|
|
4
|
+
import { chmod, realpath, unlink, lstat, mkdir } from 'node:fs/promises';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { processInfo } from '../modules/job-queue/processes.mjs';
|
|
8
|
+
import { identity, alive } from '../modules/job-queue/identity.mjs';
|
|
9
|
+
import { transaction } from '../modules/job-queue/storage.mjs';
|
|
10
|
+
import { refuse, invalid } from './errors.mjs';
|
|
11
|
+
import { serverDefaults } from './contract.mjs';
|
|
12
|
+
import { availableMemory } from '../modules/job-queue/resources.mjs';
|
|
13
|
+
|
|
14
|
+
export function positive(value, name, fallback) {
|
|
15
|
+
if(value===undefined)return fallback;
|
|
16
|
+
if(!/^[1-9][0-9]*$/.test(String(value)) || !Number.isSafeInteger(Number(value)))throw invalid(`${name} must be positive integer seconds/count.`);
|
|
17
|
+
return Number(value);
|
|
18
|
+
}
|
|
19
|
+
export const stopInstruction = record => `rer ${record.mode} --cwd ${JSON.stringify(record.checkout)} --stop`;
|
|
20
|
+
// Plain-words listing of one running server for the limit refusal.
|
|
21
|
+
const listing = record => ` ${record.mode} on port ${record.port ?? '(starting)'} in ${record.checkout}\n stop it with: ${stopInstruction(record)}`;
|
|
22
|
+
export const description = record => `${record.mode} ${record.url ?? '(starting)'} for ${record.checkout}; stop it with: ${stopInstruction(record)}`;
|
|
23
|
+
// A test or CI journey declares itself, the way RER_SERVERS_LOAD_THRESHOLD
|
|
24
|
+
// declares load. It serves on its own port and never counts toward the cap.
|
|
25
|
+
export const declaresJourney = env => {
|
|
26
|
+
if(env.RER_SERVER_JOURNEY===undefined)return false;
|
|
27
|
+
if(env.RER_SERVER_JOURNEY==='1')return true;
|
|
28
|
+
throw invalid('RER_SERVER_JOURNEY must be 1 to declare a test or CI journey.');
|
|
29
|
+
};
|
|
30
|
+
async function request(record, action) {
|
|
31
|
+
return new Promise((resolveRequest,reject)=>{
|
|
32
|
+
const socket=createConnection(record.control);let text='';
|
|
33
|
+
socket.setTimeout(35000,()=>socket.destroy(Error('Managed server did not complete cleanup.')));
|
|
34
|
+
socket.on('connect',()=>socket.end(JSON.stringify({token:record.token,action})));
|
|
35
|
+
socket.on('data',chunk=>{text+=chunk;if(text.length>16384)socket.destroy(Error('Invalid managed server reply.'));});
|
|
36
|
+
socket.on('end',()=>{try{const value=JSON.parse(text);if(value.token!==record.token)throw Error('Unverified server');resolveRequest(value);}catch(error){reject(error);}});
|
|
37
|
+
socket.on('error',reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A new server starts only while this much memory is available. An idle dev
|
|
42
|
+
// server holds 0.5-1.3 GiB (preview 0.4-0.8 GiB), so 1.5 GiB leaves room for the
|
|
43
|
+
// largest one measured to start and settle. Below it the OS is already compressing
|
|
44
|
+
// and swapping, and one more server makes every running one slower.
|
|
45
|
+
export const defaultMemoryFloor=1.5*1024**3;
|
|
46
|
+
export function memoryFloor(env=process.env){
|
|
47
|
+
const value=env.RER_SERVERS_MIN_MEMORY_BYTES;
|
|
48
|
+
if(value===undefined)return defaultMemoryFloor;
|
|
49
|
+
if(!/^(0|[1-9][0-9]*)$/.test(value)||!Number.isSafeInteger(Number(value)))throw invalid('RER_SERVERS_MIN_MEMORY_BYTES must be a number of bytes, 0 for no floor.');
|
|
50
|
+
return Number(value);
|
|
51
|
+
}
|
|
52
|
+
function memoryMessage(counted,available,floor,mode) {
|
|
53
|
+
const gib=bytes=>(bytes/1024**3).toFixed(1);
|
|
54
|
+
const head=`Not started: this computer is short of memory. ${gib(available)} GiB is available and a new server starts only at ${gib(floor)} GiB or more.`;
|
|
55
|
+
const list=counted.length?`Running now:\n${counted.map(listing).join('\n')}\nA dev server holds about 0.5-1.3 GiB and a preview 0.4-0.8 GiB; stopping one with its command above frees that.`:'No dev or preview server is running, so something else is using the memory.';
|
|
56
|
+
return `${head}\n${list}\nTo start anyway, lower the floor, for example RER_SERVERS_MIN_MEMORY_BYTES=${Math.floor(floor/2)} rer ${mode} (0 turns it off).`;
|
|
57
|
+
}
|
|
58
|
+
function limitMessage(counted,cap,limit,mode) {
|
|
59
|
+
const holders=cap<limit?counted.filter(item=>item.explicit&&item.limit===cap):[];
|
|
60
|
+
const head=`Not started: ${counted.length} dev or preview ${counted.length===1?'server is':'servers are'} running on this computer and the limit is ${cap}.`;
|
|
61
|
+
const list=`Running now:\n${counted.map(listing).join('\n')}`;
|
|
62
|
+
const raise=holders.length
|
|
63
|
+
?`The limit is ${cap} because ${holders.length===1?'a server was':'servers were'} started with RER_SERVER_LIMIT=${cap} (${holders.map(item=>item.checkout).join(', ')}). Stop ${holders.length===1?'it':'them'} and start again, or start everything with a higher RER_SERVER_LIMIT=N.`
|
|
64
|
+
:`To allow more, start this one with a higher limit, for example RER_SERVER_LIMIT=${cap*2} rer ${mode}. Servers started without RER_SERVER_LIMIT allow ${defaultServerLimit}.`;
|
|
65
|
+
return `${head}\n${list}\nTo make room, stop one with its command above. ${raise}`;
|
|
66
|
+
}
|
|
67
|
+
// A server is only ever stopped by the checkout that owns it, through its own
|
|
68
|
+
// --stop, idle timeout or signal. Capacity never stops another checkout's
|
|
69
|
+
// server: a request over the cap is refused with the running servers named.
|
|
70
|
+
// Lock order: short machine transaction -> reserve checkout; release transaction
|
|
71
|
+
// before queue admission/local operation. Stop waits for process+state cleanup,
|
|
72
|
+
// which never needs the machine transaction. Generation removal happens last.
|
|
73
|
+
// The limit is 10 unless the person sets RER_SERVER_LIMIT. Every record keeps the
|
|
74
|
+
// limit it started with and whether the person set it. Across the running
|
|
75
|
+
// servers the cap is the lowest limit a person set explicitly, so a deliberate
|
|
76
|
+
// "RER_SERVER_LIMIT=3" keeps holding every start. A recorded default (including
|
|
77
|
+
// the old default of 2 in records written before this rule) never lowers the cap
|
|
78
|
+
// below the current default.
|
|
79
|
+
export const defaultServerLimit=serverDefaults.serverLimit;
|
|
80
|
+
export const serversDirectory=(env=process.env)=>resolve(env.RER_SERVERS_DIR??join(userInfo().homedir,'.rer/servers'));
|
|
81
|
+
export async function serverRegistry(root,{env=process.env,memory=availableMemory}={}) {
|
|
82
|
+
const floor=memoryFloor(env),explicit=env.RER_SERVER_LIMIT!==undefined,limit=positive(env.RER_SERVER_LIMIT,'RER_SERVER_LIMIT',defaultServerLimit),journey=declaresJourney(env);
|
|
83
|
+
const checkout=await realpath(root), lifetime=await identity();
|
|
84
|
+
const directory=serversDirectory(env);
|
|
85
|
+
let secured;
|
|
86
|
+
async function secure(){
|
|
87
|
+
for(const path of [directory,join(directory,'locks')]){
|
|
88
|
+
await mkdir(path,{recursive:true,mode:0o700});const info=await lstat(path);
|
|
89
|
+
if(info.isSymbolicLink()||!info.isDirectory()||info.uid!==process.getuid())throw refuse('Server registry directory is foreign or linked. Preserve it and choose an owned RER_SERVERS_DIR.');
|
|
90
|
+
await chmod(path,0o700);
|
|
91
|
+
}
|
|
92
|
+
try{const info=await lstat(join(directory,'state.json'));if(!info.isFile()||info.isSymbolicLink()||info.nlink!==1||info.uid!==process.getuid())throw refuse('Server registry state is foreign or linked. Preserve it for inspection.');await chmod(join(directory,'state.json'),0o600);}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
93
|
+
}
|
|
94
|
+
const change=async action=>{await (secured??=secure());return transaction(directory,lifetime.info,action);};
|
|
95
|
+
let owned, control, closing;
|
|
96
|
+
async function inspect(state) {
|
|
97
|
+
const kept=[];
|
|
98
|
+
for(const record of state.running){
|
|
99
|
+
let live;try{live=await alive(record.owner);}catch{throw refuse(`Unverifiable server identity: ${description(record)}. Preserve it for inspection.`);}
|
|
100
|
+
if(live) kept.push(record);
|
|
101
|
+
else {
|
|
102
|
+
const current=await processInfo(record.owner.pid);
|
|
103
|
+
if(current&&!current.zombie&¤t.start===record.owner.start)throw refuse(`Server lifetime cannot be verified: ${description(record)}. Preserve its process.`);
|
|
104
|
+
// A runtime capability grants recovery only to the captured process scope.
|
|
105
|
+
if(record.runtime) {
|
|
106
|
+
const { shared }=await import('./shared.mjs');
|
|
107
|
+
const { recoverCommand }=await shared('command-recovery.mjs');
|
|
108
|
+
await recoverCommand(record.runtime);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
state.running=kept;
|
|
113
|
+
}
|
|
114
|
+
async function existing(mode,options,{stop=false}={}) {
|
|
115
|
+
return change(async state=>{
|
|
116
|
+
await inspect(state);const record=state.running.find(item=>item.checkout===checkout);
|
|
117
|
+
if(!record)return null;
|
|
118
|
+
if(record.mode!==mode)throw refuse(`This checkout already owns ${description(record)}.`);
|
|
119
|
+
if(!stop && (options.supplied.has('--host')&&record.host!==undefined&&options.host!==record.host || options.supplied.has('--port')&&record.port!==undefined&&options.port!==record.port || options.supplied.has('--idle-timeout')&&options.idleTimeout!==record.idleTimeout))throw refuse(`Options conflict with ${description(record)}.`);
|
|
120
|
+
return record;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function stop(mode,options) {
|
|
124
|
+
try{await lstat(join(directory,'state.json'));}catch(error){if(error.code==='ENOENT')return;throw error;}
|
|
125
|
+
const record=await existing(mode,options,{stop:true});if(!record)return;
|
|
126
|
+
await request(record,'stop');
|
|
127
|
+
await change(state=>{state.running=state.running.filter(item=>item.generation!==record.generation);});
|
|
128
|
+
}
|
|
129
|
+
async function reserve(mode,options) {
|
|
130
|
+
return change(async state=>{
|
|
131
|
+
await inspect(state);
|
|
132
|
+
const same=state.running.find(item=>item.checkout===checkout);
|
|
133
|
+
if(same)return {existing:same};
|
|
134
|
+
// Journeys are listed but uncounted: they neither use nor lower the cap.
|
|
135
|
+
const counted=state.running.filter(item=>!item.journey),cap=Math.min(limit,...counted.filter(item=>item.explicit).map(item=>item.limit));
|
|
136
|
+
if(!journey&&counted.length>=cap)throw refuse(limitMessage(counted,cap,limit,mode));
|
|
137
|
+
// Unreadable memory is not a reason to refuse; only a reading below the floor is.
|
|
138
|
+
const available=journey||floor===0?NaN:await memory();
|
|
139
|
+
if(available<floor)throw refuse(memoryMessage(counted,available,floor,mode));
|
|
140
|
+
owned={checkout,mode,journey,generation:randomUUID(),token:randomUUID(),owner:lifetime.info,limit,explicit,startedAt:Number(process.hrtime.bigint()/1000000n),idleTimeout:options.idleTimeout};
|
|
141
|
+
owned.lastActivity=owned.startedAt;owned.active=0;
|
|
142
|
+
const name=`rer-server-${createHash('sha256').update(owned.generation).digest('hex').slice(0,20)}.sock`;
|
|
143
|
+
owned.control=join(tmpdir(),name);if(Buffer.byteLength(owned.control)>=100)owned.control=join('/tmp',name);
|
|
144
|
+
owned.runtime={pid:process.pid,token:owned.token};
|
|
145
|
+
state.running.push(owned);return {owned};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function serve(handler) {
|
|
149
|
+
control=createServer({allowHalfOpen:true},socket=>{
|
|
150
|
+
let text='';socket.setTimeout(35000,()=>socket.destroy());
|
|
151
|
+
socket.on('data',data=>{text+=data;if(text.length>1024)socket.destroy();});
|
|
152
|
+
socket.on('end',async()=>{
|
|
153
|
+
try{const request=JSON.parse(text);if(request.token!==owned.token)throw Error('Invalid capability');socket.end(JSON.stringify({...await handler(request.action),token:owned.token}));}
|
|
154
|
+
catch{socket.destroy();}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
await new Promise((resolve,reject)=>{control.once('error',reject);control.listen(owned.control,resolve);});await chmod(owned.control,0o600);
|
|
158
|
+
}
|
|
159
|
+
async function update(fields){Object.assign(owned,fields);await change(state=>{const item=state.running.find(item=>item.generation===owned.generation);if(!item)throw Error('Server reservation disappeared');Object.assign(item,fields);});}
|
|
160
|
+
async function updateActivity(fields){await change(state=>{const item=state.running.find(item=>item.generation===owned.generation);if(item)Object.assign(item,fields);});}
|
|
161
|
+
async function waitReady(record,signal){
|
|
162
|
+
for(let attempt=0;attempt<1200;attempt++){
|
|
163
|
+
signal?.throwIfAborted();if(!await alive(record.owner))throw refuse('Concurrent server startup failed. Retry after recovery.');
|
|
164
|
+
try{const status=await request(record,'status');if(status.ready)return {...record,...status};}catch(error){if(attempt===1199)throw error;}
|
|
165
|
+
await delay(250,undefined,{signal});
|
|
166
|
+
}
|
|
167
|
+
throw refuse('The existing server is still starting. Wait for its ready output.');
|
|
168
|
+
}
|
|
169
|
+
return {checkout,existing,stop,reserve,serve,update,updateActivity,waitReady,custodyToken:()=>owned?.token,
|
|
170
|
+
// The sockets close even when the record cannot be removed (its registry
|
|
171
|
+
// deleted under it, say): an open listener would keep this process alive.
|
|
172
|
+
close(){return closing??=(async()=>{
|
|
173
|
+
try{if(owned)await change(state=>{state.running=state.running.filter(item=>item.generation!==owned.generation);});}
|
|
174
|
+
finally{
|
|
175
|
+
try{
|
|
176
|
+
if(control)await new Promise(resolve=>control.close(resolve));
|
|
177
|
+
if(owned)await unlink(owned.control).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
178
|
+
}finally{await lifetime.close();}
|
|
179
|
+
}
|
|
180
|
+
})();}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {loadavg,tmpdir} from 'node:os';
|
|
2
|
+
import {statfs} from 'node:fs/promises';
|
|
3
|
+
import {refuse} from './errors.mjs';
|
|
4
|
+
// Persistent serving is not a bounded queue job. Check its launch separately,
|
|
5
|
+
// after finite preparation, while its canonical server reservation is held.
|
|
6
|
+
// The 1-minute-load guard is a fleet's rule, set in the fleet's environment:
|
|
7
|
+
// without RER_SERVERS_LOAD_THRESHOLD a person's start is never refused for load.
|
|
8
|
+
export function servingLoadThreshold(env=process.env){
|
|
9
|
+
if(env.RER_SERVERS_LOAD_THRESHOLD===undefined)return Infinity;
|
|
10
|
+
const threshold=Number(env.RER_SERVERS_LOAD_THRESHOLD);
|
|
11
|
+
if(!Number.isFinite(threshold)||threshold<0)throw refuse('Invalid server load threshold.');
|
|
12
|
+
return threshold;
|
|
13
|
+
}
|
|
14
|
+
export async function serverResources(root,{load=()=>loadavg()[0],disk=statfs,env=process.env}={}){
|
|
15
|
+
const threshold=servingLoadThreshold(env);
|
|
16
|
+
const measured=threshold===Infinity?0:await load();
|
|
17
|
+
if(!Number.isFinite(measured)||measured>threshold)throw refuse(`Not started: this computer is busy. Its 1-minute load is ${Number.isFinite(measured)?measured.toFixed(1):'unknown'} and a server starts only at ${threshold} or below.\nTry again in a few minutes, or start it anyway with a higher threshold, for example RER_SERVERS_LOAD_THRESHOLD=${Math.ceil(Math.max(threshold,Number.isFinite(measured)?measured:0)*2)} rer dev.`);
|
|
18
|
+
for(const path of new Set([root,tmpdir()])){
|
|
19
|
+
const state=await disk(path,{bigint:true});
|
|
20
|
+
if(state.bavail*state.bsize<15n*1024n**3n)throw refuse('Server startup requires 15 GiB free on checkout and temporary volumes. Recover owned space before retrying.');
|
|
21
|
+
}
|
|
22
|
+
}
|
package/cli/settings.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Reuse the seed's existing declarations; no parallel auth setting aliases.
|
|
2
|
+
export function localSettingKeys(project) {
|
|
3
|
+
const manifest = project.setupManifest ?? project.setupSeed?.manifest;
|
|
4
|
+
const claim = manifest?.steps.find(step => step.type === 'owner-claim');
|
|
5
|
+
return {
|
|
6
|
+
origin: project.adapter?.hooks?.secrets?.originKey ?? 'AUTH_ORIGIN',
|
|
7
|
+
email: claim?.emailField ?? project.adapter?.input?.names?.email ?? 'OWNER_EMAIL',
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function requiredLocalSettings(project, values) {
|
|
12
|
+
const keys=localSettingKeys(project);
|
|
13
|
+
const manifest=project.setupManifest ?? project.setupSeed?.manifest;
|
|
14
|
+
return [...new Set([keys.email,keys.origin,...(manifest?.fields??[]).filter(field=>field.kind==='secret'&&field.required&&!field.when&&field.input!=='entered'&&field.name!==manifest?.steps.find(step=>step.type==='owner-claim')?.tokenField).map(field=>field.name)])].filter(name=>!values[name]);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { contained } from './project.mjs';
|
|
3
|
+
import { shared } from './shared.mjs';
|
|
4
|
+
import { invalid } from './errors.mjs';
|
|
5
|
+
|
|
6
|
+
const path = '.seed/installer/setup-answers.json';
|
|
7
|
+
|
|
8
|
+
// Installer choices and local runtime bindings need not have the same meaning.
|
|
9
|
+
// Keep only declared nonsecret answers, after successful setup, in owned storage.
|
|
10
|
+
export async function savedSetupAnswers(project, model) {
|
|
11
|
+
const file = await contained(project.root, path, { optional: true, privateFile: true });
|
|
12
|
+
let saved;
|
|
13
|
+
try { saved = JSON.parse(await (await shared('private-files.mjs')).readPrivateFile(file)); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') return undefined;
|
|
16
|
+
throw invalid('Saved setup answers are unreadable or invalid. Preserve private settings and repair the saved setup answers before retrying.');
|
|
17
|
+
}
|
|
18
|
+
if (!saved || typeof saved !== 'object' || Array.isArray(saved) || Object.values(saved).some(value => typeof value !== 'string'))
|
|
19
|
+
throw invalid('Saved setup answers must be a JSON object of installer answers.');
|
|
20
|
+
return Object.fromEntries(model.safeKeys.filter(key => Object.hasOwn(saved, key)).map(key => [key, saved[key]]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function rememberSetupAnswers(project, input) {
|
|
24
|
+
const answers = Object.fromEntries(project.setupSeed.model.safeKeys.filter(key => typeof input[key] === 'string').map(key => [key, input[key]]));
|
|
25
|
+
const { privateDirectory, writePrivateFile } = await shared('private-files.mjs');
|
|
26
|
+
await privateDirectory(project.root, ['.seed', 'installer']);
|
|
27
|
+
await contained(project.root, path, { optional: true, privateFile: true });
|
|
28
|
+
await writePrivateFile(join(project.root, path), JSON.stringify(answers) + '\n');
|
|
29
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {randomBytes} from 'node:crypto';
|
|
2
|
+
import {readFile} from 'node:fs/promises';
|
|
3
|
+
import {createInterface} from 'node:readline/promises';
|
|
4
|
+
import {Writable} from 'node:stream';
|
|
5
|
+
import {contained} from './project.mjs';
|
|
6
|
+
import {shared} from './shared.mjs';
|
|
7
|
+
import {localSettingKeys} from './settings.mjs';
|
|
8
|
+
import {mailbox} from './parse.mjs';
|
|
9
|
+
import {invalid,missing,refuse} from './errors.mjs';
|
|
10
|
+
import {savedSetupAnswers} from './setup-answers.mjs';
|
|
11
|
+
|
|
12
|
+
// RER_SETUP_LOCAL_DEFAULTS=1 is set only by the zero-setup `rer dev` launch for
|
|
13
|
+
// its own setup run: it fills each empty active field that declares a
|
|
14
|
+
// localDefault. Explicit setup and deploy never read it.
|
|
15
|
+
export const localDefaultsVariable = 'RER_SETUP_LOCAL_DEFAULTS';
|
|
16
|
+
|
|
17
|
+
export async function setupInput(project, options, vars, signal, { origin } = {}) {
|
|
18
|
+
let answers = {};
|
|
19
|
+
const { loadInstallerSeed } = await shared('setup-manifest.mjs');
|
|
20
|
+
let seed;
|
|
21
|
+
try { seed = project.setupSeed ?? await loadInstallerSeed(project.root, project.adapter); }
|
|
22
|
+
catch { throw invalid('Setup manifest is missing, invalid or incompatible with its installer. Restore the declared manifest and presentation sources.'); }
|
|
23
|
+
project.setupManifest = seed.manifest;
|
|
24
|
+
if (options.answers) {
|
|
25
|
+
const path = await contained(project.root, options.answers, { privateFile: true });
|
|
26
|
+
try { answers = JSON.parse(await readFile(path, 'utf8')); } catch { throw invalid('Answers must be a private JSON object using the installer model names.'); }
|
|
27
|
+
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) throw invalid('Answers must be a JSON object.');
|
|
28
|
+
if (Object.keys(answers).some(key=>/LINK.*TOKEN|IDENTITY_PRIVATE_KEY/i.test(key))) throw invalid('Link credentials are not setup answers. Use RER_LINK_TOKEN_FILE.');
|
|
29
|
+
project.privateInputs=[options.answers];
|
|
30
|
+
}
|
|
31
|
+
const names = { name: 'displayName', ...project.adapter.input?.names, email: localSettingKeys(project).email };
|
|
32
|
+
const hasOwnerName = Object.hasOwn(seed.model.fields, names.name);
|
|
33
|
+
if (!hasOwnerName && options.ownerName !== undefined) throw invalid('This installer does not declare an owner-name field; --owner-name is unsupported.');
|
|
34
|
+
for (const [flag, name] of [['ownerEmail', names.email], ['ownerName', names.name]]) {
|
|
35
|
+
if (options[flag] !== undefined && answers[name] !== undefined && options[flag] !== answers[name]) throw invalid('A setup flag conflicts with the answers file. Choose one consistent answer before setup.');
|
|
36
|
+
if (options[flag] !== undefined) answers[name] = options[flag];
|
|
37
|
+
}
|
|
38
|
+
const defaults=Object.fromEntries(seed.manifest.fields.filter(field=>field.kind!=='secret'&&field.default!==undefined).map(field=>[field.name,field.default]));
|
|
39
|
+
const adapterDefaults=await project.adapter.input?.defaults?.(project.root) ?? {};
|
|
40
|
+
// Older installations saved runtime bindings only. A runtime select value
|
|
41
|
+
// outside the installer domain is not an answer to that field; retain its
|
|
42
|
+
// private bytes and use the adapter's declared local installer choice.
|
|
43
|
+
const savedAnswers = Object.fromEntries(seed.manifest.fields.filter(field => field.kind !== 'secret' && typeof vars[field.name] === 'string' &&
|
|
44
|
+
!(field.kind === 'select' && Object.hasOwn(adapterDefaults, field.name) && !seed.model.fields[field.name].valid(vars[field.name]))).map(field => [field.name, vars[field.name]]));
|
|
45
|
+
const installerAnswers = await savedSetupAnswers(project, seed.model);
|
|
46
|
+
answers = { ...defaults, ...adapterDefaults, ...(installerAnswers ?? savedAnswers), ...answers };
|
|
47
|
+
const savedEmail = vars[names.email];
|
|
48
|
+
if (!answers[names.email] && savedEmail) answers[names.email] = savedEmail;
|
|
49
|
+
if (hasOwnerName) answers[names.name] ??= seed.model.fields[names.name].default ?? 'Owner';
|
|
50
|
+
if(process.env[localDefaultsVariable]==='1')applyLocalDefaults(seed,answers,vars,origin);
|
|
51
|
+
const entered=seed.manifest.fields.filter(field=>field.kind==='secret'&&field.input==='entered');
|
|
52
|
+
for(const field of entered)if(!answers[field.name]&&!vars[field.name]&&process.env[field.name])answers[field.name]=process.env[field.name];
|
|
53
|
+
const known={secretReadiness:Object.fromEntries(seed.manifest.fields.filter(field=>field.kind==='secret').map(field=>[field.name,Boolean(vars[field.name])]))};
|
|
54
|
+
if(!options.nonInteractive&&!options.json&&process.stdin.isTTY){
|
|
55
|
+
const asked=new Set();
|
|
56
|
+
for(;;){
|
|
57
|
+
const required=seed.model.setupModel(answers,known).missing;
|
|
58
|
+
const key=required.find(key=>!asked.has(key)&&seed.model.fieldVisible(key,answers));
|
|
59
|
+
if(!key)break;
|
|
60
|
+
asked.add(key);
|
|
61
|
+
const field=seed.model.fieldDefinition(key,answers);
|
|
62
|
+
if(field.kind==='secret'&&field.input!=='entered')continue;
|
|
63
|
+
const choices=field.options?.map(([value])=>value).join(' | ');
|
|
64
|
+
const label=`${field.label??key} (${key})${choices?` [${choices}]`:''}: `;
|
|
65
|
+
answers[key]=await question(label,field.type==='password',signal);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const invalidKeys=Object.entries(answers).filter(([key,value])=>!seed.model.fields[key]||(value!==''&&value!==undefined&&!seed.model.fields[key].valid(value))).map(([key])=>key);
|
|
69
|
+
const missingKeys=seed.model.setupModel(answers,known).missing;
|
|
70
|
+
const keys=[...new Set([...missingKeys,...invalidKeys])];
|
|
71
|
+
if(keys.length){
|
|
72
|
+
// Only declared metadata and example choices are public, never supplied values.
|
|
73
|
+
const shape={},details=keys.map(key=>{
|
|
74
|
+
const field=Object.hasOwn(seed.model.fields,key)?seed.model.fields[key]:undefined;
|
|
75
|
+
if(!field)return '(undeclared field)';
|
|
76
|
+
const privateValue=field.type==='password',choices=privateValue?undefined:field.options?.map(([value])=>value);
|
|
77
|
+
const type=privateValue?'secret':field.kind??(choices?'select':field.type??'text');
|
|
78
|
+
shape[key]=privateValue?'<private value>':choices?.[0]??'<value>';
|
|
79
|
+
return `${key} (${type}${choices?`; choices: ${choices.map(value=>JSON.stringify(value)).join(' | ')}`:''})`;
|
|
80
|
+
});
|
|
81
|
+
throw missing(`Missing or invalid active setup fields: ${details.join(', ')}. Create an owner-only JSON file (chmod 600 answers.json) shaped ${JSON.stringify(shape)}, then run rer setup --answers answers.json --non-interactive. --owner-email EMAIL and --owner-name NAME are also accepted when declared; use manifest field names inside the JSON, never an environment-file wrapper.`);
|
|
82
|
+
}
|
|
83
|
+
if(!mailbox(answers[names.email]))throw invalid('Provide a valid plain owner mailbox.');
|
|
84
|
+
answers[names.email]=answers[names.email].toLowerCase();
|
|
85
|
+
const {assertLocalOwner}=await shared('local/owner.mjs');
|
|
86
|
+
const owners=await assertLocalOwner(project.root,answers[names.email],message=>refuse(message),project.adapter.hooks?.localOwner,`${names.email} in ${project.adapter.hooks?.secrets?.file??'.dev.vars'}`);
|
|
87
|
+
if(savedEmail&&!mailbox(savedEmail))throw refuse('Saved owner is malformed. Restore private settings or run rer reset --yes.');
|
|
88
|
+
if(savedEmail&&savedEmail.toLowerCase()!==answers[names.email]){
|
|
89
|
+
if(owners.length)throw refuse(`Private setting ${names.email} differs from the retained database owner. Restore ${names.email} in ${project.adapter.hooks?.secrets?.file??'.dev.vars'} to ${answers[names.email]}, then run rer setup --owner-email ${answers[names.email]}; or back up data and run rer reset --yes. Private bytes were kept.`);
|
|
90
|
+
throw refuse(`Owner conflicts with the saved installation (${savedEmail}). Run rer setup --owner-email ${savedEmail}, or back up data and run rer reset --yes.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try{return seed.model.validateInput(answers,known);}catch{throw missing('Installer answers are incomplete. Supply the active manifest fields using rer setup --answers answers.json --non-interactive.');}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function question(label,privateValue,signal){
|
|
97
|
+
let muted=false;
|
|
98
|
+
const output=new Writable({write(chunk,_encoding,done){if(!muted)process.stderr.write(chunk);done();}});
|
|
99
|
+
const prompt=createInterface({input:process.stdin,output,terminal:true});
|
|
100
|
+
prompt.on('SIGINT',()=>process.kill(process.pid,'SIGINT'));
|
|
101
|
+
const closed=new Promise((_,reject)=>prompt.once('close',()=>reject(missing('Terminal input closed before setup answers were complete. Use rer setup --answers answers.json --non-interactive.'))));
|
|
102
|
+
try{const pending=prompt.question(label+'\n',{signal});muted=privateValue;return await Promise.race([pending,closed]);}finally{prompt.close();muted=false;output.end();if(privateValue)process.stderr.write('\n');}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A generated value goes only into the answers, which setup writes to the
|
|
106
|
+
// checkout's 0600 private settings and registers for redaction.
|
|
107
|
+
function applyLocalDefaults(seed,answers,vars,origin){
|
|
108
|
+
for(const field of seed.manifest.fields){
|
|
109
|
+
const value=field.localDefault;
|
|
110
|
+
if(value===undefined||answers[field.name]||vars[field.name]||!seed.model.fieldVisible(field.name,answers))continue;
|
|
111
|
+
answers[field.name]=typeof value==='string'?value:value.generate?randomBytes(32).toString(value.generate):origin;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/cli/shared.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// The only relocation boundary. The portable inventory mirrors Base's cli,
|
|
4
|
+
// modules and operations topology, while reusing the seed's one onboarding copy.
|
|
5
|
+
const portable = basename(dirname(fileURLToPath(new URL('../', import.meta.url)))) === 'scripts';
|
|
6
|
+
export const onboardingURL = new URL(portable ? '../../base-onboarding/' : '../onboarding/', import.meta.url);
|
|
7
|
+
export const shared = file => import(new URL(file, onboardingURL));
|