@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,287 @@
|
|
|
1
|
+
// Server-only entry. Never include this module or its configuration in a browser bundle.
|
|
2
|
+
// OpenID Connect sign-in for several pinned providers at once. Each provider
|
|
3
|
+
// has its own start and callback route; its output is a verified principal,
|
|
4
|
+
// which the accounts layer admits through the admission it is given.
|
|
5
|
+
import { safeReturnTo } from '../../policy.mjs';
|
|
6
|
+
import { securityHeaders, requireLoopback, localCookieName } from '../../http.mjs';
|
|
7
|
+
import { configure } from './config.mjs';
|
|
8
|
+
import { check, OidcError } from './errors.mjs';
|
|
9
|
+
import { random, isRandom, digest, keyedHash, seal, unseal } from './crypto.mjs';
|
|
10
|
+
import { storage } from './store.mjs';
|
|
11
|
+
import { discover, exchange, readJson } from './network.mjs';
|
|
12
|
+
import { parseIdToken, knowsKey, verifyIdToken, principalOf } from './jwt.mjs';
|
|
13
|
+
import { PRESETS, MINIMUM_PROVIDER_VERSION } from './presets.mjs';
|
|
14
|
+
import { refuseInProduction } from '../principal.mjs';
|
|
15
|
+
export { OidcError } from './errors.mjs';
|
|
16
|
+
export { PRESETS, MINIMUM_PROVIDER_VERSION } from './presets.mjs';
|
|
17
|
+
|
|
18
|
+
export const HIDE_AFTER_MS = 5 * 60000;
|
|
19
|
+
const PROBE_EVERY_MS = 60000;
|
|
20
|
+
|
|
21
|
+
function cookieName(config, state) { return config.local ? `${localCookieName('oa_oidc_local', config.origin)}_${state}` : `__Host-oa_oidc_${state}`; }
|
|
22
|
+
function cookie(config, state, value, age = config.transactionSeconds) {
|
|
23
|
+
return `${cookieName(config, state)}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${age}${config.secure ? '; Secure' : ''}`;
|
|
24
|
+
}
|
|
25
|
+
function bindingCookie(request, config, state) {
|
|
26
|
+
const header = request.headers.get('cookie') ?? '';
|
|
27
|
+
check(header.length <= 16384, 'oidc_invalid_correlation');
|
|
28
|
+
const prefix = `${cookieName(config, state)}=`;
|
|
29
|
+
const matches = header.split(';').map(s => s.trim()).filter(s => s.startsWith(prefix));
|
|
30
|
+
check(matches.length === 1 && isRandom(matches[0].slice(prefix.length)), 'oidc_invalid_correlation');
|
|
31
|
+
return matches[0].slice(prefix.length);
|
|
32
|
+
}
|
|
33
|
+
function redirect(location, cookies) {
|
|
34
|
+
const headers = new Headers({ ...securityHeaders(), Location: location });
|
|
35
|
+
for (const value of cookies) headers.append('Set-Cookie', value);
|
|
36
|
+
return new Response(null, { status: 303, headers });
|
|
37
|
+
}
|
|
38
|
+
// A browser navigation or form post gets the sign-in page back with a notice
|
|
39
|
+
// and the email link; API callers get the JSON error code.
|
|
40
|
+
function errorResponse(error, config, request, clearCookie) {
|
|
41
|
+
const known = error instanceof OidcError;
|
|
42
|
+
const headers = new Headers(securityHeaders());
|
|
43
|
+
if (clearCookie) headers.append('Set-Cookie', clearCookie);
|
|
44
|
+
const code = known ? error.code : 'oidc_unavailable';
|
|
45
|
+
if (request && /\btext\/html\b/.test(request.headers.get('accept') ?? '')) {
|
|
46
|
+
headers.set('Location', `${config.signInPath}?notice=${encodeURIComponent(code)}`);
|
|
47
|
+
return new Response(null, { status: 303, headers });
|
|
48
|
+
}
|
|
49
|
+
return Response.json({ error: code }, { status: known ? error.status : 503, headers });
|
|
50
|
+
}
|
|
51
|
+
function boundary(request, config, post) {
|
|
52
|
+
const url = new URL(request.url); const host = request.headers.get('host');
|
|
53
|
+
check(url.origin === config.origin && (!host || host.toLowerCase() === url.host.toLowerCase()), 'oidc_invalid_origin', 403);
|
|
54
|
+
if (config.local) { try { requireLoopback(request); } catch { throw new OidcError('oidc_invalid_origin', 403); } }
|
|
55
|
+
if (post) {
|
|
56
|
+
check(request.method === 'POST', 'oidc_method_not_allowed', 405);
|
|
57
|
+
check(request.headers.get('origin') === config.origin && request.headers.get('sec-fetch-site') !== 'cross-site', 'oidc_invalid_origin', 403);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Whom the app talks to: the pinned link, never a configuration changed since.
|
|
61
|
+
// While a re-link is pending, the pinned issuer keeps its own preset rules and
|
|
62
|
+
// none of the new configuration's subject associations.
|
|
63
|
+
function pinnedView(provider, link) {
|
|
64
|
+
if (link.issuer === provider.issuer && link.clientId === provider.clientId && link.preset === provider.preset) return provider;
|
|
65
|
+
const preset = PRESETS[link.preset];
|
|
66
|
+
return { ...provider, preset: link.preset, issuer: link.issuer, clientId: link.clientId, tokenAuthMethod: link.tokenAuthMethod, algorithms: link.algorithms,
|
|
67
|
+
endpointPins: link.preset === 'legacy-rs256' ? { authorization: link.authorization, token: link.token, jwks: link.jwks } : undefined, email: preset.email, interface: preset.interface, minimumVersion: preset.interface ? provider.minimumVersion ?? MINIMUM_PROVIDER_VERSION : undefined, scope: preset.scope, subjectEmails: {},
|
|
68
|
+
endpointOrigins: [...new Set([link.authorization, link.token, link.jwks].map(u => new URL(u).origin))] };
|
|
69
|
+
}
|
|
70
|
+
const samePin = (link, metadata) => link.issuer === metadata.issuer && link.jwks === metadata.jwks && link.authorization === metadata.authorization
|
|
71
|
+
&& link.token === metadata.token && link.algorithms.every(a => metadata.algorithms.includes(a)) && (!link.responseIssuer || metadata.responseIssuer);
|
|
72
|
+
|
|
73
|
+
export function createOidcProviders({ admission, db, config: input, clientIp, fetch: fetcher = globalThis.fetch, clock = Date.now, waitUntil, enabled = async () => Boolean(await admission?.accounts?.identityEnabled?.()), claim, claimScope = async () => false, policyVersion = async () => 0 }) {
|
|
74
|
+
const config = configure(input);
|
|
75
|
+
check(admission && typeof admission.admit === 'function' && db && typeof db.prepare === 'function' && typeof clientIp === 'function' && typeof fetcher === 'function' && typeof clock === 'function', 'oidc_invalid_configuration', 503);
|
|
76
|
+
const store = storage(db);
|
|
77
|
+
const providers = new Map(config.providers.map(p => [p.id, p]));
|
|
78
|
+
// Every network call records the provider's reachability.
|
|
79
|
+
async function observe(provider, operation) {
|
|
80
|
+
try { const value = await operation(); await store.healthy(provider.id, clock()); return value; }
|
|
81
|
+
catch (error) { if (error instanceof OidcError && error.status === 502) await store.failing(provider.id, error.hard === true, clock()); throw error; }
|
|
82
|
+
}
|
|
83
|
+
// The pinned link, made at the provider's first use. Afterwards a changed
|
|
84
|
+
// configuration only records a pending re-link; the pin is never swapped.
|
|
85
|
+
async function linked(provider) {
|
|
86
|
+
const link = await store.link(provider.id);
|
|
87
|
+
if (!link) {
|
|
88
|
+
const metadata = await observe(provider, () => discover(fetcher, provider, config));
|
|
89
|
+
return { link: await store.pin(provider.id, provider, metadata, clock()), metadata };
|
|
90
|
+
}
|
|
91
|
+
if (link.issuer !== provider.issuer || link.clientId !== provider.clientId || link.preset !== provider.preset) await store.pending(provider.id, provider, clock());
|
|
92
|
+
else if (link.pendingAt) await store.clearPending(provider.id);
|
|
93
|
+
return { link };
|
|
94
|
+
}
|
|
95
|
+
// A pinned provider whose discovery no longer matches its pin cannot be used
|
|
96
|
+
// until the owner re-links it.
|
|
97
|
+
async function current(provider) {
|
|
98
|
+
const { link, metadata: fresh } = await linked(provider);
|
|
99
|
+
check(link.status === 'linked', 'oidc_provider_unlinked', 403);
|
|
100
|
+
const view = pinnedView(provider, link);
|
|
101
|
+
if (!fresh) {
|
|
102
|
+
const metadata = await observe(provider, () => discover(fetcher, view, config));
|
|
103
|
+
if (!samePin(link, metadata)) { await store.failing(provider.id, true, clock()); throw new OidcError('oidc_relink_required', 502); }
|
|
104
|
+
}
|
|
105
|
+
return { link, view };
|
|
106
|
+
}
|
|
107
|
+
// Changes to a provider's pin, callback or policy invalidate its outstanding logins.
|
|
108
|
+
const fingerprint = (provider, link) => keyedHash(config.transactionSecret, 'configuration', JSON.stringify({
|
|
109
|
+
configured: providers.get(provider.id), id: provider.id, linkedAt: link.linkedAt, issuer: link.issuer, clientId: link.clientId, jwks: link.jwks, token: link.token, authorization: link.authorization, algorithms: link.algorithms,
|
|
110
|
+
redirectUri: provider.redirectUri, method: provider.tokenAuthMethod, subjectEmails: provider.subjectEmails, audiences: provider.additionalAudiences, email: provider.email,
|
|
111
|
+
transactionSeconds: config.transactionSeconds, maxTokenAgeSeconds: config.maxTokenAgeSeconds, clockSkewSeconds: config.clockSkewSeconds }));
|
|
112
|
+
// State is keyed to its provider: a response for one provider presented at
|
|
113
|
+
// another provider's redirect URI finds no transaction.
|
|
114
|
+
const stateHash = (provider, state) => keyedHash(config.transactionSecret, 'state', `${provider.id}\0${state}`);
|
|
115
|
+
async function keysFor(provider, link, kid) {
|
|
116
|
+
if (knowsKey(link.keySet, kid)) return link.keySet;
|
|
117
|
+
// One refetch for a key this app has not seen; a key still unknown is refused.
|
|
118
|
+
const fetched = await observe(provider, () => readJson(fetcher, link.jwks, config, {}, 262144));
|
|
119
|
+
await store.keys(link, fetched, clock());
|
|
120
|
+
check(knowsKey(fetched, kid), 'oidc_unknown_key');
|
|
121
|
+
return fetched;
|
|
122
|
+
}
|
|
123
|
+
async function probe(provider) {
|
|
124
|
+
try {
|
|
125
|
+
const { link, metadata } = await linked(provider);
|
|
126
|
+
if (link.status !== 'linked') return;
|
|
127
|
+
if (!metadata) {
|
|
128
|
+
const fresh = await observe(provider, () => discover(fetcher, pinnedView(provider, link), config));
|
|
129
|
+
if (!samePin(link, fresh)) { await store.failing(provider.id, true, clock()); return; }
|
|
130
|
+
}
|
|
131
|
+
const keys = await observe(provider, () => readJson(fetcher, link.jwks, config, {}, 262144));
|
|
132
|
+
await store.keys(link, keys, clock());
|
|
133
|
+
} catch { /* recorded by observe */ }
|
|
134
|
+
}
|
|
135
|
+
async function begin(request, id, { returnTo } = {}) {
|
|
136
|
+
const provider = providers.get(id);
|
|
137
|
+
try {
|
|
138
|
+
check(await enabled(), 'oidc_sign_in_disabled', 403);
|
|
139
|
+
boundary(request, config, true);
|
|
140
|
+
check(provider && new URL(request.url).pathname === provider.startPath && !new URL(request.url).search, 'oidc_invalid_route', 404);
|
|
141
|
+
const ip = await clientIp(request);
|
|
142
|
+
check(typeof ip === 'string' && ip.length > 0 && ip.length <= 256, 'oidc_client_address_unavailable', 503);
|
|
143
|
+
const now = clock();
|
|
144
|
+
const window = Math.floor(now / 900000);
|
|
145
|
+
const bucket = await keyedHash(config.transactionSecret, 'start-limit', `${provider.id}\0${provider.issuer}\0${provider.clientId}\0${ip}\0${window}`);
|
|
146
|
+
await store.limit(bucket, (window + 1) * 900000, config.startLimit);
|
|
147
|
+
const { link, view } = await current(provider);
|
|
148
|
+
const state = random(); const binding = random(); const verifier = random(); const nonce = random();
|
|
149
|
+
const hashed = await stateHash(provider, state);
|
|
150
|
+
const expiresAt = clock() + config.transactionSeconds * 1000;
|
|
151
|
+
const payload = await seal(config.transactionSecret, hashed, { nonce, verifier, returnTo: safeReturnTo(returnTo), expiresAt, provider: provider.id, policyVersion: await policyVersion() });
|
|
152
|
+
await store.put({ stateHash: hashed, bindingHash: await keyedHash(config.transactionSecret, 'binding', binding), configHash: await fingerprint(view, link), payload, expiresAt });
|
|
153
|
+
const authorization = new URL(link.authorization);
|
|
154
|
+
const params = { response_type: 'code', response_mode: 'query', client_id: link.clientId, redirect_uri: provider.redirectUri,
|
|
155
|
+
scope: view.scope + (await claimScope() ? ' claim' : ''), state, nonce, code_challenge: await digest(verifier), code_challenge_method: 'S256' };
|
|
156
|
+
for (const [key, value] of Object.entries(params)) authorization.searchParams.set(key, value);
|
|
157
|
+
return redirect(authorization.href, [cookie(config, state, binding)]);
|
|
158
|
+
} catch (e) { return errorResponse(e, config, request); }
|
|
159
|
+
}
|
|
160
|
+
async function callback(request, id) {
|
|
161
|
+
const provider = providers.get(id);
|
|
162
|
+
let clearCookie;
|
|
163
|
+
try {
|
|
164
|
+
check(await enabled(), 'oidc_sign_in_disabled', 403);
|
|
165
|
+
boundary(request, config, false);
|
|
166
|
+
check(request.method === 'GET', 'oidc_method_not_allowed', 405);
|
|
167
|
+
check(request.url.length <= 12288, 'oidc_invalid_response');
|
|
168
|
+
const url = new URL(request.url);
|
|
169
|
+
check(provider && url.origin + url.pathname === provider.redirectUri && !url.hash, 'oidc_invalid_route', 404);
|
|
170
|
+
for (const name of url.searchParams.keys()) check(url.searchParams.getAll(name).length === 1);
|
|
171
|
+
const state = url.searchParams.get('state'); check(isRandom(state), 'oidc_invalid_correlation');
|
|
172
|
+
const binding = bindingCookie(request, config, state);
|
|
173
|
+
const link = await store.link(provider.id);
|
|
174
|
+
check(link && link.status === 'linked', 'oidc_invalid_correlation');
|
|
175
|
+
const view = pinnedView(provider, link);
|
|
176
|
+
const hashed = await stateHash(provider, state);
|
|
177
|
+
const row = await store.consume(hashed, await keyedHash(config.transactionSecret, 'binding', binding), await fingerprint(view, link), clock());
|
|
178
|
+
check(row, 'oidc_invalid_correlation');
|
|
179
|
+
clearCookie = cookie(config, state, '', 0);
|
|
180
|
+
let transaction;
|
|
181
|
+
try { transaction = await unseal(config.transactionSecret, hashed, row.payload); }
|
|
182
|
+
catch { throw new OidcError('oidc_storage_unavailable', 503); }
|
|
183
|
+
check(transaction.provider === provider.id, 'oidc_invalid_correlation');
|
|
184
|
+
// RFC 9207: where the provider sends iss with its response, it must be the pinned issuer.
|
|
185
|
+
const issuer = url.searchParams.get('iss');
|
|
186
|
+
check(issuer === null ? !link.responseIssuer : issuer === link.issuer, 'oidc_issuer_mismatch');
|
|
187
|
+
if (url.searchParams.has('error')) { check(!url.searchParams.has('code')); throw new OidcError('oidc_provider_denied', 401); }
|
|
188
|
+
const code = url.searchParams.get('code');
|
|
189
|
+
check(typeof code === 'string' && code.length <= 4096 && /^[\x21-\x7e]+$/.test(code));
|
|
190
|
+
const tokens = await exchange(fetcher, link, view, config, code, transaction.verifier, clock());
|
|
191
|
+
let claims;
|
|
192
|
+
try {
|
|
193
|
+
const parsed = parseIdToken(tokens.id_token, link);
|
|
194
|
+
const keys = await keysFor(provider, link, parsed.header.kid);
|
|
195
|
+
claims = await verifyIdToken(parsed, keys, link, view, config, transaction, { ...tokens, code }, clock());
|
|
196
|
+
} catch (error) { if (error instanceof OidcError && error.status !== 400) throw error; throw new OidcError(error instanceof OidcError ? error.code : 'oidc_invalid_response'); }
|
|
197
|
+
check(clock() < transaction.expiresAt, 'oidc_invalid_correlation');
|
|
198
|
+
check(transaction.policyVersion === await policyVersion() && await enabled(), 'oidc_sign_in_disabled', 403);
|
|
199
|
+
const providerPin={id:link.id,issuer:link.issuer,clientId:link.clientId,linkedAt:link.linkedAt};
|
|
200
|
+
if (Object.hasOwn(claims,'install_owner')) {
|
|
201
|
+
check(typeof claim === 'function','oidc_claim_refused',403);
|
|
202
|
+
const claimed=await claim(request,tokens.id_token,providerPin);check(claimed,'oidc_claim_refused',403);return claimed;
|
|
203
|
+
}
|
|
204
|
+
let result;
|
|
205
|
+
try {
|
|
206
|
+
const principal = {...principalOf(claims, view),providerGeneration:transaction.policyVersion,providerPin};
|
|
207
|
+
// A production app refuses dev identities and addresses only local or test systems hold.
|
|
208
|
+
refuseInProduction(principal, config);
|
|
209
|
+
result = await admission.admit(request, principal, { returnTo: transaction.returnTo });
|
|
210
|
+
}
|
|
211
|
+
catch (error) { throw new OidcError(error?.code === 'provider_required' ? 'oidc_provider_required' : 'oidc_admission_denied', 403); }
|
|
212
|
+
return redirect(result.returnTo, [clearCookie, result.cookie]);
|
|
213
|
+
} catch (e) { return errorResponse(e, config, request, clearCookie); }
|
|
214
|
+
}
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
ids: Object.freeze([...providers.keys()]),
|
|
217
|
+
begin, callback,
|
|
218
|
+
callbackPaths: Object.freeze(Object.fromEntries([...providers.values()].map(p => [new URL(p.redirectUri).pathname, p.id]))),
|
|
219
|
+
/** The sign-in page's list: a provider is hidden while unlinked or unreachable. */
|
|
220
|
+
async list() {
|
|
221
|
+
if (!await enabled()) return [];
|
|
222
|
+
const now = clock();
|
|
223
|
+
const [links, health] = await Promise.all([store.links(), store.health()]);
|
|
224
|
+
const result = [];
|
|
225
|
+
const stale = [];
|
|
226
|
+
for (const provider of providers.values()) {
|
|
227
|
+
const link = links.find(l => l.id === provider.id);
|
|
228
|
+
const h = health.get(provider.id);
|
|
229
|
+
const unreachable = !!h && (h.hardFailureAt !== null || h.failingSince !== null && now - h.failingSince >= HIDE_AFTER_MS);
|
|
230
|
+
if (unreachable && now - h.checkedAt >= PROBE_EVERY_MS) stale.push(provider);
|
|
231
|
+
// Where the start route sends the browser: a form posting there needs it in form-action.
|
|
232
|
+
const authorizationOrigins = link ? [new URL(link.authorization).origin] : [...provider.endpointOrigins];
|
|
233
|
+
result.push({ id: provider.id, label: provider.label, preset: provider.preset, startPath: provider.startPath, authorizationOrigins,
|
|
234
|
+
visible: link?.status !== 'unlinked' && !unreachable, linked: link?.status === 'linked', pendingRelink: !!link?.pendingAt });
|
|
235
|
+
}
|
|
236
|
+
// A hidden provider is checked again at most once a minute, after the page answers.
|
|
237
|
+
if (stale.length) { const probes = Promise.all(stale.map(probe)); if (typeof waitUntil === 'function') waitUntil(probes); else await probes; }
|
|
238
|
+
return result;
|
|
239
|
+
},
|
|
240
|
+
/** Owner actions; the caller requires a fresh step-up proof first. */
|
|
241
|
+
async relink(id) {
|
|
242
|
+
const provider = providers.get(id);
|
|
243
|
+
check(provider, 'oidc_unknown_provider', 404);
|
|
244
|
+
const metadata = await observe(provider, () => discover(fetcher, provider, config));
|
|
245
|
+
if (await store.link(id)) await store.relink(id, provider, metadata, clock());
|
|
246
|
+
else await store.pin(id, provider, metadata, clock());
|
|
247
|
+
return store.link(id);
|
|
248
|
+
},
|
|
249
|
+
async unlink(id) {
|
|
250
|
+
check(providers.has(id), 'oidc_unknown_provider', 404);
|
|
251
|
+
// Only a pin can be ended; a provider never linked has nothing to unlink.
|
|
252
|
+
check(await store.link(id), 'oidc_not_linked', 409);
|
|
253
|
+
await store.unlink(id);
|
|
254
|
+
},
|
|
255
|
+
async links() {
|
|
256
|
+
return (await store.links()).map(({ keySet: _, keysFetchedAt: __, ...link }) => link);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Exact routes only: /auth/oidc/<id>/start and /auth/oidc/<id>/callback.
|
|
262
|
+
export function createOidcHandler(oidc) {
|
|
263
|
+
return async request => {
|
|
264
|
+
const path = new URL(request.url).pathname;
|
|
265
|
+
const callbackId = oidc.callbackPaths?.[path];
|
|
266
|
+
if (callbackId) return oidc.callback(request, callbackId);
|
|
267
|
+
const match = /^\/auth\/oidc\/([a-z][a-z0-9-]{0,31})\/(start|callback|logout)$/.exec(path);
|
|
268
|
+
if (!match || !oidc.ids.includes(match[1])) return null;
|
|
269
|
+
if (match[2] === 'logout') return oidc.logout?.(request) ?? new Response(null,{status:404});
|
|
270
|
+
if (match[2] === 'callback') return oidc.callbackPaths ? null : oidc.callback(request, match[1]);
|
|
271
|
+
try {
|
|
272
|
+
check(request.method === 'POST', 'oidc_method_not_allowed', 405);
|
|
273
|
+
const type = (request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
|
274
|
+
check(type === 'application/x-www-form-urlencoded' || type === 'application/json', 'oidc_unsupported_media_type', 415);
|
|
275
|
+
// Read a bounded stream, never unbounded request.json()/formData().
|
|
276
|
+
const reader = request.body?.getReader(); let bytes = 0; const chunks = [];
|
|
277
|
+
if (reader) { try { while (true) { const { done, value } = await reader.read(); if (done) break; bytes += value.length; check(bytes <= 4096, 'oidc_input_too_large', 413); chunks.push(value); } } finally { void reader.cancel().catch(() => {}); } }
|
|
278
|
+
const all = new Uint8Array(bytes); let offset = 0; for (const chunk of chunks) { all.set(chunk, offset); offset += chunk.length; }
|
|
279
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(all);
|
|
280
|
+
let input;
|
|
281
|
+
if (type === 'application/json') input = JSON.parse(text);
|
|
282
|
+
else { const form = new URLSearchParams(text); check(form.getAll('returnTo').length <= 1); input = { returnTo: form.get('returnTo') ?? undefined }; }
|
|
283
|
+
check(input && typeof input === 'object' && !Array.isArray(input));
|
|
284
|
+
return oidc.begin(request, match[1], { returnTo: input.returnTo });
|
|
285
|
+
} catch (e) { return Response.json({ error: e instanceof OidcError ? e.code : 'oidc_invalid_input' }, { status: e instanceof OidcError ? e.status : 400, headers: securityHeaders() }); }
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { check } from './errors.mjs';
|
|
2
|
+
import { decode, encode, utf8 } from './crypto.mjs';
|
|
3
|
+
|
|
4
|
+
function object(part) {
|
|
5
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(decode(part)));
|
|
6
|
+
check(value && typeof value === 'object' && !Array.isArray(value)); return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Header checks come first, before any key is looked up: the algorithm must be
|
|
10
|
+
// one the link pinned (so never `none`, HS* or anything unlisted).
|
|
11
|
+
export function parseIdToken(token, link, purpose = 'JWT') {
|
|
12
|
+
check(typeof token === 'string' && token.length < 32768);
|
|
13
|
+
const parts = token.split('.'); check(parts.length === 3);
|
|
14
|
+
const [headerPart, bodyPart, signaturePart] = parts;
|
|
15
|
+
const header = object(headerPart); const claims = object(bodyPart);
|
|
16
|
+
check(typeof header.alg === 'string' && link.algorithms.includes(header.alg) && (header.typ === undefined || header.typ === purpose), 'oidc_algorithm_refused');
|
|
17
|
+
check(typeof header.kid === 'string' && header.kid.length > 0 && header.kid.length <= 256);
|
|
18
|
+
// Neither remote header keys nor critical/alternate payload semantics are used.
|
|
19
|
+
check(!['crit', 'jku', 'jwk', 'x5u', 'b64', 'zip'].some(k => Object.hasOwn(header, k)));
|
|
20
|
+
return { header, claims, input: `${headerPart}.${bodyPart}`, signature: signaturePart };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function knowsKey(jwks, kid) {
|
|
24
|
+
return Array.isArray(jwks?.keys) && jwks.keys.some(k => k && typeof k === 'object' && k.kid === kid);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function verifyIdToken(parsed, jwks, link, provider, settings, transaction, tokens, now) {
|
|
28
|
+
check(!Object.hasOwn(parsed.claims, 'events'), 'oidc_invalid_id_token');
|
|
29
|
+
const { header, claims } = parsed;
|
|
30
|
+
await verifySignature(parsed, jwks);
|
|
31
|
+
// The token's issuer is the pinned issuer, and its audience is this client.
|
|
32
|
+
check(claims.iss === link.issuer && typeof claims.sub === 'string' && claims.sub.length > 0 && claims.sub.length <= 255, 'oidc_issuer_mismatch');
|
|
33
|
+
const audiences = typeof claims.aud === 'string' ? [claims.aud] : claims.aud;
|
|
34
|
+
check(Array.isArray(audiences) && audiences.length > 0 && audiences.every(a => typeof a === 'string' && a.length > 0) && new Set(audiences).size === audiences.length && audiences.includes(link.clientId), 'oidc_audience_mismatch');
|
|
35
|
+
check(audiences.every(a => a === link.clientId || provider.additionalAudiences.includes(a)), 'oidc_audience_mismatch');
|
|
36
|
+
check(audiences.length === 1 || claims.azp === link.clientId, 'oidc_audience_mismatch');
|
|
37
|
+
check(claims.azp === undefined || claims.azp === link.clientId, 'oidc_audience_mismatch');
|
|
38
|
+
const time = now / 1000; const skew = settings.clockSkewSeconds;
|
|
39
|
+
check(Number.isSafeInteger(claims.exp) && Number.isSafeInteger(claims.iat) && claims.exp > claims.iat && claims.exp > time - skew && claims.iat <= time + skew && claims.iat >= time - settings.maxTokenAgeSeconds - skew);
|
|
40
|
+
check(claims.nbf === undefined || Number.isSafeInteger(claims.nbf) && claims.nbf <= time + skew);
|
|
41
|
+
check(claims.nonce === transaction.nonce);
|
|
42
|
+
for (const [claim, value] of [['at_hash', tokens.access_token], ['c_hash', tokens.code]]) {
|
|
43
|
+
if (claims[claim] !== undefined) {
|
|
44
|
+
check(typeof value === 'string');
|
|
45
|
+
const hash = await crypto.subtle.digest('SHA-256', utf8.encode(value));
|
|
46
|
+
check(claims[claim] === encode(new Uint8Array(hash).slice(0, 16)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return claims;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// What a verified token says about the person, under the provider's email
|
|
53
|
+
// trust. A configured subject association replaces the email claim; it is
|
|
54
|
+
// never turned into a fictional verified email.
|
|
55
|
+
export function principalOf(claims, provider) {
|
|
56
|
+
const base = { emailVerified:claims.email_verified===true, datedEmailRequired: provider.email === 'verified-at', ...(Object.hasOwn(claims,'email_verified_at') ? {emailVerifiedAt:typeof claims.email_verified_at==='number'?claims.email_verified_at*1000:null} : {}), sid: typeof claims.sid === 'string' && claims.sid.length > 0 ? claims.sid : undefined, issuer: provider.issuer, subject: claims.sub, provider: provider.id, ...(Array.isArray(claims.amr) ? { amr: claims.amr.filter(v => typeof v === 'string') } : {}) };
|
|
57
|
+
if (Object.hasOwn(provider.subjectEmails, claims.sub)) return { ...base, associatedEmail: provider.subjectEmails[claims.sub] };
|
|
58
|
+
const email = typeof claims.email === 'string' ? claims.email : undefined;
|
|
59
|
+
if (!email) return base;
|
|
60
|
+
const domain = email.slice(email.lastIndexOf('@') + 1).toLowerCase();
|
|
61
|
+
if (provider.email === 'verified-at') {
|
|
62
|
+
const verified = claims.email_verified === true && Number.isSafeInteger(claims.email_verified_at);
|
|
63
|
+
return { ...base, email, emailVerified: verified, ...(verified ? { emailVerifiedAt: claims.email_verified_at * 1000 } : {}) };
|
|
64
|
+
}
|
|
65
|
+
if (provider.email === 'legacy-verified') {
|
|
66
|
+
const dated = Object.hasOwn(claims, 'email_verified_at');
|
|
67
|
+
const validDate = Number.isSafeInteger(claims.email_verified_at) && Number.isSafeInteger(claims.email_verified_at * 1000);
|
|
68
|
+
return { ...base, email, emailVerified: claims.email_verified === true && (!dated || validDate) };
|
|
69
|
+
}
|
|
70
|
+
if (provider.email === 'google') {
|
|
71
|
+
const dated = Object.hasOwn(claims, 'email_verified_at');
|
|
72
|
+
const validDate = Number.isSafeInteger(claims.email_verified_at) && Number.isSafeInteger(claims.email_verified_at * 1000);
|
|
73
|
+
const trusted = claims.email_verified === true && (!dated || validDate) && (domain === 'gmail.com' || typeof claims.hd === 'string' && claims.hd.toLowerCase() === domain);
|
|
74
|
+
return { ...base, email, emailVerified: trusted, ...(dated && validDate ? { emailVerifiedAt: claims.email_verified_at * 1000 } : {}) };
|
|
75
|
+
}
|
|
76
|
+
return { ...base, email, emailVerified: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function verifySignature(parsed, jwks) {
|
|
80
|
+
const { header } = parsed;
|
|
81
|
+
check(Array.isArray(jwks.keys) && jwks.keys.length > 0 && jwks.keys.length <= 100);
|
|
82
|
+
const candidates = jwks.keys.filter(k => k && typeof k === 'object' && k.kid === header.kid);
|
|
83
|
+
check(candidates.length === 1);
|
|
84
|
+
const key = candidates[0];
|
|
85
|
+
check((key.alg === undefined || key.alg === header.alg) && (key.use === undefined || key.use === 'sig') &&
|
|
86
|
+
(key.key_ops === undefined || Array.isArray(key.key_ops) && key.key_ops.length === 1 && key.key_ops[0] === 'verify'));
|
|
87
|
+
check(!['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'].some(k => Object.hasOwn(key, k)));
|
|
88
|
+
let algorithm; let publicKey;
|
|
89
|
+
if (header.alg === 'RS256') {
|
|
90
|
+
check(key.kty === 'RSA'); const n = decode(key.n); const e = decode(key.e);
|
|
91
|
+
check(n.length >= 256 && n.length <= 1024 && n[0] >= 128 && e.length > 0 && e.length <= 4 && e[0] !== 0);
|
|
92
|
+
const exponent = e.reduce((v, b) => v * 256 + b, 0); check(exponent >= 3 && exponent % 2 === 1);
|
|
93
|
+
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
|
|
94
|
+
publicKey = { kty: 'RSA', n: key.n, e: key.e };
|
|
95
|
+
} else {
|
|
96
|
+
check(header.alg === 'ES256' && key.kty === 'EC' && key.crv === 'P-256' && decode(key.x).length === 32 && decode(key.y).length === 32 && decode(parsed.signature).length === 64);
|
|
97
|
+
algorithm = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' };
|
|
98
|
+
publicKey = { kty: 'EC', crv: 'P-256', x: key.x, y: key.y };
|
|
99
|
+
}
|
|
100
|
+
const imported = await crypto.subtle.importKey('jwk', publicKey, algorithm, false, ['verify']);
|
|
101
|
+
check(await crypto.subtle.verify(algorithm, imported, decode(parsed.signature), utf8.encode(parsed.input)));
|
|
102
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { check, fail, OidcError } from './errors.mjs';
|
|
2
|
+
import { endpoint } from './config.mjs';
|
|
3
|
+
import { compareVersions, validVersion } from './presets.mjs';
|
|
4
|
+
import { clientAssertion } from './client-auth.mjs';
|
|
5
|
+
|
|
6
|
+
// A provider failure that hides it at once: the address no longer exists (404)
|
|
7
|
+
// or its name does not resolve (a resolver error, or the 530 a Cloudflare
|
|
8
|
+
// Worker receives for an origin DNS failure).
|
|
9
|
+
export class ProviderUnreachable extends OidcError { constructor() { super('oidc_provider_unavailable', 502); this.hard = true; } }
|
|
10
|
+
const dnsFailure = error => /\b(?:dns|getaddrinfo|enotfound|eai_again|name not resolved|could not resolve)\b/i.test(String(error?.message ?? error));
|
|
11
|
+
|
|
12
|
+
// The deadline includes stream consumption; redirects never forward credentials.
|
|
13
|
+
export async function readJson(fetcher, uri, settings, init = {}, maxBytes = 65536) {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
let timer, hard = false;
|
|
16
|
+
const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('deadline')); }, settings.timeoutMs); });
|
|
17
|
+
let reader;
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([deadline, (async () => {
|
|
20
|
+
let response;
|
|
21
|
+
try { response = await fetcher(uri, { ...init, redirect: 'manual', signal: controller.signal, headers: { Accept: 'application/json', ...init.headers } }); }
|
|
22
|
+
catch (error) { hard = dnsFailure(error); throw error; }
|
|
23
|
+
check(response.status < 300 || response.status >= 400, 'oidc_provider_redirect', 502);
|
|
24
|
+
if (response.status === 404 || response.status === 530) hard = true;
|
|
25
|
+
check(response.status === 200 && !response.redirected && (!response.url || response.url === uri), 'oidc_provider_unavailable', 502);
|
|
26
|
+
check(/^application\/(?:[a-z0-9.+-]+\+)?json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? ''), 'oidc_provider_unavailable', 502);
|
|
27
|
+
const length = response.headers.get('content-length');
|
|
28
|
+
check(!length || /^\d+$/.test(length) && Number(length) <= maxBytes, 'oidc_provider_unavailable', 502);
|
|
29
|
+
check(response.body, 'oidc_provider_unavailable', 502);
|
|
30
|
+
reader = response.body.getReader();
|
|
31
|
+
let size = 0; const chunks = [];
|
|
32
|
+
while (true) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; check(size <= maxBytes, 'oidc_provider_unavailable', 502); chunks.push(value); }
|
|
33
|
+
const bytes = new Uint8Array(size); let offset = 0;
|
|
34
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
35
|
+
const body = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
36
|
+
check(body && typeof body === 'object' && !Array.isArray(body), 'oidc_provider_unavailable', 502);
|
|
37
|
+
return body;
|
|
38
|
+
})()]);
|
|
39
|
+
} catch { if (hard) throw new ProviderUnreachable(); fail('oidc_provider_unavailable', 502); }
|
|
40
|
+
finally { clearTimeout(timer); controller.abort(); if (reader) void reader.cancel().catch(() => {}); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Discovery for one provider. The result is what linking pins.
|
|
44
|
+
export async function discover(fetcher, provider, settings) {
|
|
45
|
+
const d = await readJson(fetcher, `${provider.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`, settings);
|
|
46
|
+
check(d.issuer === provider.issuer, 'oidc_issuer_mismatch', 502);
|
|
47
|
+
check(Array.isArray(d.response_types_supported) && d.response_types_supported.includes('code'), 'oidc_provider_unsupported', 502);
|
|
48
|
+
check(d.response_modes_supported === undefined || Array.isArray(d.response_modes_supported) && d.response_modes_supported.includes('query'), 'oidc_provider_unsupported', 502);
|
|
49
|
+
check(d.grant_types_supported === undefined || Array.isArray(d.grant_types_supported) && d.grant_types_supported.includes('authorization_code'), 'oidc_provider_unsupported', 502);
|
|
50
|
+
// Discovery may omit PKCE metadata (Google does). Always send S256 anyway.
|
|
51
|
+
check(d.code_challenge_methods_supported === undefined || Array.isArray(d.code_challenge_methods_supported) && d.code_challenge_methods_supported.includes('S256'), 'oidc_provider_unsupported', 502);
|
|
52
|
+
check(Array.isArray(d.id_token_signing_alg_values_supported), 'oidc_provider_unsupported', 502);
|
|
53
|
+
const algorithms = provider.algorithms.filter(a => d.id_token_signing_alg_values_supported.includes(a));
|
|
54
|
+
check(algorithms.length > 0, 'oidc_provider_unsupported', 502);
|
|
55
|
+
const methods = d.token_endpoint_auth_methods_supported ?? ['client_secret_basic'];
|
|
56
|
+
check(Array.isArray(methods) && methods.includes(provider.tokenAuthMethod), 'oidc_provider_unsupported', 502);
|
|
57
|
+
if (provider.tokenAuthMethod === 'private_key_jwt') {
|
|
58
|
+
const signing = d.token_endpoint_auth_signing_alg_values_supported;
|
|
59
|
+
check(signing === undefined || Array.isArray(signing) && signing.includes(provider.keyAlgorithm), 'oidc_provider_unsupported', 502);
|
|
60
|
+
}
|
|
61
|
+
check(d.authorization_response_iss_parameter_supported === undefined || typeof d.authorization_response_iss_parameter_supported === 'boolean', 'oidc_provider_unsupported', 502);
|
|
62
|
+
let interfaceVersion = null;
|
|
63
|
+
if (provider.interface) {
|
|
64
|
+
// An RER provider states the interface versions it serves; one below the
|
|
65
|
+
// minimum this app needs is refused.
|
|
66
|
+
const served = Array.isArray(d.rer_interfaces) ? d.rer_interfaces.filter(v => typeof v === 'string' && v.startsWith(`${provider.interface}@`)).map(v => v.slice(provider.interface.length + 1)).filter(validVersion) : [];
|
|
67
|
+
interfaceVersion = served.sort(compareVersions).at(-1) ?? null;
|
|
68
|
+
check(interfaceVersion && compareVersions(interfaceVersion, provider.minimumVersion) >= 0, 'oidc_provider_version_unsupported', 502);
|
|
69
|
+
}
|
|
70
|
+
if (provider.endpointPins) check(d.authorization_endpoint === provider.endpointPins.authorization && d.token_endpoint === provider.endpointPins.token && d.jwks_uri === provider.endpointPins.jwks, 'oidc_relink_required', 502);
|
|
71
|
+
return { issuer: d.issuer, authorization: endpoint(d.authorization_endpoint, provider), token: endpoint(d.token_endpoint, provider), jwks: endpoint(d.jwks_uri, provider),
|
|
72
|
+
algorithms, responseIssuer: d.authorization_response_iss_parameter_supported === true, interfaceVersion };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function exchange(fetcher, link, provider, settings, code, verifier, now) {
|
|
76
|
+
const form = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: provider.redirectUri, client_id: link.clientId, code_verifier: verifier });
|
|
77
|
+
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
|
78
|
+
if (provider.tokenAuthMethod === 'client_secret_post') form.set('client_secret', provider.clientSecret);
|
|
79
|
+
if (provider.tokenAuthMethod === 'client_secret_basic') {
|
|
80
|
+
const formEncode = s => new URLSearchParams({ v: s }).toString().slice(2);
|
|
81
|
+
headers.Authorization = `Basic ${btoa(`${formEncode(link.clientId)}:${formEncode(provider.clientSecret)}`)}`;
|
|
82
|
+
}
|
|
83
|
+
if (provider.tokenAuthMethod === 'private_key_jwt') {
|
|
84
|
+
// No shared secret exists: the app proves itself with a short assertion
|
|
85
|
+
// signed by its own private key, addressed to the pinned token endpoint.
|
|
86
|
+
form.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
87
|
+
form.set('client_assertion', await clientAssertion(provider, link, now));
|
|
88
|
+
}
|
|
89
|
+
const response = await readJson(fetcher, link.token, settings, { method: 'POST', headers, body: form.toString() });
|
|
90
|
+
check(!Object.hasOwn(response, 'error') && typeof response.id_token === 'string' && (response.access_token === undefined || typeof response.access_token === 'string' && /^[\x21-\x7e]+$/.test(response.access_token))
|
|
91
|
+
&& (response.token_type === undefined && response.access_token === undefined || typeof response.token_type === 'string' && response.token_type.toLowerCase() === 'bearer'));
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The providers an app can offer. Each preset fixes the signing algorithms its
|
|
2
|
+
// ID tokens may use (never `none` or HS*) and how far its email claim is
|
|
3
|
+
// trusted. GitHub is not an OpenID provider and is not offered.
|
|
4
|
+
//
|
|
5
|
+
// email:
|
|
6
|
+
// 'verified-at' email_verified with email_verified_at in the last 30 days (Auth)
|
|
7
|
+
// 'google' email_verified, and only for @gmail.com or tokens carrying hd
|
|
8
|
+
// 'subject' never by email: only explicit subjectEmails associations
|
|
9
|
+
export const MINIMUM_PROVIDER_VERSION = '1.0.0';
|
|
10
|
+
export const PRESETS = Object.freeze({
|
|
11
|
+
auth: Object.freeze({ label: 'Auth', algorithms: ['ES256'], methods: ['private_key_jwt'], email: 'verified-at', interface: 'oidc-provider', scope: 'openid email profile' }),
|
|
12
|
+
google: Object.freeze({ label: 'Google', issuer: 'https://accounts.google.com', algorithms: ['RS256'], methods: ['private_key_jwt', 'client_secret_basic', 'client_secret_post'],
|
|
13
|
+
email: 'google', endpointOrigins: ['https://accounts.google.com', 'https://oauth2.googleapis.com', 'https://www.googleapis.com'], scope: 'openid email' }),
|
|
14
|
+
microsoft: Object.freeze({ label: 'Microsoft', tenantIssuer: tenant => `https://login.microsoftonline.com/${tenant}/v2.0`, algorithms: ['RS256'],
|
|
15
|
+
methods: ['private_key_jwt', 'client_secret_basic', 'client_secret_post'], email: 'subject', endpointOrigins: ['https://login.microsoftonline.com'], scope: 'openid email' }),
|
|
16
|
+
'legacy-rs256': Object.freeze({ label: 'Company', algorithms: ['RS256'], methods: ['private_key_jwt', 'client_secret_basic', 'client_secret_post'], email: 'legacy-verified', scope: 'openid email' }),
|
|
17
|
+
okta: Object.freeze({ label: 'Okta', algorithms: ['RS256'], methods: ['private_key_jwt', 'client_secret_basic', 'client_secret_post'], email: 'subject', scope: 'openid email' }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function compareVersions(a, b) {
|
|
21
|
+
const parse = v => v.split('.').map(Number);
|
|
22
|
+
const [x, y] = [parse(a), parse(b)];
|
|
23
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
export const validVersion = v => typeof v === 'string' && /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/.test(v);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Identity providers (OpenID Connect). Apply as a NEW numbered migration after
|
|
2
|
+
-- the accounts schema. Every statement is IF NOT EXISTS, so a store that applied
|
|
3
|
+
-- an earlier version of this file applies the current one again as its next
|
|
4
|
+
-- migration and gains only what it lacks.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS oa_oidc_transactions (
|
|
6
|
+
state_hash TEXT PRIMARY KEY,
|
|
7
|
+
binding_hash TEXT NOT NULL,
|
|
8
|
+
config_hash TEXT NOT NULL,
|
|
9
|
+
payload TEXT NOT NULL,
|
|
10
|
+
expires_at INTEGER NOT NULL
|
|
11
|
+
);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS oa_oidc_transactions_expiry ON oa_oidc_transactions(expires_at);
|
|
13
|
+
CREATE TABLE IF NOT EXISTS oa_oidc_limits (
|
|
14
|
+
bucket TEXT PRIMARY KEY,
|
|
15
|
+
count INTEGER NOT NULL,
|
|
16
|
+
expires_at INTEGER NOT NULL
|
|
17
|
+
);
|
|
18
|
+
-- One pinned link per configured provider. Linking stores the issuer, JWKS URI,
|
|
19
|
+
-- client id, endpoints and allowed algorithms; a changed configuration only
|
|
20
|
+
-- records a pending re-link, which the owner completes with a step-up proof.
|
|
21
|
+
CREATE TABLE IF NOT EXISTS oa_identity_providers (
|
|
22
|
+
provider_id TEXT PRIMARY KEY,
|
|
23
|
+
preset TEXT NOT NULL,
|
|
24
|
+
status TEXT NOT NULL CHECK(status IN ('linked','unlinked')),
|
|
25
|
+
issuer TEXT NOT NULL,
|
|
26
|
+
client_id TEXT NOT NULL,
|
|
27
|
+
jwks_uri TEXT NOT NULL,
|
|
28
|
+
authorization_endpoint TEXT NOT NULL,
|
|
29
|
+
token_endpoint TEXT NOT NULL,
|
|
30
|
+
algorithms TEXT NOT NULL,
|
|
31
|
+
token_auth_method TEXT NOT NULL,
|
|
32
|
+
response_iss INTEGER NOT NULL,
|
|
33
|
+
interface_version TEXT,
|
|
34
|
+
linked_at INTEGER NOT NULL,
|
|
35
|
+
pending_issuer TEXT,
|
|
36
|
+
pending_client_id TEXT,
|
|
37
|
+
pending_preset TEXT,
|
|
38
|
+
pending_at INTEGER,
|
|
39
|
+
jwks TEXT,
|
|
40
|
+
jwks_fetched_at INTEGER
|
|
41
|
+
);
|
|
42
|
+
-- Reachability per configured provider, linked or not. A provider that has
|
|
43
|
+
-- failed discovery or JWKS for 5 minutes, or answered 404 or a DNS failure, is
|
|
44
|
+
-- hidden from the sign-in page until a later check succeeds.
|
|
45
|
+
CREATE TABLE IF NOT EXISTS oa_identity_provider_health (
|
|
46
|
+
provider_id TEXT PRIMARY KEY,
|
|
47
|
+
failing_since INTEGER,
|
|
48
|
+
hard_failure_at INTEGER,
|
|
49
|
+
checked_at INTEGER NOT NULL
|
|
50
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { check, fail } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
const linkColumns = `provider_id AS id, preset, status, issuer, client_id AS clientId, jwks_uri AS jwks, authorization_endpoint AS authorization, token_endpoint AS token,
|
|
4
|
+
algorithms, token_auth_method AS tokenAuthMethod, response_iss AS responseIssuer, interface_version AS interfaceVersion, linked_at AS linkedAt,
|
|
5
|
+
pending_issuer AS pendingIssuer, pending_client_id AS pendingClientId, pending_preset AS pendingPreset, pending_at AS pendingAt,
|
|
6
|
+
jwks AS keySet, jwks_fetched_at AS keysFetchedAt`;
|
|
7
|
+
function link(row) {
|
|
8
|
+
if (!row) return null;
|
|
9
|
+
return { ...row, algorithms: JSON.parse(row.algorithms), responseIssuer: row.responseIssuer === 1, keySet: row.keySet ? JSON.parse(row.keySet) : null };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function storage(db) {
|
|
13
|
+
const guarded = async operation => { try { return await operation(); } catch { fail('oidc_storage_unavailable', 503); } };
|
|
14
|
+
return {
|
|
15
|
+
async limit(bucket, expiresAt, maximum) {
|
|
16
|
+
try {
|
|
17
|
+
const row = await db.prepare('INSERT INTO oa_oidc_limits (bucket,count,expires_at) VALUES (?,1,?) ON CONFLICT(bucket) DO UPDATE SET count=count+1 RETURNING count').bind(bucket, expiresAt).first();
|
|
18
|
+
check(row && row.count <= maximum, 'oidc_rate_limited', 429);
|
|
19
|
+
} catch (e) { if (e.code === 'oidc_rate_limited') throw e; fail('oidc_storage_unavailable', 503); }
|
|
20
|
+
},
|
|
21
|
+
put(row) {
|
|
22
|
+
return guarded(() => db.prepare('INSERT INTO oa_oidc_transactions (state_hash,binding_hash,config_hash,payload,expires_at) VALUES (?,?,?,?,?)').bind(row.stateHash, row.bindingHash, row.configHash, row.payload, row.expiresAt).run());
|
|
23
|
+
},
|
|
24
|
+
consume(stateHash, bindingHash, configHash, now) {
|
|
25
|
+
return guarded(() => db.prepare('DELETE FROM oa_oidc_transactions WHERE state_hash=? AND binding_hash=? AND config_hash=? AND expires_at>? RETURNING payload').bind(stateHash, bindingHash, configHash, now).first());
|
|
26
|
+
},
|
|
27
|
+
async links() { return guarded(async () => (await db.prepare(`SELECT ${linkColumns} FROM oa_identity_providers`).all()).results.map(link)); },
|
|
28
|
+
async link(id) { return guarded(async () => link(await db.prepare(`SELECT ${linkColumns} FROM oa_identity_providers WHERE provider_id=?`).bind(id).first())); },
|
|
29
|
+
// The first link only: an existing row, linked or unlinked, is never replaced here.
|
|
30
|
+
async pin(id, provider, metadata, now) {
|
|
31
|
+
return guarded(async () => {
|
|
32
|
+
await 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)
|
|
33
|
+
VALUES(?,?,'linked',?,?,?,?,?,?,?,?,?,?) ON CONFLICT(provider_id) DO NOTHING`).bind(id, provider.preset, metadata.issuer, provider.clientId, metadata.jwks, metadata.authorization, metadata.token,
|
|
34
|
+
JSON.stringify(metadata.algorithms), provider.tokenAuthMethod, metadata.responseIssuer ? 1 : 0, metadata.interfaceVersion, now).run();
|
|
35
|
+
return link(await db.prepare(`SELECT ${linkColumns} FROM oa_identity_providers WHERE provider_id=?`).bind(id).first());
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
// A re-link the owner approved: the configured provider replaces the pin.
|
|
39
|
+
async relink(id, provider, metadata, now) {
|
|
40
|
+
return guarded(() => db.prepare(`UPDATE oa_identity_providers SET preset=?,status='linked',issuer=?,client_id=?,jwks_uri=?,authorization_endpoint=?,token_endpoint=?,algorithms=?,token_auth_method=?,
|
|
41
|
+
response_iss=?,interface_version=?,linked_at=MAX(linked_at+1,?),pending_issuer=NULL,pending_client_id=NULL,pending_preset=NULL,pending_at=NULL,jwks=NULL,jwks_fetched_at=NULL
|
|
42
|
+
WHERE provider_id=?`).bind(provider.preset, metadata.issuer, provider.clientId, metadata.jwks, metadata.authorization, metadata.token, JSON.stringify(metadata.algorithms), provider.tokenAuthMethod,
|
|
43
|
+
metadata.responseIssuer ? 1 : 0, metadata.interfaceVersion, now, id).run());
|
|
44
|
+
},
|
|
45
|
+
// A changed configuration only records what a re-link would pin.
|
|
46
|
+
pending(id, provider, now) {
|
|
47
|
+
return guarded(() => db.prepare(`UPDATE oa_identity_providers SET pending_issuer=?,pending_client_id=?,pending_preset=?,pending_at=COALESCE(pending_at,?)
|
|
48
|
+
WHERE provider_id=? AND (pending_issuer IS NOT ? OR pending_client_id IS NOT ? OR pending_preset IS NOT ?)`).bind(provider.issuer, provider.clientId, provider.preset, now, id, provider.issuer, provider.clientId, provider.preset).run());
|
|
49
|
+
},
|
|
50
|
+
clearPending(id) { return guarded(() => db.prepare('UPDATE oa_identity_providers SET pending_issuer=NULL,pending_client_id=NULL,pending_preset=NULL,pending_at=NULL WHERE provider_id=?').bind(id).run()); },
|
|
51
|
+
unlink(id) { return guarded(() => db.prepare("UPDATE oa_identity_providers SET status='unlinked' WHERE provider_id=?").bind(id).run()); },
|
|
52
|
+
keys(pin, keySet, now) {
|
|
53
|
+
return guarded(() => db.prepare("UPDATE oa_identity_providers SET jwks=?,jwks_fetched_at=? WHERE provider_id=? AND status='linked' AND issuer=? AND client_id=? AND linked_at=?").bind(JSON.stringify(keySet), now, pin.id, pin.issuer, pin.clientId, pin.linkedAt).run());
|
|
54
|
+
},
|
|
55
|
+
async health() {
|
|
56
|
+
return guarded(async () => new Map((await db.prepare('SELECT provider_id AS id,failing_since AS failingSince,hard_failure_at AS hardFailureAt,checked_at AS checkedAt FROM oa_identity_provider_health').all()).results.map(r => [r.id, r])));
|
|
57
|
+
},
|
|
58
|
+
healthy(id, now) {
|
|
59
|
+
return guarded(() => db.prepare(`INSERT INTO oa_identity_provider_health(provider_id,failing_since,hard_failure_at,checked_at) VALUES(?,NULL,NULL,?)
|
|
60
|
+
ON CONFLICT(provider_id) DO UPDATE SET failing_since=NULL,hard_failure_at=NULL,checked_at=excluded.checked_at`).bind(id, now).run());
|
|
61
|
+
},
|
|
62
|
+
failing(id, hard, now) {
|
|
63
|
+
return guarded(() => db.prepare(`INSERT INTO oa_identity_provider_health(provider_id,failing_since,hard_failure_at,checked_at) VALUES(?,?,?,?)
|
|
64
|
+
ON CONFLICT(provider_id) DO UPDATE SET failing_since=COALESCE(failing_since,excluded.failing_since),hard_failure_at=COALESCE(excluded.hard_failure_at,hard_failure_at),checked_at=excluded.checked_at`)
|
|
65
|
+
.bind(id, now, hard ? now : null, now).run());
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|