@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,157 @@
|
|
|
1
|
+
"""Operate only on fresh Docker inventory, never on stored executable config."""
|
|
2
|
+
import subprocess, socket, io, tarfile
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def docker(*args, input=None):
|
|
6
|
+
result = subprocess.run(['docker', *args], input=input, capture_output=True, text=not isinstance(input, bytes), timeout=120)
|
|
7
|
+
if result.returncode:
|
|
8
|
+
# Docker may echo environment/configuration. Never propagate its output.
|
|
9
|
+
refuse('Docker host operation failed: ' + args[0])
|
|
10
|
+
return (result.stdout.decode() if isinstance(result.stdout, bytes) else result.stdout).strip()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def inspect(kind, names):
|
|
14
|
+
return json.loads(docker(kind, 'inspect', *names)) if names else []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def labels_for(plan, role):
|
|
18
|
+
return {**plan['labels'], 'io.runeditrun.role': role}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_labels(plan, role, actual):
|
|
22
|
+
if any((actual or {}).get(key) != value for key, value in labels_for(plan, role).items()):
|
|
23
|
+
refuse('Docker resource ownership mismatch: ' + role)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def inventory(plan):
|
|
27
|
+
resources = {'container': {}, 'volume': {}, 'network': {}}
|
|
28
|
+
# Inventory every container so foreign mounts, network peers and occupied
|
|
29
|
+
# bindings are accounted for, even when they carry no RER label at all.
|
|
30
|
+
containers = inspect('container', docker('container', 'ls', '-aq').split())
|
|
31
|
+
for item in containers:
|
|
32
|
+
resources['container'][item['Name'].lstrip('/')] = item
|
|
33
|
+
for kind in ('volume', 'network'):
|
|
34
|
+
names = docker(kind, 'ls', '--format', '{{.Name}}').splitlines()
|
|
35
|
+
for item in inspect(kind, names):
|
|
36
|
+
resources[kind][item['Name']] = item
|
|
37
|
+
expected = {'container': ['app', 'caddy'], 'volume': ['data', 'caddy', 'caddy-config'], 'network': ['ingress']}
|
|
38
|
+
owned = {'container': {}, 'volume': {}, 'network': {}}
|
|
39
|
+
for kind, objects in resources.items():
|
|
40
|
+
for name, item in objects.items():
|
|
41
|
+
labels = item.get('Config', {}).get('Labels') if kind == 'container' else item.get('Labels')
|
|
42
|
+
matching_name = name in [plan['name'] + '-' + role for role in expected[kind]]
|
|
43
|
+
matching_owner = (labels or {}).get('io.runeditrun.installation') == plan['labels']['io.runeditrun.installation']
|
|
44
|
+
# Old compose-labelled extra services can otherwise be selected by
|
|
45
|
+
# compose down. Refuse these too rather than silently adopting them.
|
|
46
|
+
matching_compose = (labels or {}).get('com.docker.compose.project') == plan['name']
|
|
47
|
+
if matching_name or matching_owner or matching_compose:
|
|
48
|
+
role = name.removeprefix(plan['name'] + '-')
|
|
49
|
+
if role not in expected[kind] or name != plan['name'] + '-' + role:
|
|
50
|
+
refuse('Unexpected Docker installation resource')
|
|
51
|
+
check_labels(plan, role, labels)
|
|
52
|
+
owned[kind][role] = item
|
|
53
|
+
if kind == 'volume' and (item.get('Driver') != 'local' or item.get('Options')):
|
|
54
|
+
refuse('Unsafe Docker volume driver/options')
|
|
55
|
+
if kind == 'network' and (item.get('Driver') != 'bridge' or item.get('Options') or item.get('Internal') or item.get('ConfigOnly')):
|
|
56
|
+
refuse('Unsafe Docker ingress network')
|
|
57
|
+
owned_ids = {item['Id'] for item in owned['container'].values()}
|
|
58
|
+
for item in containers:
|
|
59
|
+
if item['Id'] not in owned_ids:
|
|
60
|
+
if any(mount.get('Name') in {v['Name'] for v in owned['volume'].values()} for mount in item.get('Mounts', [])):
|
|
61
|
+
refuse('Foreign container uses installation volume')
|
|
62
|
+
for network in owned['network'].values():
|
|
63
|
+
if any(key not in owned_ids for key in (network.get('Containers') or {})):
|
|
64
|
+
refuse('Foreign container uses installation network')
|
|
65
|
+
return owned, containers
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def admit_ports(plan, owned, containers):
|
|
69
|
+
own_ids = {item['Id'] for item in owned['container'].values()}
|
|
70
|
+
for binding in plan['ports']:
|
|
71
|
+
address, port, _ = binding.split(':')
|
|
72
|
+
occupied_by_own = False
|
|
73
|
+
for item in containers:
|
|
74
|
+
if not item.get('State', {}).get('Running'):
|
|
75
|
+
continue
|
|
76
|
+
for entries in (item.get('NetworkSettings', {}).get('Ports') or {}).values():
|
|
77
|
+
for entry in entries or []:
|
|
78
|
+
overlap = address == '0.0.0.0' or entry['HostIp'] in ('0.0.0.0', '::', address)
|
|
79
|
+
if entry['HostPort'] == port and overlap:
|
|
80
|
+
if item['Id'] not in own_ids:
|
|
81
|
+
refuse('Host ingress binding is already occupied')
|
|
82
|
+
occupied_by_own = True
|
|
83
|
+
if not occupied_by_own:
|
|
84
|
+
with socket.socket() as listener:
|
|
85
|
+
try:
|
|
86
|
+
listener.bind((address, int(port)))
|
|
87
|
+
except OSError:
|
|
88
|
+
refuse('Host ingress binding is unavailable')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def label_args(plan, role):
|
|
92
|
+
return [arg for key, value in labels_for(plan, role).items() for arg in ('--label', key + '=' + value)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def prepare_resources(plan, owned):
|
|
96
|
+
# Images and persistent resources must be available before stopping service.
|
|
97
|
+
app = json.loads(docker('image', 'inspect', plan['digest']))[0]
|
|
98
|
+
if app['Id'] != plan['digest']:
|
|
99
|
+
refuse('Application image identity mismatch')
|
|
100
|
+
caddy = None
|
|
101
|
+
if not plan['local']:
|
|
102
|
+
if not docker('image', 'ls', '--quiet', 'caddy:2.10.2'):
|
|
103
|
+
docker('image', 'pull', 'caddy:2.10.2')
|
|
104
|
+
caddy = json.loads(docker('image', 'inspect', 'caddy:2.10.2'))[0]['Id']
|
|
105
|
+
for role in (['data'] if plan['local'] else ['data', 'caddy', 'caddy-config']):
|
|
106
|
+
if role not in owned['volume']:
|
|
107
|
+
docker('volume', 'create', *label_args(plan, role), plan['name'] + '-' + role)
|
|
108
|
+
if 'ingress' not in owned['network']:
|
|
109
|
+
docker('network', 'create', '--driver', 'bridge', *label_args(plan, 'ingress'), plan['name'] + '-ingress')
|
|
110
|
+
return caddy
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def replace_containers(plan, validate, files, caddy):
|
|
114
|
+
# Configuration is the exact content checked in the owned generation.
|
|
115
|
+
# Never ask either Docker client or daemon to reopen a host pathname after
|
|
116
|
+
# shutdown; their filesystem/process namespaces may also be different.
|
|
117
|
+
archive = io.BytesIO()
|
|
118
|
+
if caddy:
|
|
119
|
+
content = files['Caddyfile'].encode()
|
|
120
|
+
with tarfile.open(fileobj=archive, mode='w') as bundle:
|
|
121
|
+
entry = tarfile.TarInfo('Caddyfile')
|
|
122
|
+
entry.size, entry.mode = len(content), 0o600
|
|
123
|
+
bundle.addfile(entry, io.BytesIO(content))
|
|
124
|
+
owned, containers = inventory(plan)
|
|
125
|
+
admit_ports(plan, owned, containers)
|
|
126
|
+
validate()
|
|
127
|
+
# Stop by immutable IDs, with fresh labels on each individual operation.
|
|
128
|
+
# rm never uses -v: persistent data survives replacement and failures.
|
|
129
|
+
for role, item in owned['container'].items():
|
|
130
|
+
current = inspect('container', [item['Id']])[0]
|
|
131
|
+
check_labels(plan, role, current['Config'].get('Labels'))
|
|
132
|
+
validate()
|
|
133
|
+
docker('container', 'stop', '--time', '10', item['Id'])
|
|
134
|
+
check_labels(plan, role, inspect('container', [item['Id']])[0]['Config'].get('Labels'))
|
|
135
|
+
docker('container', 'rm', item['Id'])
|
|
136
|
+
common = ['--restart', 'unless-stopped', '--network', plan['name'] + '-ingress']
|
|
137
|
+
app = ['container', 'create', '--name', plan['name'] + '-app', *label_args(plan, 'app'), *common, '--network-alias', 'app', '--env-file', '/dev/stdin', '--env', 'PORT=8080', '--env', 'RER_IMAGE_DIGEST=' + plan['digest'], '--mount', 'type=volume,source=' + plan['name'] + '-data,target=/data']
|
|
138
|
+
if plan['local']:
|
|
139
|
+
for binding in plan['ports']:
|
|
140
|
+
app.extend(['--publish', binding])
|
|
141
|
+
else:
|
|
142
|
+
app.extend(['--env', 'TRUSTED_PROXY=caddy'])
|
|
143
|
+
app.append(plan['digest'])
|
|
144
|
+
inventory(plan)
|
|
145
|
+
app_id = docker(*app, input=files['settings.env'])
|
|
146
|
+
if caddy:
|
|
147
|
+
args = ['container', 'create', '--name', plan['name'] + '-caddy', *label_args(plan, 'caddy'), *common, '--network-alias', 'caddy', '--mount', 'type=volume,source=' + plan['name'] + '-caddy,target=/data', '--mount', 'type=volume,source=' + plan['name'] + '-caddy-config,target=/config']
|
|
148
|
+
for binding in plan['ports']:
|
|
149
|
+
args.extend(['--publish', binding])
|
|
150
|
+
args.append(caddy)
|
|
151
|
+
inventory(plan)
|
|
152
|
+
caddy_id = docker(*args)
|
|
153
|
+
check_labels(plan, 'caddy', inspect('container', [caddy_id])[0]['Config'].get('Labels'))
|
|
154
|
+
docker('container', 'cp', '-', caddy_id + ':/etc/caddy/', input=archive.getvalue())
|
|
155
|
+
docker('container', 'start', caddy_id)
|
|
156
|
+
check_labels(plan, 'app', inspect('container', [app_id])[0]['Config'].get('Labels'))
|
|
157
|
+
docker('container', 'start', app_id)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function superviseRunner({ root, manifest, settings, failed }) {
|
|
4
|
+
if (!manifest.runner) return { running:()=>true, async close(){} };
|
|
5
|
+
const child=spawn(process.execPath,[join(root,'runner',manifest.runner.entry)],{cwd:join(root,'runner'),env:{...process.env,...settings},stdio:['ignore','ignore','ignore']});
|
|
6
|
+
let stopping=false;
|
|
7
|
+
child.once('error',()=>{if(!stopping)failed();});
|
|
8
|
+
child.once('exit',()=>{if(!stopping)failed();});
|
|
9
|
+
return { running:()=>child.exitCode===null&&child.signalCode===null, async close(){stopping=true;if(child.exitCode!==null||child.signalCode!==null)return;child.kill('SIGTERM');await new Promise(resolve=>{const timer=setTimeout(()=>child.kill('SIGKILL'),35000);child.once('exit',()=>{clearTimeout(timer);resolve();});});} };
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ingressBindings } from '../deploy-kit/container-config.mjs';
|
|
3
|
+
export { ingressBindings } from '../deploy-kit/container-config.mjs';
|
|
4
|
+
|
|
5
|
+
export function installationIdentity(project, environment = 'production') {
|
|
6
|
+
if (typeof project !== 'string' || !/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/.test(project) || project.length > 512 || !/^[a-z][a-z0-9-]{0,39}$/.test(environment)) throw Error('Invalid installation identity');
|
|
7
|
+
const identity = createHash('sha256').update(JSON.stringify([project, environment])).digest('hex');
|
|
8
|
+
return { name: `rer-${identity}`, labels: { 'io.runeditrun.managed': 'ssh-v1', 'io.runeditrun.installation': identity, 'io.runeditrun.project': project, 'io.runeditrun.environment': environment } };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function deploymentPlan({ declaration, settings, environment, digest, ingress }) {
|
|
13
|
+
const { name, labels } = installationIdentity(declaration.project, environment);
|
|
14
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest)) throw Error('Invalid Docker image digest');
|
|
15
|
+
const origin = settings[declaration.originVar] ?? settings.PUBLIC_ORIGIN;
|
|
16
|
+
if (!origin) throw Error('Declare the public origin in the private env file');
|
|
17
|
+
const url = new URL(origin);
|
|
18
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw Error('Invalid public origin');
|
|
19
|
+
const local = /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname);
|
|
20
|
+
if (!local && url.protocol !== 'https:') throw Error('Production ingress requires HTTPS');
|
|
21
|
+
if (local && url.protocol !== 'http:') throw Error('Loopback qualification requires an HTTP origin');
|
|
22
|
+
const hostnames = [url.hostname];
|
|
23
|
+
for (const variable of declaration.container?.hostVariables ?? []) {
|
|
24
|
+
const hostname = settings[variable];
|
|
25
|
+
if (!hostname || !/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(hostname)) throw Error('Invalid declared ingress hostname');
|
|
26
|
+
if (!hostnames.includes(hostname)) hostnames.push(hostname);
|
|
27
|
+
}
|
|
28
|
+
const bindings = ingressBindings({ ...declaration.container?.ingress, ...ingress });
|
|
29
|
+
const roleLabels = role => ({ ...labels, 'io.runeditrun.role': role });
|
|
30
|
+
const volume = role => ({ name: `${name}-${role}`, labels: roleLabels(role) });
|
|
31
|
+
const ports = local ? [`127.0.0.1:${url.port || 80}:8080`] : [`${bindings.address}:${bindings.httpPort}:80`, `${bindings.address}:${bindings.httpsPort}:443`];
|
|
32
|
+
const compose = { name, services: { app: { image: digest, container_name: `${name}-app`, labels: roleLabels('app'), restart: 'unless-stopped', environment: { RER_IMAGE_DIGEST: digest, PORT: '8080', ...(local ? {} : { TRUSTED_PROXY: 'caddy' }) }, env_file: [{ path: 'settings.env', format: 'raw' }], volumes: ['data:/data'], networks: ['ingress'], ...(local ? { ports } : {}) } }, volumes: { data: volume('data') }, networks: { ingress: { name: `${name}-ingress`, labels: roleLabels('ingress') } } };
|
|
33
|
+
if (!local) {
|
|
34
|
+
compose.services.caddy = { image: 'caddy:2.10.2', container_name: `${name}-caddy`, labels: roleLabels('caddy'), restart: 'unless-stopped', ports, configs: [{ source: 'caddy', target: '/etc/caddy/Caddyfile' }], volumes: ['caddy:/data', 'caddy-config:/config'], networks: ['ingress'] };
|
|
35
|
+
compose.volumes.caddy = volume('caddy'); compose.volumes['caddy-config'] = volume('caddy-config');
|
|
36
|
+
}
|
|
37
|
+
// Listener ports are internal. A TCP/SNI proxy must preserve the public
|
|
38
|
+
// authority and route ACME HTTP/TLS challenges to these private bindings.
|
|
39
|
+
const sites = hostnames.map(hostname => `https://${hostname}`).join(', ');
|
|
40
|
+
const redirects = hostnames.map(hostname => `http://${hostname} {\n redir https://${hostname}${url.port ? ':' + url.port : ''}{uri} permanent\n}`).join('\n');
|
|
41
|
+
const caddyfile = `{\n auto_https disable_redirects\n}\n${sites} {\n reverse_proxy app:8080\n}\n${redirects}\n`;
|
|
42
|
+
if (!local) compose.configs = { caddy: { content: caddyfile } };
|
|
43
|
+
return { name, labels, origin, digest, local, ports, compose, caddyfile };
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { deploymentPlan } from './ssh-plan.mjs';
|
|
6
|
+
import { deployInputs } from './deploy-input.mjs';
|
|
7
|
+
const execute=promisify(execFile);
|
|
8
|
+
export const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
|
|
9
|
+
export function sshTarget(value){
|
|
10
|
+
let url;try{url=new URL(value);}catch{throw new Error('Host must be ssh://user@host[:port]');}
|
|
11
|
+
if(url.protocol!=='ssh:'||url.password||url.search||url.hash||!['','/'].includes(url.pathname)||!url.username||!/^[a-zA-Z0-9_.-]+$/.test(url.username)||!/^([a-zA-Z0-9.-]+|\[[0-9a-fA-F:]+\])$/.test(url.hostname))throw new Error('Invalid ssh host');
|
|
12
|
+
const args=['-o','BatchMode=yes','-o','StrictHostKeyChecking=yes'];
|
|
13
|
+
if(process.env.RER_SSH_IDENTITY)args.push('-i',resolve(process.env.RER_SSH_IDENTITY));
|
|
14
|
+
if(process.env.RER_SSH_KNOWN_HOSTS)args.push('-o','UserKnownHostsFile='+resolve(process.env.RER_SSH_KNOWN_HOSTS));
|
|
15
|
+
if(url.port)args.push('-p',url.port);args.push(url.username+'@'+url.hostname.replace(/^\[|\]$/g,''));return args;
|
|
16
|
+
}
|
|
17
|
+
export async function deploySSH({root,host,image,envFile,environment='production',localOnlyKeys=[],dryRun=false,ingress}) {
|
|
18
|
+
const ssh=sshTarget(host),{declaration,settings,encoded}=await deployInputs({root,envFile,localOnlyKeys});
|
|
19
|
+
const {stdout:digestText}=await execute('docker',['image','inspect','--format','{{.Id}}',image]);
|
|
20
|
+
const plan=deploymentPlan({declaration,settings,environment,digest:digestText.trim(),ingress});
|
|
21
|
+
const directory=join(root,'.rer/container/deploy');await mkdir(directory,{recursive:true,mode:0o700});
|
|
22
|
+
await writeFile(join(directory,'compose.yaml'),JSON.stringify(plan.compose,null,2)+'\n',{mode:0o600});
|
|
23
|
+
await writeFile(join(directory,'Caddyfile'),plan.caddyfile,{mode:0o600});
|
|
24
|
+
if(dryRun)return{dryRun:true,deployed:false,origin:plan.origin,installation:'.rer-host/'+plan.name,compose:join(directory,'compose.yaml')};
|
|
25
|
+
// The code is transferred from this installation's maintained CLI. No remote
|
|
26
|
+
// Python, shell or compose file is ever imported or executed from disk.
|
|
27
|
+
const source=(await Promise.all(['files','resources','deploy'].map(name=>readFile(new URL('./remote/'+name+'.py',import.meta.url),'utf8')))).join('\n');
|
|
28
|
+
const command='python3 -I -c '+quote(source);
|
|
29
|
+
const remote=async payload=>{
|
|
30
|
+
const child=spawn('ssh',[...ssh,command],{stdio:['pipe','pipe','pipe']});
|
|
31
|
+
const result=completed(child,true);child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify(payload));return result;
|
|
32
|
+
};
|
|
33
|
+
// This also proves Python and Docker availability, filesystem custody and
|
|
34
|
+
// existing resource ownership before loading an image or sending secrets.
|
|
35
|
+
await remote({plan,phase:'check'});
|
|
36
|
+
const load=spawn('ssh',[...ssh,'docker load'],{stdio:['pipe','pipe','pipe']}),save=spawn('docker',['save',plan.digest],{stdio:['ignore','pipe','pipe']});
|
|
37
|
+
load.stdin.on('error',()=>{});save.stdout.pipe(load.stdin);
|
|
38
|
+
try{await Promise.all([completed(save),completed(load)]);}finally{if(save.exitCode===null)save.kill('SIGTERM');if(load.exitCode===null)load.kill('SIGTERM');}
|
|
39
|
+
const result=JSON.parse(await remote({plan,files:{'settings.env':encoded,'compose.yaml':JSON.stringify(plan.compose,null,2)+'\n','Caddyfile':plan.caddyfile}}));
|
|
40
|
+
const end=Date.now()+60000;
|
|
41
|
+
while(Date.now()<end){
|
|
42
|
+
try{const response=await fetch(plan.origin+'/health',{signal:AbortSignal.timeout(3000)});if(response.ok){const health=await response.json();if(health.platform==='container'&&health.deployment?.versionId===plan.digest)return{deployed:true,origin:plan.origin,imageDigest:plan.digest,...result};}}catch{}
|
|
43
|
+
await new Promise(resolve=>setTimeout(resolve,250));
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Container public health did not become ready; inspect host logs');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hostRefusals=new Set(['Unsafe host path ownership, permissions or type','Unsafe account home','Account home is not owned by login user','Host deployment path changed','Host deployment file changed','Installation ownership mismatch','Unexpected deployment file','Unexpected installation path','Unexpected Docker installation resource','Unsafe Docker volume driver/options','Unsafe Docker ingress network','Foreign container uses installation volume','Foreign container uses installation network','Host ingress binding is already occupied','Host ingress binding is unavailable','Application image identity mismatch','Host deployment refused: unsafe path or failed operation']);
|
|
49
|
+
function hostFailure(stderr){
|
|
50
|
+
const reason=stderr.trim();
|
|
51
|
+
const message=hostRefusals.has(reason)||/^Docker resource ownership mismatch: (app|caddy|caddy-config|data|ingress)$/.test(reason)||/^Docker host operation failed: (image|volume|network|container)$/.test(reason)?reason:'SSH host operation failed';
|
|
52
|
+
return Object.assign(Error(message),{code:'SSH_HOST_REFUSAL'});
|
|
53
|
+
}
|
|
54
|
+
function completed(child, capture=false) {
|
|
55
|
+
return new Promise((resolve,reject)=>{
|
|
56
|
+
let stdout='',stderr='';
|
|
57
|
+
const timer=setTimeout(()=>{child.kill('SIGTERM');reject(Error('Host operation timed out'));},120000);
|
|
58
|
+
child.stdout?.on('data',chunk=>{if(capture&&stdout.length<1024*1024)stdout+=chunk;});
|
|
59
|
+
child.stderr?.on('data',chunk=>{if(capture&&stderr.length<2048)stderr+=chunk;});
|
|
60
|
+
child.once('error',()=>{clearTimeout(timer);reject(Error('Host operation failed'));});
|
|
61
|
+
child.once('close',code=>{clearTimeout(timer);code===0?resolve(stdout):reject(capture?hostFailure(stderr):Error('Host operation failed'));});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { D1 } from './adapters/d1.mjs';
|
|
5
|
+
import { Objects } from './adapters/objects.mjs';
|
|
6
|
+
import { ensurePortabilitySchema } from './portability-schema.mjs';
|
|
7
|
+
import { migrate } from './migrations.mjs';
|
|
8
|
+
import { createPortability } from '../portability/index.mjs';
|
|
9
|
+
import { migrationReplay } from '../portability/migration-history.mjs';
|
|
10
|
+
import { exportArchive, importArchive } from '../portability/files.mjs';
|
|
11
|
+
let db,platform,worker;
|
|
12
|
+
try {
|
|
13
|
+
const spec=JSON.parse(await readFile('/private/transfer.json','utf8')),manifest=JSON.parse(await readFile('/app/container.json','utf8'));
|
|
14
|
+
const buckets={};
|
|
15
|
+
if(manifest.engine==='node'){
|
|
16
|
+
db=new D1('/data/'+spec.binding+'.sqlite');await migrate(db,manifest.migrations[spec.binding]);await ensurePortabilitySchema(db,manifest.portabilityMigrations);
|
|
17
|
+
platform=new DatabaseSync('/data/platform.sqlite');
|
|
18
|
+
for(const [name,binding]of Object.entries(spec.buckets))buckets[name]=new Objects(platform,'/data/objects',binding);
|
|
19
|
+
}else{
|
|
20
|
+
const {workerEngine}=await import('./worker-engine.mjs');worker=await workerEngine({root:'/app',data:'/data',manifest,settings:{}});db=worker.databases[spec.binding];
|
|
21
|
+
for(const [name,binding]of Object.entries(spec.buckets))buckets[name]=worker.objects[binding];
|
|
22
|
+
}
|
|
23
|
+
const service=await createPortability({db,buckets,policy:spec.policy,migrations:async()=>migrationReplay(manifest.migrations[spec.binding],spec.policy)});
|
|
24
|
+
const receipt=await(spec.direction==='export'?exportArchive:importArchive)(service,'/archive/'+spec.file,spec.transient,spec.journal);
|
|
25
|
+
console.log(JSON.stringify(receipt));
|
|
26
|
+
}catch(cause){console.error('Container transfer refused or failed:',cause.name);process.exitCode=1;}
|
|
27
|
+
finally{await worker?.close();await db?.close?.();platform?.close();}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Miniflare, convertV4MiniflareOptions } from 'miniflare';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { rootCertificates } from 'node:tls';
|
|
5
|
+
import { ensurePortabilitySchema } from './portability-schema.mjs';
|
|
6
|
+
import { migrate } from './migrations.mjs';
|
|
7
|
+
async function files(root) { const result = []; for (const item of await readdir(root, { withFileTypes: true })) { const path = join(root, item.name); if (item.isDirectory()) result.push(...await files(path)); else if (/\.(m?js)$/.test(item.name)) result.push(path); } return result; }
|
|
8
|
+
export async function workerEngine({ root, data, manifest, settings, edge }) {
|
|
9
|
+
const config = manifest.config, directory = join(root, 'dist/server'), entry = join(root, manifest.entry);
|
|
10
|
+
const modules = [entry, ...(await files(directory)).filter(path => path !== entry)].map(path => ({ type: 'ESModule', path }));
|
|
11
|
+
const app = { name:'app', modules, modulesRoot:directory, compatibilityDate:config.compatibility_date, compatibilityFlags:config.compatibility_flags,
|
|
12
|
+
bindings:{ ...config.vars, ...settings }, d1Databases:Object.fromEntries((config.d1_databases ?? []).map(item => [item.binding, item.database_name])),
|
|
13
|
+
r2Buckets:Object.fromEntries((config.r2_buckets ?? []).map(item => [item.binding, item.bucket_name])),
|
|
14
|
+
durableObjects:Object.fromEntries((config.durable_objects?.bindings ?? []).map(item => [item.name, { className:item.class_name, useSQLite:true }])),
|
|
15
|
+
queueProducers:Object.fromEntries((config.queues?.producers ?? []).map(item => [item.binding, item.queue])),
|
|
16
|
+
queueConsumers:Object.fromEntries((config.queues?.consumers ?? []).map(item => [item.queue, { maxBatchSize:item.max_batch_size, maxRetries:item.max_retries }])),
|
|
17
|
+
assets:{ directory:join(root, 'dist/client'), binding:config.assets?.binding ?? 'ASSETS', routerConfig:{ has_user_worker:true }, run_worker_first:config.assets?.run_worker_first ?? false },
|
|
18
|
+
outboundService: edge ? request => edge(request, request => mf.getWorker('egress').then(worker => worker.fetch(request))) : 'egress',
|
|
19
|
+
};
|
|
20
|
+
const mf = new Miniflare(convertV4MiniflareOptions({ host:'127.0.0.1',port:0,cf:false,resourcePersistencePath:data,unsafeLocalExplorer:false,
|
|
21
|
+
workers:[{ name:'ingress',modules:true,script:`export default {fetch(request,env){const observed=request.headers.get('x-rer-original-url');if(!observed)return new Response(null,{status:400});const url=new URL(observed),headers=new Headers(request.headers);headers.delete('x-rer-original-url');headers.set('host',url.host);return env.APP.fetch(new Request(url,{method:request.method,headers,redirect:'manual',...(!['GET','HEAD'].includes(request.method)?{body:request.body}:{})}))}}`,serviceBindings:{ APP:'app' } }, app,
|
|
22
|
+
{ name:'egress',modules:true,script:'export default {fetch(request,env){return env.NETWORK.fetch(request)}}',serviceBindings:{ NETWORK:{ network:{ allow:['public'],tlsOptions:{ trustBrowserCas:true,trustedCertificates:[...rootCertificates] } } } } }],
|
|
23
|
+
}));
|
|
24
|
+
await mf.ready; const databases = {}, objects = {};
|
|
25
|
+
for (const [binding, plan] of Object.entries(manifest.migrations)) { const db = await mf.getD1Database(binding, 'app'); databases[binding] = db; await migrate(db, plan); await ensurePortabilitySchema(db, manifest.portabilityMigrations); }
|
|
26
|
+
for (const bucket of config.r2_buckets ?? []) objects[bucket.binding] = await mf.getR2Bucket(bucket.binding, 'app');
|
|
27
|
+
const worker = await mf.getWorker('app');
|
|
28
|
+
return { mf, databases, objects, app:worker,
|
|
29
|
+
fetch(request) { const headers = new Headers(request.headers); headers.set('x-rer-original-url', request.url); return mf.dispatchFetch(request.url, { method:request.method,headers,body:request.body,duplex:'half',redirect:'manual' }); },
|
|
30
|
+
scheduled:event => worker.scheduled(event), async drain() {}, dead:() => 0, close:() => mf.dispose(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Application dependency links
|
|
2
|
+
|
|
3
|
+
This module implements the application side of the pull-link protocol. Auth's
|
|
4
|
+
provider implementation (auth-7) is separate. `createDependencies` mounts the
|
|
5
|
+
well-known routes and owner controls; `AuthConfig.dependencies: true` resolves
|
|
6
|
+
OIDC client settings from the persisted link and the server-generated key.
|
|
7
|
+
No client ID or private-key environment answer is needed. The default seed and
|
|
8
|
+
composed foundation declare an optional `oidc-provider@1` dependency using the
|
|
9
|
+
`IDENTITY_PROVIDER` URL answer. A blank answer keeps email-link access working.
|
|
10
|
+
Other interfaces use the same handshake, storage and callback; only the OIDC
|
|
11
|
+
adapter adds OIDC discovery, sessions, domain policy and owner claims.
|
|
12
|
+
|
|
13
|
+
## Discovery and registration
|
|
14
|
+
|
|
15
|
+
`GET /.well-known/rer-dependencies` returns `name`, `version: 1`, the exact
|
|
16
|
+
`origin`, stable `sector_id`, `provides`, `dependencies`, `claim`, `callback`, and
|
|
17
|
+
`jwks_uri`. Each dependency has `interface` (for example `oidc-provider@1`),
|
|
18
|
+
`optional`, `cardinality: 1`, selected `provider` origin or null, registered
|
|
19
|
+
same-origin `paths` as absolute URLs, and a per-interface `jwks_uri`.
|
|
20
|
+
`GET /.well-known/rer-link/jwks/INTERFACE` publishes an ES256 public JWK only.
|
|
21
|
+
The unqualified JWKS URL selects the identity interface. The application retains
|
|
22
|
+
its private key in the server database. Sector/install identity survives re-link,
|
|
23
|
+
provider moves and export/import. Keys are isolated per interface.
|
|
24
|
+
|
|
25
|
+
Provider origins and metadata endpoints require HTTPS. The application's local
|
|
26
|
+
runtime explicitly permits loopback HTTP; deployed registration requires HTTPS.
|
|
27
|
+
Every provider serves `/.well-known/rer-dependencies` with the same graph fields,
|
|
28
|
+
including its `provides` list and selected dependency provider origins. The CLI
|
|
29
|
+
traverses these documents before setup effects, including selected optional
|
|
30
|
+
edges, and refuses cycles, invalid references and oversized graphs.
|
|
31
|
+
|
|
32
|
+
A provider serves `GET /.well-known/rer-provider` with `issuer`, `version: 1`,
|
|
33
|
+
`interface` or `provides`, and absolute `initiate`, `redeem`, `deregister` URLs.
|
|
34
|
+
All three endpoints must be HTTPS on exactly that issuer, with no query,
|
|
35
|
+
fragment or URL credentials. OIDC additionally serves standards discovery with
|
|
36
|
+
`rer_interfaces: ["oidc-provider@1.0.0"]` and compatible authorization, token,
|
|
37
|
+
JWKS, ES256 and private_key_jwt support. The application pins those metadata and
|
|
38
|
+
interface versions; callback input cannot select an endpoint or change the pin.
|
|
39
|
+
|
|
40
|
+
## Owner approval and pull handshake
|
|
41
|
+
|
|
42
|
+
1. The provider owner approves this exact app origin, interface and callback set.
|
|
43
|
+
For unattended `rer setup` or `rer deploy`, the provider issues an opaque,
|
|
44
|
+
one-use approval lasting at most one hour. Save it in an untracked 0600 file
|
|
45
|
+
named by `RER_LINK_TOKEN_FILE`. For multiple interfaces, use
|
|
46
|
+
`RER_LINK_TOKEN_FILES` instead: a JSON map such as
|
|
47
|
+
`{"oidc-provider@1":"auth-approval.json","error-sink@1":"errors-approval.json"}`.
|
|
48
|
+
Each interface needs a distinct private file; all pending approvals are
|
|
49
|
+
validated before setup effects and each consumed file is deleted. Its JSON envelope is
|
|
50
|
+
`{token, issuer, interface, origin, callbacks, issuedAt, expiresAt}`; times are
|
|
51
|
+
integer epoch milliseconds. `callbacks` is exactly the fixed link callback
|
|
52
|
+
plus the dependency's registered paths. Git-tracked files are refused.
|
|
53
|
+
2. Before any provider callback, the app must hold its own ten-minute approval.
|
|
54
|
+
On an empty app, `POST /auth/dependencies/setup` accepts `{issuer,interface,token}`
|
|
55
|
+
using the app's private setup token, or the local CLI proof on loopback only.
|
|
56
|
+
Local onboarding opens these approvals before native bootstrap. Hosted deploy
|
|
57
|
+
uses the deployed app's setup token, never a local HMAC. Base account bootstrap binds
|
|
58
|
+
pending setup approvals to that owner's version and exact bootstrap session;
|
|
59
|
+
callback commit requires that session to remain active, unrevoked and unexpired.
|
|
60
|
+
A signed IdP owner claim may convert that same session to OIDC while sibling
|
|
61
|
+
setup approvals remain pending. Those approvals also require enabled provider
|
|
62
|
+
sign-in, the current policy generation and the session's matching linked
|
|
63
|
+
provider pin, including when claim completes during callback redemption.
|
|
64
|
+
After claim, use the current owner's fresh email proof at
|
|
65
|
+
`/auth/dependencies/relink`.
|
|
66
|
+
The CLI posts `{origin, interface, token}` to the provider's pinned `initiate`
|
|
67
|
+
endpoint. Interactive provider-owned approval can start the same pull without
|
|
68
|
+
a CLI token. The provider fetches the app's document and public JWKS, verifies
|
|
69
|
+
its approval against that origin and exact callback set, and stores the key,
|
|
70
|
+
sector, interface and registration paths with a short one-use code.
|
|
71
|
+
3. The provider posts only `{issuer, interface, code}` to the fixed app URL
|
|
72
|
+
`/.well-known/rer-link/callback`. The app accepts only its configured/trusted
|
|
73
|
+
issuer named by that fresh app-local approval. Every callback, including an
|
|
74
|
+
unchanged link or a link after unlink, requires a new approval. The exact
|
|
75
|
+
attempt, owner/session authority and link snapshot are rechecked atomically
|
|
76
|
+
after redemption; replacing an approval invalidates in-flight callbacks. It never
|
|
77
|
+
follows a callback URL supplied in the request.
|
|
78
|
+
4. The app durably consumes the code digest and posts to the pinned `redeem`
|
|
79
|
+
endpoint: `{code, interface, origin, callback, client_assertion_type,
|
|
80
|
+
client_assertion}`. Assertion type is
|
|
81
|
+
`urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Its signed ES256 JWT
|
|
82
|
+
has `iss` and `sub` equal to the app origin, `aud` equal to the redeem URL,
|
|
83
|
+
`iat`, `exp` at most 60 seconds later, unique `jti`, `code`, `interface`,
|
|
84
|
+
`sector_id` and the fixed `callback`.
|
|
85
|
+
5. The provider verifies the signature against the key fetched at approval,
|
|
86
|
+
audience, times, unique assertion/code use and every bound registration
|
|
87
|
+
property. Code redemption is atomic: concurrent requests have one winner.
|
|
88
|
+
The response contains `{issuer, interface, origin, callback, sector_id,
|
|
89
|
+
client_id}`. For an existing same-provider link it must retain the client ID
|
|
90
|
+
and provide `continuity: "previous-key"` after proving the previous key, or
|
|
91
|
+
`continuity: "provider-owner"` after fresh provider-owner approval. A new key
|
|
92
|
+
alone is never continuity proof. The app checks these values and commits the
|
|
93
|
+
link and OIDC pins in one transaction. The provider returns `{linked:true}`
|
|
94
|
+
from initiation only after the app accepted the callback.
|
|
95
|
+
6. The CLI checks the app's accepted link then deletes its approval file. The
|
|
96
|
+
token never enters public answers, runtime variables, logs or exports. An
|
|
97
|
+
unchanged completed link needs no token on later setup. An explicitly supplied
|
|
98
|
+
token is always validated. Changed provider answers retain the old link and
|
|
99
|
+
require a new app-local approval and provider approval.
|
|
100
|
+
|
|
101
|
+
Provider implementations must enforce code and token expiry, one-use redemption,
|
|
102
|
+
origin/callback binding and key continuity themselves. The application-side
|
|
103
|
+
network tests model those provider obligations without contacting a live service.
|
|
104
|
+
|
|
105
|
+
## App owner controls and claims
|
|
106
|
+
|
|
107
|
+
Normal Base account setup claims the native owner first, then links. Only that first setup's
|
|
108
|
+
successful identity link enables `idpSignIn` automatically. The setup attempt
|
|
109
|
+
must have been opened before that native claim; a native claim without one
|
|
110
|
+
closes eligibility in its transaction. A link completed before a native claim
|
|
111
|
+
cannot authorize a later relink to enable sign-in. The CLI closes that
|
|
112
|
+
first-setup eligibility after successful setup, including a blank optional
|
|
113
|
+
provider. Existing and imported installs remain off until their current owner
|
|
114
|
+
enables the app setting. `GET/POST /auth/settings/identity` has JSON and plain HTML
|
|
115
|
+
forms, requires the current owner, records effective changes in `oa_auth_audit`,
|
|
116
|
+
and never accepts platform authority. Any valid settings POST closes setup
|
|
117
|
+
eligibility, including an unchanged value, without auditing a nonexistent change
|
|
118
|
+
or advancing the session generation. Turning off increments a session generation
|
|
119
|
+
and revokes provider sessions. In-flight callbacks cannot cross that generation;
|
|
120
|
+
turning on never restores old sessions. Provider identity or roles alone grant no
|
|
121
|
+
app membership: accounts or invitations remain authoritative.
|
|
122
|
+
|
|
123
|
+
An app linked before claim advertises `claim: true` and requests the OIDC `claim`
|
|
124
|
+
scope. SETUP owner-claim mode `idp` names the declared identity dependency and
|
|
125
|
+
`/auth/owner-claim`. The provider can return `install_owner` from link redemption
|
|
126
|
+
or an OIDC ID token containing the grant. The former value is itself a signed JWT;
|
|
127
|
+
the latter is the signed ID token. Both use the pinned issuer, client audience,
|
|
128
|
+
subject, fresh verified designated email, unique jti, iat and exp (at most five
|
|
129
|
+
minutes), and `install_owner: {client_id, sector_id, subject, email}`. The app
|
|
130
|
+
requires all four properties to match its own installation and designated owner.
|
|
131
|
+
The existing bootstrap transaction commits owner, grant consumption, binding,
|
|
132
|
+
setting audit and provider session together. Durable issuer/client uniqueness
|
|
133
|
+
allows one owner grant even after a restart; the grant never becomes an ordinary
|
|
134
|
+
role or a general sign-in credential. CLI readiness also verifies the local owner
|
|
135
|
+
exists; linking alone is insufficient.
|
|
136
|
+
|
|
137
|
+
`POST /auth/dependencies/relink` with `{interface,issuer}` requires the current
|
|
138
|
+
owner's fresh app-local email proof. It creates a ten-minute pending approval
|
|
139
|
+
bound to interface, issuer, exact callback set, owner, owner version and session. A provider move
|
|
140
|
+
keeps existing principal bindings immutable; new-issuer access needs a newly
|
|
141
|
+
proved email matching an existing account/invitation. `POST
|
|
142
|
+
/auth/dependencies/unlink` with `{interface}` uses the same owner proof, deletes
|
|
143
|
+
that interface's link, key and pending approval while retaining the sector, revokes only its provider sessions, and lifts only its
|
|
144
|
+
provider domain rules before any network call. Signed deregistration is best
|
|
145
|
+
effort; offline providers cannot prevent local removal.
|
|
146
|
+
|
|
147
|
+
## Sessions, logout and portability
|
|
148
|
+
|
|
149
|
+
Provider sessions record issuer, subject, sid and policy generation and last at
|
|
150
|
+
most 24 hours. OIDC sessions also record their provider pin. Each completed
|
|
151
|
+
re-link advances that provider's monotonic link timestamp, including an unchanged
|
|
152
|
+
metadata re-link in the same millisecond. Binding, invitation acceptance and
|
|
153
|
+
session issuance check the captured pin inside their transaction; session reads
|
|
154
|
+
and privileged writes also require that pin to remain linked. Unrelated
|
|
155
|
+
providers and native sessions retain their own authority. Existing consumers
|
|
156
|
+
apply `accounts/schema/provider-pins.sql` as a new numbered migration (the
|
|
157
|
+
default seed uses `0014_provider_pins.sql`). Existing consumers also apply
|
|
158
|
+
`operator-auth/accounts/schema/dependency-setup.sql` and
|
|
159
|
+
`dependencies/schema/approval-attempt.sql` as new migrations (default-seed
|
|
160
|
+
`0015` and `0016`). Native-account consumers apply only the latter alongside
|
|
161
|
+
their own native migrations; they must not install the OA setup schema. The
|
|
162
|
+
latter invalidates older pending approvals. Auth email proof must be dated, valid and no older than 30 days,
|
|
163
|
+
even for a bound account. Explicit mappings between an upstream subject and email remain
|
|
164
|
+
supported only when they do not contradict an asserted stale proof.
|
|
165
|
+
|
|
166
|
+
`POST /auth/oidc/auth/logout` accepts `logout_token` and ignores other fields.
|
|
167
|
+
Use a signed JWT with `typ: "logout+jwt"`, pinned issuer/client audience, current
|
|
168
|
+
iat/exp, nonempty durable one-use jti, no nonce property, and sub or sid. When
|
|
169
|
+
both selectors exist they must agree. `events` must contain an empty object at
|
|
170
|
+
`http://schemas.openid.net/event/backchannel-logout` or
|
|
171
|
+
`https://runeditrun.com/events/identity-fence`; unknown events are refused.
|
|
172
|
+
The fence event increments only the selected person's durable identity fence.
|
|
173
|
+
All their fenced sessions and minted credentials then fail admission, while
|
|
174
|
+
other people remain active. Ordinary ID tokens with any `events` property are
|
|
175
|
+
refused. Credential consumers must record/check the fence; the provided
|
|
176
|
+
`mintCredential`/`authenticateCredential` functions also refuse legacy credentials
|
|
177
|
+
with no recorded fence. Editor grants require the actual current install owner;
|
|
178
|
+
OIDC sessions must also match that owner's current issuer/subject account binding.
|
|
179
|
+
|
|
180
|
+
Exports omit server link keys, approvals, code state, provider sessions and OIDC
|
|
181
|
+
transients, preserve durable people/bindings/grant use/sector, set sign-in off and
|
|
182
|
+
mark links as needing re-link. Import checks these conditions and requires an
|
|
183
|
+
explicit fresh provider approval. Local `rer import` returns `needsRelink: true`
|
|
184
|
+
and `nextCommand: "rer setup"`; that setup continuation consumes the fresh approval
|
|
185
|
+
and retains the imported sector and bindings. `rer doctor` reports dangling providers or a
|
|
186
|
+
needed re-link without reading or emitting private keys or approvals.
|
|
187
|
+
|
|
188
|
+
## Native accounts
|
|
189
|
+
|
|
190
|
+
Native `createOperatorAuth` composition reuses this protocol with
|
|
191
|
+
`accounts.dependencies: NativeDependencyAuthority<R>`. Missing capabilities
|
|
192
|
+
refuse enablement. Native authority checks and revocation statements join the
|
|
193
|
+
same D1 link/unlink transaction; verified logout consumption uses the native
|
|
194
|
+
binding/session/fence store atomically. No OA account tables are queried.
|
|
195
|
+
Every native callback requires a new local owner approval, including initial
|
|
196
|
+
and unchanged links. Shared code fences the exact attempt and approving
|
|
197
|
+
person/session/version snapshot at preflight and commit; native contributions
|
|
198
|
+
repeat their own current authority. Ordinary owner linking preserves disabled
|
|
199
|
+
identity regardless of stale setup flags. Unlink deletes the link, key and
|
|
200
|
+
pending approval while preserving the sector.
|
|
201
|
+
|
|
202
|
+
Native dependency setup before a first owner is currently unsupported:
|
|
203
|
+
`/auth/dependencies/setup` and `auth.authorizeDependencySetup` explicitly refuse
|
|
204
|
+
with `native_capability_unavailable` (501) before pending/key mutation or OA
|
|
205
|
+
setup SQL. Claim the native owner through the seed's supported bootstrap, then
|
|
206
|
+
confirm a local email proof and approve at `/auth/dependencies/relink`. A native
|
|
207
|
+
`claimOwner` capability alone does not make an empty installation's initial IdP
|
|
208
|
+
link/claim journey available. Static incumbent providers remain available beside
|
|
209
|
+
linked Auth.
|
|
210
|
+
See [the exact native contract](../operator-auth/docs/native-accounts.md) for
|
|
211
|
+
method arguments, migration selection, proof fences and adoption steps.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { providerPinFence } from '../operator-auth/accounts/provider-pin.mjs';
|
|
2
|
+
import { ownerFence } from './authority.mjs';
|
|
3
|
+
import { CALLBACK, insist } from './protocol.mjs';
|
|
4
|
+
|
|
5
|
+
export const callbacksFor = (origin, declared) => JSON.stringify([origin+CALLBACK,...declared.paths.map(path=>origin+path)].sort());
|
|
6
|
+
export async function approveSetup({db,auth,request,input,contract,callbacks,clock}) {
|
|
7
|
+
// Native setup needs durable native bootstrap provenance. Until that optional
|
|
8
|
+
// capability exists, refuse before touching pending approvals or OA tables.
|
|
9
|
+
insist(!auth.native,'native_capability_unavailable',501);
|
|
10
|
+
await auth.authorizeDependencySetup(request,input);
|
|
11
|
+
const attempt=crypto.randomUUID(),expires=clock()+600000,guard=crypto.randomUUID();
|
|
12
|
+
await db.batch([
|
|
13
|
+
db.prepare('INSERT INTO rer_dependency_codes(digest,created_at) SELECT ?,CASE WHEN NOT EXISTS(SELECT 1 FROM oa_operators) AND NOT EXISTS(SELECT 1 FROM oa_bootstrap) THEN ? ELSE -1 END').bind(guard,clock()),
|
|
14
|
+
db.prepare("INSERT INTO rer_dependency_pending(interface,issuer,approved_by,expires_at,approved_session,approved_version,callbacks,attempt_id) VALUES(?,?,'setup',?,?,0,?,?) ON CONFLICT(interface) DO UPDATE SET issuer=excluded.issuer,approved_by=excluded.approved_by,expires_at=excluded.expires_at,approved_session=excluded.approved_session,approved_version=excluded.approved_version,callbacks=excluded.callbacks,attempt_id=excluded.attempt_id").bind(contract,input.issuer,expires,attempt,callbacks,attempt),
|
|
15
|
+
db.prepare('INSERT INTO oa_setup_links(interface,attempt_id,expires_at) VALUES(?,?,?) ON CONFLICT(interface) DO UPDATE SET attempt_id=excluded.attempt_id,expires_at=excluded.expires_at,owner_id=NULL,owner_version=NULL,session_id=NULL,completed=0').bind(contract,attempt,expires),
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
export async function approveOwner({store,actor,input,contract,callbacks,clock,native}) {
|
|
19
|
+
const fence=native?await native.ownerFence(actor,clock()):ownerFence(actor,clock());
|
|
20
|
+
const approval=await store.run(`INSERT INTO rer_dependency_pending(interface,issuer,approved_by,expires_at,approved_session,approved_version,callbacks,attempt_id) SELECT ?,?,?,?,?,?,?,? WHERE ${fence.sql} ON CONFLICT(interface) DO UPDATE SET issuer=excluded.issuer,approved_by=excluded.approved_by,expires_at=excluded.expires_at,approved_session=excluded.approved_session,approved_version=excluded.approved_version,callbacks=excluded.callbacks,attempt_id=excluded.attempt_id`,contract,input.issuer,actor.operator.id,clock()+600000,actor.id,actor.operator.version,callbacks,crypto.randomUUID(),...fence.args);
|
|
21
|
+
insist(approval.meta.changes===1,'authority_changed',409);
|
|
22
|
+
}
|
|
23
|
+
// Snapshot the attempt before network I/O. Every authority and exact attempt
|
|
24
|
+
// predicate is executed again in the transaction that installs the link.
|
|
25
|
+
export async function approvalFence(pending,contract,issuer,callbacks,now,pendingPin,native) {
|
|
26
|
+
const base={sql:'EXISTS(SELECT 1 FROM rer_dependency_pending p WHERE p.interface=? AND p.issuer=? AND p.callbacks=? AND p.expires_at>? AND p.attempt_id=?',args:[contract,issuer,callbacks,now,pending.attempt_id]};
|
|
27
|
+
if(native) {
|
|
28
|
+
const authority=await native.approvalFence({pending,contract,issuer,callbacks,now});
|
|
29
|
+
// The shared protocol owns attempt/snapshot equality. An adopter contributes
|
|
30
|
+
// native authority only; replacing any approval invalidates this callback.
|
|
31
|
+
return {sql:`${base.sql} AND p.approved_by=? AND p.approved_session=? AND p.approved_version=?) AND (${authority.sql})`,args:[...base.args,pending.approved_by,pending.approved_session,pending.approved_version,...authority.args]};
|
|
32
|
+
}
|
|
33
|
+
// A signed IdP claim converts the bootstrap session in the same transaction.
|
|
34
|
+
// Resolve its live provenance here: a callback may have started before claim.
|
|
35
|
+
if(pending.approved_by==='setup')return {
|
|
36
|
+
sql:base.sql+` AND EXISTS(SELECT 1 FROM oa_setup_links l WHERE l.interface=p.interface AND l.attempt_id=p.attempt_id AND l.completed=0 AND l.expires_at>?
|
|
37
|
+
AND ((l.owner_id IS NULL AND l.owner_version IS NULL AND l.session_id IS NULL AND NOT EXISTS(SELECT 1 FROM oa_operators) AND NOT EXISTS(SELECT 1 FROM oa_bootstrap))
|
|
38
|
+
OR EXISTS(SELECT 1 FROM oa_bootstrap b JOIN oa_operators o ON o.id=b.claim_id JOIN oa_sessions s ON s.operator_id=o.id
|
|
39
|
+
WHERE o.id=l.owner_id AND o.role='owner' AND o.status='active' AND o.version=l.owner_version
|
|
40
|
+
AND s.id=l.session_id AND s.operator_version=l.owner_version AND s.revoked_at IS NULL AND s.expires_at>?
|
|
41
|
+
AND (s.kind='native' OR (s.kind='oidc' AND EXISTS(
|
|
42
|
+
SELECT 1 FROM oa_provider_sessions ps
|
|
43
|
+
JOIN oa_auth_settings a ON a.singleton=1 AND a.idp_sign_in=1 AND a.generation=ps.generation
|
|
44
|
+
JOIN oa_provider_session_pins pin ON pin.session_id=ps.session_id AND pin.provider_id='auth' AND pin.issuer=ps.issuer
|
|
45
|
+
JOIN oa_identity_providers provider ON provider.provider_id=pin.provider_id AND provider.status='linked'
|
|
46
|
+
AND provider.issuer=pin.issuer AND provider.client_id=pin.client_id AND provider.linked_at=pin.linked_at
|
|
47
|
+
WHERE ps.session_id=s.id)))))))`,args:[...base.args,now,now],
|
|
48
|
+
};
|
|
49
|
+
const pin=providerPinFence(pendingPin);
|
|
50
|
+
return {sql:`${pin.sql} AND ${base.sql} AND p.approved_by=? AND p.approved_session=? AND p.approved_version=? AND EXISTS(SELECT 1 FROM oa_operators o JOIN oa_sessions s ON s.operator_id=o.id WHERE o.id=p.approved_by AND o.version=p.approved_version AND s.id=p.approved_session AND s.operator_version=o.version AND s.revoked_at IS NULL AND s.expires_at>? AND o.status='active' AND o.id=(SELECT a.id FROM oa_operators a LEFT JOIN oa_bootstrap b ON b.claim_id=a.id WHERE a.role='owner' AND a.status='active' ORDER BY b.claim_id IS NULL,a.created_at,a.id LIMIT 1)))`,args:[...pin.args,...base.args,pending.approved_by,pending.approved_session,pending.approved_version,now]};
|
|
51
|
+
}
|