@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,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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { nativeDependencyAuthority } from './native.mjs';
|
|
2
|
+
import { createIdpOwnerClaimHandler } from '../owner-claim/idp.mjs';
|
|
3
|
+
import { createOidcProviders } from '../operator-auth/identity/oidc/index.mjs';
|
|
4
|
+
import { dependencyStore } from './store.mjs';
|
|
5
|
+
import { backchannelLogout } from './logout.mjs';
|
|
6
|
+
// Resolve the persisted server key at use, so Worker instances never cache a removed link.
|
|
7
|
+
export function linkedProviders(options) {
|
|
8
|
+
const {db,config,staticProviders,native}=options;
|
|
9
|
+
if(native)nativeDependencyAuthority(native);
|
|
10
|
+
if(config.dependencies && (staticProviders?.ids.includes('auth') || staticProviders?.callbackPaths?.['/auth/oidc/auth/callback'])) throw new Error('dependency_provider_conflict');
|
|
11
|
+
async function current() {
|
|
12
|
+
if (!config.dependencies) return staticProviders;
|
|
13
|
+
const store=dependencyStore(db),link=await store.link();
|
|
14
|
+
if(!link || link.needs_relink) return staticProviders;
|
|
15
|
+
const row=await store.first("SELECT private_key FROM rer_dependency_keys WHERE interface='oidc-provider@1'");
|
|
16
|
+
if(!row?.private_key) return null;
|
|
17
|
+
const unclaimed=native?()=>native.unclaimed():async()=>!await db.prepare('SELECT 1 FROM oa_operators LIMIT 1').first();
|
|
18
|
+
return createOidcProviders({...options, enabled:async()=>await options.enabled() || await unclaimed(), claim:async(request,token,expectedPin)=>{
|
|
19
|
+
if(!await unclaimed())return null;
|
|
20
|
+
if(native)return native.claimOwner?native.claimOwner({request,token,expectedPin}):null;
|
|
21
|
+
return createIdpOwnerClaimHandler({expectedPin,db,authConfig:config,clientIp:()=>options.clientIp(request),clock:options.clock,fetch:options.fetch})(new Request(config.origin+'/auth/owner-claim',{method:'POST',headers:{Host:new URL(config.origin).host,Origin:config.origin,'Content-Type':'application/json'},body:JSON.stringify({token})}));
|
|
22
|
+
}, claimScope: unclaimed, config:{origin:config.origin,local:config.local,transactionSecret:config.OIDC_TRANSACTION_SECRET ?? config.SESSION_SECRET+'-oidc',providers:[{
|
|
23
|
+
id:'auth',preset:'auth',issuer:link.issuer,clientId:link.client_id,privateKey:JSON.parse(row.private_key),
|
|
24
|
+
}]}});
|
|
25
|
+
}
|
|
26
|
+
if(!config.dependencies) return staticProviders;
|
|
27
|
+
return {
|
|
28
|
+
ids:[...new Set(['auth',...(staticProviders?.ids??[])])],
|
|
29
|
+
callbackPaths:{...staticProviders?.callbackPaths,'/auth/oidc/auth/callback':'auth'},
|
|
30
|
+
async begin(...args){const p=args[1]!=='auth'?staticProviders:await current();return p?p.begin(...args):Response.json({error:'provider_unlinked'},{status:403});},
|
|
31
|
+
async callback(...args){const p=args[1]!=='auth'?staticProviders:await current();return p?p.callback(...args):Response.json({error:'provider_unlinked'},{status:403});},
|
|
32
|
+
async logout(request){return backchannelLogout({...options,request});},
|
|
33
|
+
async list(){const p=await current();return p===staticProviders?(p?.list()??[]):[...await staticProviders?.list()??[],...await p?.list()??[]];},
|
|
34
|
+
async links(){const p=await current();const links=[...await staticProviders?.links()??[],...(p!==staticProviders?await p?.links()??[]:[])];return [...new Map(links.map(link=>[link.id,link])).values()];},
|
|
35
|
+
// The Auth dependency is linked and unlinked through its own approval routes;
|
|
36
|
+
// a static provider keeps its own pin controls.
|
|
37
|
+
async relink(id){if(id==='auth')throw new Error('Use the dependency link with fresh local owner proof');return staticProviders.relink(id);},
|
|
38
|
+
async unlink(id){if(id==='auth')throw new Error('Use the dependency unlink with fresh local owner proof');return staticProviders.unlink(id);},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { providerPinFence } from '../operator-auth/accounts/provider-pin.mjs';
|
|
2
|
+
// Repeat local owner authority inside the mutation transaction.
|
|
3
|
+
export function ownerFence(actor, now) {
|
|
4
|
+
const pin=providerPinFence(actor.providerPin);
|
|
5
|
+
return {sql:`${pin.sql} AND EXISTS(SELECT 1 FROM oa_sessions s JOIN oa_operators o ON o.id=s.operator_id
|
|
6
|
+
WHERE s.id=? AND o.id=? AND o.version=? AND s.operator_version=o.version AND s.revoked_at IS NULL AND s.expires_at>?
|
|
7
|
+
AND o.status='active' AND o.id=(SELECT p.id FROM oa_operators p LEFT JOIN oa_bootstrap b ON b.claim_id=p.id WHERE p.role='owner' AND p.status='active' ORDER BY b.claim_id IS NULL,p.created_at,p.id LIMIT 1))`,args:[...pin.args,actor.id,actor.operator.id,actor.operator.version,now]};
|
|
8
|
+
}
|
|
9
|
+
export function pinStatement(db, clientId, metadata, now) {
|
|
10
|
+
return db.prepare(`INSERT INTO oa_identity_providers(provider_id,preset,status,issuer,client_id,jwks_uri,authorization_endpoint,token_endpoint,algorithms,token_auth_method,response_iss,interface_version,linked_at)
|
|
11
|
+
VALUES('auth','auth','linked',?,?,?,?,?,?,'private_key_jwt',?,?,?) ON CONFLICT(provider_id) DO UPDATE SET
|
|
12
|
+
preset='auth',status='linked',issuer=excluded.issuer,client_id=excluded.client_id,jwks_uri=excluded.jwks_uri,authorization_endpoint=excluded.authorization_endpoint,token_endpoint=excluded.token_endpoint,algorithms=excluded.algorithms,token_auth_method=excluded.token_auth_method,response_iss=excluded.response_iss,interface_version=excluded.interface_version,linked_at=MAX(oa_identity_providers.linked_at+1,excluded.linked_at),pending_issuer=NULL,pending_client_id=NULL,pending_preset=NULL,pending_at=NULL,jwks=NULL,jwks_fetched_at=NULL`)
|
|
13
|
+
.bind(metadata.issuer,clientId,metadata.jwks,metadata.authorization,metadata.token,JSON.stringify(metadata.algorithms),metadata.responseIssuer?1:0,metadata.interfaceVersion,now);
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const encode = bytes => btoa(String.fromCharCode(...bytes)).replaceAll('+','-').replaceAll('/','_').replace(/=+$/,'');
|
|
2
|
+
const json = value => encode(new TextEncoder().encode(JSON.stringify(value)));
|
|
3
|
+
export async function keyPair() {
|
|
4
|
+
const pair = await crypto.subtle.generateKey({name:'ECDSA',namedCurve:'P-256'},true,['sign','verify']);
|
|
5
|
+
return {...await crypto.subtle.exportKey('jwk',pair.privateKey),kid:crypto.randomUUID(),alg:'ES256',use:'sig'};
|
|
6
|
+
}
|
|
7
|
+
export function publicKey(key) { return {kty:key.kty,crv:key.crv,x:key.x,y:key.y,kid:key.kid,alg:'ES256',use:'sig',key_ops:['verify']}; }
|
|
8
|
+
export async function assertion(key, claims) {
|
|
9
|
+
const encoded = `${json({alg:'ES256',typ:'JWT',kid:key.kid})}.${json(claims)}`;
|
|
10
|
+
const imported = await crypto.subtle.importKey('jwk',{...key,key_ops:['sign']},{name:'ECDSA',namedCurve:'P-256'},false,['sign']);
|
|
11
|
+
return `${encoded}.${encode(new Uint8Array(await crypto.subtle.sign({name:'ECDSA',hash:'SHA-256'},imported,new TextEncoder().encode(encoded))))}`;
|
|
12
|
+
}
|
|
13
|
+
export async function digest(value) { return encode(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(value)))); }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Database, OperatorAuth } from '../operator-auth/index.mjs';
|
|
2
|
+
export function createDependencies<R extends string>(options: { db: Database; origin: string; name: string; provider?: string; local?: boolean; auth: OperatorAuth<R>; claimOwner?:(token:string)=>Promise<Response|null>; clock?: () => number; fetch?: typeof fetch; provides?:string[]; interfaces?: {interface:string;optional?:boolean;provider?:string;paths:string[]}[] }): {document():Promise<unknown>;handle(request:Request):Promise<Response|null>};
|
|
3
|
+
export const INTERFACE:string;
|
|
4
|
+
export const CALLBACK:string;
|
|
5
|
+
export const PATHS:string[];
|
|
6
|
+
export interface SqlFence {sql: string; args: unknown[]}
|
|
7
|
+
/** Snapshot captured before provider I/O; equality is repeated by shared code. */
|
|
8
|
+
export interface DependencyApproval {
|
|
9
|
+
interface: string; issuer: string; callbacks: string; attempt_id: string;
|
|
10
|
+
approved_by: string; approved_session: string; approved_version: number; expires_at: number;
|
|
11
|
+
}
|
|
12
|
+
export interface NativeDependencyAuthority<R extends string = string> {
|
|
13
|
+
unclaimed(): Promise<boolean>;
|
|
14
|
+
ownerFence(actor: import('../operator-auth/index.mjs').Session<R>, now: number): SqlFence | Promise<SqlFence>;
|
|
15
|
+
/** Repeat native current-owner/session/version/proof and provider policy/pin authority. Shared code fences the exact pending attempt and actor snapshot. */
|
|
16
|
+
approvalFence(input: {pending: DependencyApproval; contract: string; issuer: string; callbacks: string; now: number}): SqlFence | Promise<SqlFence>;
|
|
17
|
+
/** Ordinary owner approval must never enable identity from a stale first-setup flag. */
|
|
18
|
+
linkedStatements(input: {pending: DependencyApproval; contract: string; old: Record<string, unknown> | null; issuer: string; now: number}): import('../operator-auth/index.mjs').Statement[] | Promise<import('../operator-auth/index.mjs').Statement[]>;
|
|
19
|
+
unlinkStatements(input: {contract: string; actor: import('../operator-auth/index.mjs').Session<R>; now: number}): import('../operator-auth/index.mjs').Statement[] | Promise<import('../operator-auth/index.mjs').Statement[]>;
|
|
20
|
+
/** Atomic replay claim + pinned link/current binding checks + native session revocation/fence bump. */
|
|
21
|
+
consumeLogout(input: {claims: Record<string, unknown>; link: Record<string, unknown>; now: number; fence: boolean}): Promise<boolean>;
|
|
22
|
+
claimOwner?(input: {request: Request; token: string; expectedPin: {id: string; issuer: string; clientId: string; linkedAt: number}}): Promise<Response | null>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { nativeDependencyAuthority } from './native.mjs';
|
|
2
|
+
import { approveSetup, approveOwner, approvalFence, callbacksFor } from './approval.mjs';
|
|
3
|
+
import { dependencyStore } from './store.mjs';
|
|
4
|
+
import { publicKey, assertion, digest } from './crypto.mjs';
|
|
5
|
+
import { CALLBACK, INTERFACE, PATHS, NETWORK, insist, originOf, providerMetadata } from './protocol.mjs';
|
|
6
|
+
import { readJson } from '../operator-auth/identity/oidc/network.mjs';
|
|
7
|
+
import { readInput } from '../operator-auth/input.mjs';
|
|
8
|
+
import { ownerFence, pinStatement } from './authority.mjs';
|
|
9
|
+
export { INTERFACE, CALLBACK, PATHS } from './protocol.mjs';
|
|
10
|
+
export function createDependencies({db,origin,name,provider,interfaces=[{interface:INTERFACE,provider,paths:PATHS}],provides=[],local=false,auth,claimOwner,clock=Date.now,fetch:fetcher=globalThis.fetch}) {
|
|
11
|
+
originOf(origin,local);insist(Array.isArray(provides) && new Set(provides).size===provides.length && provides.every(value=>/^[a-z][a-z0-9-]*@[1-9][0-9]*$/.test(value)));
|
|
12
|
+
insist(Array.isArray(interfaces) && new Set(interfaces.map(d=>d.interface)).size===interfaces.length && interfaces.every(d=>/^[a-z][a-z0-9-]*@[1-9][0-9]*$/.test(d.interface) && Array.isArray(d.paths) && new Set(d.paths).size===d.paths.length && d.paths.every(p=>typeof p==='string' && /^\/[A-Za-z0-9_~/-]+$/.test(p) && !p.startsWith('//'))));
|
|
13
|
+
const store=dependencyStore(db);
|
|
14
|
+
const native=auth?.native?nativeDependencyAuthority(auth.accounts.dependencies):null;
|
|
15
|
+
async function owner(request,action) {
|
|
16
|
+
const actor=await auth.requireStepUp(request,action), current=await auth.accounts.owner();
|
|
17
|
+
insist(actor.operator.id===current?.id,'owner_required',403);return actor;
|
|
18
|
+
}
|
|
19
|
+
async function document() {
|
|
20
|
+
const installation=await store.installation(interfaces[0]?.interface??INTERFACE);
|
|
21
|
+
const linked=await Promise.all(interfaces.map(d=>store.link(d.interface)));
|
|
22
|
+
return {name,version:1,origin,sector_id:installation.sector_id,dependencies:interfaces.map((d,i)=>({provider:linked[i]?.issuer??d.provider??null,interface:d.interface,optional:d.optional??true,cardinality:1,paths:d.paths.map(p=>origin+p),jwks_uri:origin+'/.well-known/rer-link/jwks/'+d.interface})),provides,
|
|
23
|
+
claim:interfaces.some(d=>d.interface===INTERFACE) && (native?await native.unclaimed():!await db.prepare('SELECT 1 FROM oa_operators LIMIT 1').first()),callback:origin+CALLBACK,jwks_uri:origin+'/.well-known/rer-link/jwks'};
|
|
24
|
+
}
|
|
25
|
+
async function callback(request) {
|
|
26
|
+
const data=await readInput(request);
|
|
27
|
+
insist(Object.keys(data).every(k=>['issuer','code','interface'].includes(k)) && typeof data.code==='string' && data.code.length>0 && data.code.length<=4096);
|
|
28
|
+
const contract=data.interface??INTERFACE,declared=interfaces.find(d=>d.interface===contract);insist(declared,'undeclared_interface',403);
|
|
29
|
+
const old=await store.link(contract),pending=await store.first('SELECT * FROM rer_dependency_pending WHERE interface=?',contract);
|
|
30
|
+
const callbacks=callbacksFor(origin,declared);
|
|
31
|
+
insist(pending?.attempt_id && pending.expires_at>clock() && pending.callbacks===callbacks,'local_owner_proof_required',403);
|
|
32
|
+
const expected=pending.issuer;
|
|
33
|
+
insist(data.issuer===expected,'untrusted_link_issuer',403);
|
|
34
|
+
const pendingPin=native?null:await store.first('SELECT provider_id AS id,issuer,client_id AS clientId,linked_at AS linkedAt FROM oa_provider_session_pins WHERE session_id=?',pending.approved_session);
|
|
35
|
+
const initialApproval=await approvalFence(pending,contract,expected,callbacks,clock(),pendingPin,native);
|
|
36
|
+
insist(await store.first(`SELECT 1 WHERE ${initialApproval.sql}`,...initialApproval.args),'local_owner_proof_required',403);
|
|
37
|
+
const metadata=await providerMetadata(expected,fetcher,contract),installation=await store.installation(contract);
|
|
38
|
+
const codeDigest=await digest(expected+'\0'+data.code);
|
|
39
|
+
const consumed=await store.run('INSERT INTO rer_dependency_codes(digest,created_at) VALUES(?,?) ON CONFLICT DO NOTHING',codeDigest,clock());
|
|
40
|
+
insist(consumed.meta.changes===1,'link_code_used',409);
|
|
41
|
+
const now=Math.floor(clock()/1000), proof=await assertion(installation.key,{iss:origin,sub:origin,aud:metadata.link.redeem,iat:now,exp:now+60,jti:crypto.randomUUID(),code:data.code,interface:contract,sector_id:installation.sector_id,callback:origin+CALLBACK});
|
|
42
|
+
const result=await readJson(fetcher,metadata.link.redeem,NETWORK,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:data.code,interface:contract,origin,callback:origin+CALLBACK,client_assertion_type:'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',client_assertion:proof})});
|
|
43
|
+
insist(result.issuer===expected && result.interface===contract && result.sector_id===installation.sector_id && result.origin===origin && result.callback===origin+CALLBACK && typeof result.client_id==='string' && result.client_id.length>0 && result.client_id.length<=512,'invalid_link_grant',403);
|
|
44
|
+
if(old && old.issuer===expected) insist(result.client_id===old.client_id && ['previous-key','provider-owner'].includes(result.continuity),'link_continuity_required',403);
|
|
45
|
+
// A provider move never edits identity bindings. Only a new, proven-email sign-in
|
|
46
|
+
// can establish a binding at the new issuer; existing principals remain immutable.
|
|
47
|
+
const unchanged=old?{sql:'EXISTS(SELECT 1 FROM rer_dependency_link WHERE interface=? AND issuer=? AND client_id=? AND metadata=? AND needs_relink=? AND linked_at=?)',args:[contract,old.issuer,old.client_id,JSON.stringify(old.metadata),old.needs_relink,old.linked_at]}:{sql:'NOT EXISTS(SELECT 1 FROM rer_dependency_link WHERE interface=?)',args:[contract]};
|
|
48
|
+
const approval=await approvalFence(pending,contract,expected,callbacks,clock(),pendingPin,native);
|
|
49
|
+
await db.batch([
|
|
50
|
+
db.prepare(`UPDATE rer_dependency_codes SET created_at=CASE WHEN EXISTS(SELECT 1 FROM rer_dependency_keys WHERE interface=? AND json_extract(private_key,'$.kid')=?) AND ${unchanged.sql} AND ${approval.sql} THEN created_at ELSE -1 END WHERE digest=?`).bind(contract,installation.key.kid,...unchanged.args,...approval.args,codeDigest),
|
|
51
|
+
db.prepare(`INSERT INTO rer_dependency_link(interface,issuer,client_id,metadata,needs_relink,linked_at) VALUES(?,?,?,?,0,?) ON CONFLICT(interface) DO UPDATE SET issuer=excluded.issuer,client_id=excluded.client_id,metadata=excluded.metadata,needs_relink=0,linked_at=MAX(rer_dependency_link.linked_at+1,excluded.linked_at)`).bind(contract,expected,result.client_id,JSON.stringify(metadata),clock()),
|
|
52
|
+
...(native?await native.linkedStatements({pending,contract,old,issuer:expected,now:clock()}):[
|
|
53
|
+
...(contract===INTERFACE?[db.prepare("INSERT INTO oa_auth_audit(id,actor_id,action,value,created_at) SELECT ?,claim_id,'idpSignIn','true',? FROM oa_bootstrap WHERE EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND first_setup=1 AND idp_sign_in=0) AND EXISTS(SELECT 1 FROM oa_setup_links WHERE interface='oidc-provider@1' AND attempt_id=? AND completed=0 AND owner_id=claim_id)").bind(crypto.randomUUID(),clock(),pending.attempt_id)]:[]),
|
|
54
|
+
...(contract===INTERFACE?[db.prepare("UPDATE oa_auth_settings SET idp_sign_in=CASE WHEN first_setup=1 AND EXISTS(SELECT 1 FROM oa_setup_links WHERE interface='oidc-provider@1' AND attempt_id=? AND completed=0 AND owner_id=(SELECT claim_id FROM oa_bootstrap)) THEN 1 ELSE idp_sign_in END,first_setup=0 WHERE singleton=1").bind(pending.attempt_id)]:[]),
|
|
55
|
+
db.prepare('UPDATE oa_setup_links SET completed=1 WHERE interface=? AND attempt_id=?').bind(contract,pending.attempt_id),
|
|
56
|
+
]),
|
|
57
|
+
db.prepare('DELETE FROM rer_dependency_pending WHERE interface=?').bind(contract),
|
|
58
|
+
...(contract===INTERFACE?[pinStatement(db,result.client_id,metadata,clock())]:[]),
|
|
59
|
+
...(!native && contract===INTERFACE && old?[db.prepare("UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE id IN (SELECT p.session_id FROM oa_provider_sessions p LEFT JOIN oa_provider_session_pins pin ON pin.session_id=p.session_id WHERE pin.provider_id='auth' OR (p.issuer=? AND (? OR pin.session_id IS NULL)))").bind(clock(),old.issuer,old.issuer!==expected?1:0)]:[]),
|
|
60
|
+
...(!native && contract===INTERFACE && old && old.issuer!==expected?[db.prepare("DELETE FROM oa_domain_rules WHERE provider_id='auth'")]:[]),
|
|
61
|
+
]);
|
|
62
|
+
if(result.install_owner!==undefined) {
|
|
63
|
+
insist(contract===INTERFACE && typeof result.install_owner==='string' && typeof claimOwner==='function','owner_claim_unavailable',403);
|
|
64
|
+
const claimed=await claimOwner(result.install_owner);
|
|
65
|
+
if(claimed?.status!==303){const detail=await claimed?.json().catch(()=>null);insist(false,typeof detail?.error==='string' && /^[a-z_]+$/.test(detail.error)?detail.error:'owner_claim_refused',claimed?.status??403);}
|
|
66
|
+
}
|
|
67
|
+
return Response.json({linked:true,interface:contract});
|
|
68
|
+
}
|
|
69
|
+
async function unlink(request) {
|
|
70
|
+
const actor=await owner(request,'providers:unlink'),input=await readInput(request),contract=input.interface??INTERFACE,link=await store.link(contract);
|
|
71
|
+
insist(interfaces.some(d=>d.interface===contract));
|
|
72
|
+
const key=await store.first('SELECT private_key FROM rer_dependency_keys WHERE interface=?',contract),fence=native?await native.ownerFence(actor,clock()):ownerFence(actor,clock()),guard=crypto.randomUUID();
|
|
73
|
+
await db.batch([
|
|
74
|
+
db.prepare(`INSERT INTO rer_dependency_codes(digest,created_at) SELECT ?,CASE WHEN ${fence.sql} THEN ? ELSE -1 END`).bind(guard,...fence.args,clock()),
|
|
75
|
+
db.prepare('DELETE FROM rer_dependency_keys WHERE interface=?').bind(contract),
|
|
76
|
+
...(native?await native.unlinkStatements({contract,actor,now:clock()}):[
|
|
77
|
+
...(contract===INTERFACE?[db.prepare("UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE id IN (SELECT p.session_id FROM oa_provider_sessions p LEFT JOIN oa_provider_session_pins pin ON pin.session_id=p.session_id WHERE pin.provider_id='auth' OR p.issuer=(SELECT issuer FROM rer_dependency_link WHERE interface=?))").bind(clock(),contract)]:[]),
|
|
78
|
+
...(contract===INTERFACE?[db.prepare("DELETE FROM oa_domain_rules WHERE provider_id='auth'")]:[]),
|
|
79
|
+
]),
|
|
80
|
+
...(contract===INTERFACE?[db.prepare("UPDATE oa_identity_providers SET status='unlinked' WHERE provider_id='auth'")]:[]),
|
|
81
|
+
db.prepare('DELETE FROM rer_dependency_pending WHERE interface=?').bind(contract),
|
|
82
|
+
db.prepare('DELETE FROM rer_dependency_link WHERE interface=?').bind(contract),
|
|
83
|
+
...(!native?[db.prepare('DELETE FROM oa_setup_links WHERE interface=?').bind(contract),
|
|
84
|
+
db.prepare("INSERT INTO oa_auth_audit VALUES(?,?,'provider:unlink',?,?)").bind(crypto.randomUUID(),actor.operator.id,contract,clock())]:[]),
|
|
85
|
+
]);
|
|
86
|
+
// Local removal is already complete. The provider may be offline.
|
|
87
|
+
if(link && key) {try {await readJson(fetcher,link.metadata.link.deregister,NETWORK,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({client_id:link.client_id,origin,interface:contract,client_assertion_type:'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',client_assertion:await assertion(JSON.parse(key.private_key),{iss:link.client_id,sub:link.client_id,aud:link.metadata.link.deregister,iat:Math.floor(clock()/1000),exp:Math.floor(clock()/1000)+60,jti:crypto.randomUUID()})})});}catch{}}
|
|
88
|
+
return Response.json({unlinked:true});
|
|
89
|
+
}
|
|
90
|
+
return {document,async handle(request) {
|
|
91
|
+
const u=new URL(request.url),path=u.pathname;
|
|
92
|
+
const jwksContract=path.startsWith('/.well-known/rer-link/jwks/')?path.slice('/.well-known/rer-link/jwks/'.length):null;
|
|
93
|
+
if(!jwksContract && !['/.well-known/rer-dependencies','/.well-known/rer-link/jwks',CALLBACK,'/auth/dependencies/relink','/auth/dependencies/setup','/auth/dependencies/unlink','/auth/dependencies/status'].includes(path))return null;
|
|
94
|
+
try {
|
|
95
|
+
insist(u.origin===origin && (!request.headers.has('host') || request.headers.get('host')===u.host) && !u.search && !u.hash,'invalid_link_origin',403);
|
|
96
|
+
if(path==='/.well-known/rer-dependencies' && request.method==='GET')return Response.json(await document());
|
|
97
|
+
if((path==='/.well-known/rer-link/jwks' || jwksContract) && request.method==='GET') {const contract=jwksContract??interfaces[0]?.interface??INTERFACE;insist(interfaces.some(d=>d.interface===contract));return Response.json({keys:[publicKey((await store.installation(contract)).key)]});}
|
|
98
|
+
if(path==='/auth/dependencies/status' && request.method==='GET') {const link=await store.link(),links=(await db.prepare('SELECT interface,issuer,needs_relink FROM rer_dependency_link').all()).results;return Response.json({linked:!!link && !link.needs_relink,needsRelink:!!link?.needs_relink,links:links.map(r=>({interface:r.interface,issuer:r.issuer,linked:!r.needs_relink}))});}
|
|
99
|
+
insist(request.method==='POST','method_not_allowed',405);
|
|
100
|
+
if(path===CALLBACK)return await callback(request);
|
|
101
|
+
insist(request.headers.get('origin')===origin,'invalid_link_origin',403);
|
|
102
|
+
if(path==='/auth/dependencies/unlink')return await unlink(request);
|
|
103
|
+
if(path==='/auth/dependencies/relink' || path==='/auth/dependencies/setup') {
|
|
104
|
+
const input=await readInput(request),contract=input.interface??INTERFACE;originOf(input.issuer);const declared=interfaces.find(d=>d.interface===contract);insist(declared);
|
|
105
|
+
const callbacks=callbacksFor(origin,declared);
|
|
106
|
+
if(path==='/auth/dependencies/setup') await approveSetup({db,auth,request,input,contract,callbacks,clock});
|
|
107
|
+
else await approveOwner({store,actor:await owner(request,'providers:relink'),input,contract,callbacks,clock,native});
|
|
108
|
+
return Response.json({pending:true});
|
|
109
|
+
}
|
|
110
|
+
insist(false,'not_found',404);
|
|
111
|
+
}catch(e){return Response.json({error:e.code??'link_unavailable'},{status:e.status??503});}
|
|
112
|
+
}};
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dependencyStore } from './store.mjs';
|
|
2
|
+
import { parseIdToken, verifySignature } from '../operator-auth/identity/oidc/jwt.mjs';
|
|
3
|
+
import { readJson } from '../operator-auth/identity/oidc/network.mjs';
|
|
4
|
+
import { readInput } from '../operator-auth/input.mjs';
|
|
5
|
+
import { insist, NETWORK } from './protocol.mjs';
|
|
6
|
+
export const LOGOUT_EVENT='http://schemas.openid.net/event/backchannel-logout';
|
|
7
|
+
export const FENCE_EVENT='https://runeditrun.com/events/identity-fence';
|
|
8
|
+
export async function backchannelLogout({db,config,request,clock=Date.now,fetch:fetcher=globalThis.fetch,native}) {
|
|
9
|
+
try {
|
|
10
|
+
const url=new URL(request.url);
|
|
11
|
+
insist(url.origin===config.origin && url.pathname==='/auth/oidc/auth/logout' && !url.search && request.method==='POST');
|
|
12
|
+
const data=await readInput(request);insist(typeof data.logout_token==='string');
|
|
13
|
+
const link=await dependencyStore(db).link();insist(link && !link.needs_relink,'provider_unlinked',403);
|
|
14
|
+
const pin={issuer:link.issuer,clientId:link.client_id,algorithms:link.metadata.algorithms};
|
|
15
|
+
const parsed=parseIdToken(data.logout_token,pin, 'logout+jwt'),c=parsed.claims;
|
|
16
|
+
await verifySignature(parsed,await readJson(fetcher,link.metadata.jwks,NETWORK));
|
|
17
|
+
const now=clock()/1000;
|
|
18
|
+
insist(c.iss===pin.issuer && (c.aud===pin.clientId || Array.isArray(c.aud) && c.aud.length===1 && c.aud[0]===pin.clientId));
|
|
19
|
+
insist(Number.isSafeInteger(c.iat) && c.iat<=now+30 && c.iat>=now-86400 && Number.isSafeInteger(c.exp) && c.exp>now && c.exp>c.iat && c.exp-c.iat<=86400);
|
|
20
|
+
insist(c.nbf===undefined || Number.isSafeInteger(c.nbf) && c.nbf<=now+30);
|
|
21
|
+
insist(!Object.hasOwn(c,'nonce') && typeof c.jti==='string' && c.jti.trim().length>0 && c.jti.length<=256);
|
|
22
|
+
insist(c.events && typeof c.events==='object' && !Array.isArray(c.events));
|
|
23
|
+
const events=Object.keys(c.events);insist(events.length>0 && events.every(k=>[LOGOUT_EVENT,FENCE_EVENT].includes(k) && c.events[k] && typeof c.events[k]==='object' && !Array.isArray(c.events[k]) && Object.keys(c.events[k]).length===0));
|
|
24
|
+
for(const key of ['sub','sid']) if(Object.hasOwn(c,key)) insist(typeof c[key]==='string' && c[key].length>0 && c[key].length<=255);
|
|
25
|
+
insist(c.sub || c.sid);
|
|
26
|
+
if(native) {
|
|
27
|
+
const consumed=await native.consumeLogout({claims:c,link,now:clock(),fence:events.includes(FENCE_EVENT)});
|
|
28
|
+
insist(consumed,'logout_already_used',409);
|
|
29
|
+
return new Response(null,{status:204});
|
|
30
|
+
}
|
|
31
|
+
const rows=(await db.prepare('SELECT DISTINCT subject FROM oa_provider_sessions WHERE issuer=? AND (? IS NULL OR sid=?)').bind(c.iss,c.sid??null,c.sid??null).all()).results;
|
|
32
|
+
if(c.sid) insist(rows.length>0 && (!c.sub || rows.every(r=>r.subject===c.sub)));
|
|
33
|
+
const subject=c.sub??(rows.length===1?rows[0].subject:null);insist(subject);
|
|
34
|
+
const person=await db.prepare('SELECT operator_id FROM oa_external_identities WHERE issuer=? AND subject=?').bind(c.iss,subject).first();insist(person);
|
|
35
|
+
const statements=[db.prepare(`INSERT INTO oa_logout_events(issuer,jti,consumed_at) VALUES(?,?,CASE WHEN EXISTS(SELECT 1 FROM rer_dependency_link WHERE interface='oidc-provider@1' AND issuer=? AND client_id=? AND metadata=? AND needs_relink=0 AND linked_at=?) AND EXISTS(SELECT 1 FROM oa_external_identities WHERE issuer=? AND subject=? AND operator_id=?) THEN ? ELSE -1 END)`).bind(c.iss,c.jti,link.issuer,link.client_id,JSON.stringify(link.metadata),link.linked_at,c.iss,subject,person.operator_id,clock())];
|
|
36
|
+
if(events.includes(FENCE_EVENT)) statements.push(db.prepare('UPDATE oa_operators SET version=version+1 WHERE id=?').bind(person.operator_id));
|
|
37
|
+
statements.push(db.prepare(`UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE id IN
|
|
38
|
+
(SELECT session_id FROM oa_provider_sessions WHERE issuer=? AND subject=? AND (? IS NULL OR sid=?))`).bind(clock(),c.iss,subject,c.sid??null,c.sid??null));
|
|
39
|
+
try { await db.batch(statements); } catch { insist(false,'logout_already_used',409); }
|
|
40
|
+
return new Response(null,{status:204});
|
|
41
|
+
} catch(error) { return Response.json({error:error.code??'invalid_logout'},{status:error.status??400}); }
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AuthError } from '../operator-auth/errors.mjs';
|
|
2
|
+
// These are D1 transaction fragments, not a second linking protocol. Each
|
|
3
|
+
// fragment uses only the adopter's tenant/account/session tables. The common
|
|
4
|
+
// protocol adds key/link/code/pin guards and commits everything in one batch.
|
|
5
|
+
export function nativeDependencyAuthority(value) {
|
|
6
|
+
const methods = ['unclaimed', 'ownerFence', 'approvalFence', 'linkedStatements', 'unlinkStatements', 'consumeLogout'];
|
|
7
|
+
if (!value || typeof value !== 'object' || methods.some(k => typeof value[k] !== 'function') || value.claimOwner !== undefined && typeof value.claimOwner !== 'function') throw new AuthError('native_dependency_capability_required', 503);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readJson, discover } from '../operator-auth/identity/oidc/network.mjs';
|
|
2
|
+
import { PRESETS, MINIMUM_PROVIDER_VERSION } from '../operator-auth/identity/oidc/presets.mjs';
|
|
3
|
+
export const INTERFACE = 'oidc-provider@1';
|
|
4
|
+
export const CALLBACK = '/.well-known/rer-link/callback';
|
|
5
|
+
export const PATHS = ['/auth/oidc/auth/callback','/auth/oidc/auth/logout'];
|
|
6
|
+
export const NETWORK = { timeoutMs:5000 };
|
|
7
|
+
export function insist(value, code = 'invalid_link', status = 400) { if (!value) throw Object.assign(new Error(code),{code,status}); }
|
|
8
|
+
export function originOf(value, local = false) {
|
|
9
|
+
let u;try { u=new URL(value); } catch { insist(false); }
|
|
10
|
+
insist(u.origin===value && !u.username && !u.password && (u.protocol==='https:' || local && u.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(u.hostname)));
|
|
11
|
+
return u.origin;
|
|
12
|
+
}
|
|
13
|
+
export async function providerMetadata(issuer, fetcher, contract = INTERFACE) {
|
|
14
|
+
originOf(issuer);
|
|
15
|
+
const p={...PRESETS.auth,id:'auth',issuer,endpointOrigins:[issuer],tokenAuthMethod:'private_key_jwt',keyAlgorithm:'ES256',minimumVersion:MINIMUM_PROVIDER_VERSION};
|
|
16
|
+
const oidc=contract===INTERFACE?await discover(fetcher,p,NETWORK):{};
|
|
17
|
+
const link=await readJson(fetcher,`${issuer}/.well-known/rer-provider`,NETWORK);
|
|
18
|
+
insist(link.issuer===issuer && (link.interface===contract || Array.isArray(link.provides) && new Set(link.provides).size===link.provides.length && link.provides.every(i=>typeof i==='string' && /^[a-z][a-z0-9-]*@[1-9][0-9]*$/.test(i)) && link.provides.includes(contract)) && link.version===1,'unsupported_link_interface',502);
|
|
19
|
+
for (const name of ['initiate','redeem','deregister']) {
|
|
20
|
+
const u=new URL(link[name]);insist(u.origin===issuer && u.protocol==='https:' && !u.search && !u.hash && !u.username && !u.password,'untrusted_link_endpoint',502);
|
|
21
|
+
}
|
|
22
|
+
return {...oidc,link};
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS rer_dependency_installation (
|
|
2
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton=1), sector_id TEXT NOT NULL UNIQUE
|
|
3
|
+
);
|
|
4
|
+
CREATE TABLE IF NOT EXISTS rer_dependency_link (
|
|
5
|
+
interface TEXT PRIMARY KEY, issuer TEXT NOT NULL, client_id TEXT NOT NULL, metadata TEXT NOT NULL,
|
|
6
|
+
needs_relink INTEGER NOT NULL DEFAULT 0, linked_at INTEGER NOT NULL
|
|
7
|
+
);
|
|
8
|
+
CREATE TABLE IF NOT EXISTS rer_dependency_pending (
|
|
9
|
+
interface TEXT PRIMARY KEY, issuer TEXT NOT NULL, approved_by TEXT NOT NULL, expires_at INTEGER NOT NULL, approved_session TEXT NOT NULL, approved_version INTEGER NOT NULL, callbacks TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS rer_dependency_codes (
|
|
12
|
+
digest TEXT PRIMARY KEY, created_at INTEGER NOT NULL CHECK(created_at>=0)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS rer_dependency_keys (interface TEXT PRIMARY KEY, private_key TEXT NOT NULL);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { keyPair } from './crypto.mjs';
|
|
2
|
+
export function dependencyStore(db) {
|
|
3
|
+
const first=(sql,...args)=>db.prepare(sql).bind(...args).first();
|
|
4
|
+
const run=(sql,...args)=>db.prepare(sql).bind(...args).run();
|
|
5
|
+
return {
|
|
6
|
+
first,run,
|
|
7
|
+
async installation(contract = 'oidc-provider@1') {
|
|
8
|
+
await run('INSERT INTO rer_dependency_installation(singleton,sector_id) VALUES(1,?) ON CONFLICT DO NOTHING',crypto.randomUUID());
|
|
9
|
+
const row=await first('SELECT * FROM rer_dependency_installation WHERE singleton=1');
|
|
10
|
+
let key=await first('SELECT private_key FROM rer_dependency_keys WHERE interface=?',contract);
|
|
11
|
+
if(!key) {
|
|
12
|
+
await run('INSERT INTO rer_dependency_keys(interface,private_key) VALUES(?,?) ON CONFLICT DO NOTHING',contract,JSON.stringify(await keyPair()));
|
|
13
|
+
key=await first('SELECT private_key FROM rer_dependency_keys WHERE interface=?',contract);
|
|
14
|
+
}
|
|
15
|
+
return {...row,key:JSON.parse(key.private_key)};
|
|
16
|
+
},
|
|
17
|
+
async link(contract = 'oidc-provider@1') { const row=await first("SELECT * FROM rer_dependency_link WHERE interface=?",contract);return row?{...row,metadata:JSON.parse(row.metadata)}:null; },
|
|
18
|
+
};
|
|
19
|
+
}
|