@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,17 @@
|
|
|
1
|
+
-- Ephemeral app-local setup attempts are never exported. A native claim binds
|
|
2
|
+
-- only attempts opened before that claim to its exact owner/session authority,
|
|
3
|
+
-- in its own bootstrap transaction.
|
|
4
|
+
CREATE TABLE IF NOT EXISTS oa_setup_links (
|
|
5
|
+
interface TEXT PRIMARY KEY, attempt_id TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
6
|
+
owner_id TEXT, owner_version INTEGER, session_id TEXT,
|
|
7
|
+
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))
|
|
8
|
+
);
|
|
9
|
+
CREATE TRIGGER IF NOT EXISTS oa_native_claim_closes_provider_setup AFTER INSERT ON oa_sessions
|
|
10
|
+
WHEN NEW.kind='native' AND NOT EXISTS(SELECT 1 FROM oa_sessions WHERE operator_id=NEW.operator_id AND id!=NEW.id) AND EXISTS(SELECT 1 FROM oa_bootstrap WHERE claim_id=NEW.operator_id AND claimed_at=NEW.created_at)
|
|
11
|
+
BEGIN
|
|
12
|
+
UPDATE oa_auth_settings SET first_setup=0 WHERE singleton=1 AND NOT EXISTS(
|
|
13
|
+
SELECT 1 FROM oa_setup_links WHERE interface='oidc-provider@1' AND completed=0 AND owner_id IS NULL AND expires_at>NEW.created_at
|
|
14
|
+
);
|
|
15
|
+
UPDATE oa_setup_links SET owner_id=NEW.operator_id,owner_version=NEW.operator_version,session_id=NEW.id
|
|
16
|
+
WHERE owner_id IS NULL AND completed=0 AND expires_at>NEW.created_at;
|
|
17
|
+
END;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Local dev identity for an existing store. Apply as a NEW numbered migration
|
|
2
|
+
-- under the seed's next unused number; never rewrite an applied migration.
|
|
3
|
+
-- SQLite cannot change a CHECK in place, so oa_sessions is rebuilt with 'dev'
|
|
4
|
+
-- admitted, keeping every row, column, index and constraint. No table refers
|
|
5
|
+
-- to oa_sessions. The file is also safe on a store created from the current
|
|
6
|
+
-- schema/operator-auth.sql, so a fresh store may apply both.
|
|
7
|
+
CREATE TABLE oa_sessions_dev_rebuild (
|
|
8
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE,
|
|
9
|
+
operator_id TEXT NOT NULL REFERENCES oa_operators(id), operator_version INTEGER NOT NULL,
|
|
10
|
+
kind TEXT NOT NULL CHECK(kind IN ('native','review','oidc','password','dev')),
|
|
11
|
+
source_token_id TEXT UNIQUE REFERENCES oa_tokens(id),
|
|
12
|
+
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
|
|
13
|
+
);
|
|
14
|
+
INSERT INTO oa_sessions_dev_rebuild(id,token_hash,operator_id,operator_version,kind,source_token_id,created_at,expires_at,revoked_at)
|
|
15
|
+
SELECT id,token_hash,operator_id,operator_version,kind,source_token_id,created_at,expires_at,revoked_at FROM oa_sessions;
|
|
16
|
+
DROP TABLE oa_sessions;
|
|
17
|
+
ALTER TABLE oa_sessions_dev_rebuild RENAME TO oa_sessions;
|
|
18
|
+
CREATE INDEX oa_sessions_operator ON oa_sessions(operator_id);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS oa_dev_credentials (
|
|
20
|
+
credential TEXT NOT NULL, id TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
21
|
+
PRIMARY KEY(credential, id)
|
|
22
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Domain rules and step-up proofs for an existing store. Apply as a NEW
|
|
2
|
+
-- numbered migration under the seed's next unused number; never rewrite an
|
|
3
|
+
-- applied migration. Additive only: every existing row stays as it is. The file
|
|
4
|
+
-- is also safe after the current schema/operator-auth.sql.
|
|
5
|
+
-- A required sign-in provider for an email domain. Sign-ins for that domain by
|
|
6
|
+
-- any other route are refused; app owners are exempt.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS oa_domain_rules (
|
|
8
|
+
domain TEXT PRIMARY KEY, provider_id TEXT NOT NULL,
|
|
9
|
+
created_by TEXT NOT NULL REFERENCES oa_operators(id), created_at INTEGER NOT NULL
|
|
10
|
+
);
|
|
11
|
+
-- A step-up proof: an app-local email link sent to the operator's own address,
|
|
12
|
+
-- bound to the session that asked and completed only in that session. No
|
|
13
|
+
-- foreign key names oa_sessions, which schema/dev-sessions.sql rebuilds.
|
|
14
|
+
CREATE TABLE IF NOT EXISTS oa_step_up_challenges (
|
|
15
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE,
|
|
16
|
+
session_id TEXT NOT NULL, operator_id TEXT NOT NULL REFERENCES oa_operators(id),
|
|
17
|
+
operator_version INTEGER NOT NULL, action TEXT NOT NULL, return_to TEXT NOT NULL,
|
|
18
|
+
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER
|
|
19
|
+
);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS oa_step_up_session ON oa_step_up_challenges(session_id);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Apply as a NEW numbered migration after operator-auth.sql in existing stores.
|
|
2
|
+
-- This durable record is admission state, not an expiring credential. Never prune it.
|
|
3
|
+
-- Existing active/inactive operators retain their recorded status: no historical
|
|
4
|
+
-- acceptance is inferred. Only a witnessed invited -> inactive transition records
|
|
5
|
+
-- an unaccepted cancellation, including direct administrative SQL transitions.
|
|
6
|
+
CREATE TABLE oa_invitation_cancellations (
|
|
7
|
+
operator_id TEXT PRIMARY KEY REFERENCES oa_operators(id),
|
|
8
|
+
operator_version INTEGER NOT NULL
|
|
9
|
+
);
|
|
10
|
+
CREATE TRIGGER oa_record_invitation_cancellation AFTER UPDATE OF status ON oa_operators
|
|
11
|
+
WHEN OLD.status='invited' AND NEW.status='inactive'
|
|
12
|
+
BEGIN INSERT INTO oa_invitation_cancellations(operator_id,operator_version)
|
|
13
|
+
VALUES(NEW.id,OLD.version+1)
|
|
14
|
+
ON CONFLICT(operator_id) DO UPDATE SET operator_version=excluded.operator_version; END;
|
|
15
|
+
CREATE TRIGGER oa_reject_cancelled_invitation_reactivation BEFORE UPDATE OF status ON oa_operators
|
|
16
|
+
WHEN NEW.status='active' AND EXISTS(SELECT 1 FROM oa_invitation_cancellations WHERE operator_id=OLD.id)
|
|
17
|
+
BEGIN SELECT RAISE(ABORT, 'invitation_required'); END;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
-- Additive schema draft. Integrator assigns the next migration number; never
|
|
2
|
+
-- replace an applied migration. All epoch timestamps are milliseconds.
|
|
3
|
+
CREATE TABLE oa_operators (
|
|
4
|
+
id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL,
|
|
5
|
+
role TEXT NOT NULL CHECK(role IN ('owner','admin','member')),
|
|
6
|
+
status TEXT NOT NULL CHECK(status IN ('invited','active','inactive')),
|
|
7
|
+
version INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL
|
|
8
|
+
);
|
|
9
|
+
CREATE TABLE oa_bootstrap (singleton INTEGER PRIMARY KEY CHECK(singleton=1), claim_id TEXT NOT NULL UNIQUE, claimed_at INTEGER NOT NULL);
|
|
10
|
+
CREATE TABLE oa_tokens (
|
|
11
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE,
|
|
12
|
+
kind TEXT NOT NULL CHECK(kind IN ('magic','invite')),
|
|
13
|
+
operator_id TEXT NOT NULL REFERENCES oa_operators(id), operator_version INTEGER NOT NULL,
|
|
14
|
+
issuer_id TEXT REFERENCES oa_operators(id), issuer_version INTEGER,
|
|
15
|
+
return_to TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL,
|
|
16
|
+
consumed_at INTEGER, revoked_at INTEGER,
|
|
17
|
+
CHECK((kind='invite' AND issuer_id IS NOT NULL AND issuer_version IS NOT NULL)
|
|
18
|
+
OR (kind='magic' AND issuer_id IS NULL AND issuer_version IS NULL))
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE oa_sessions (
|
|
21
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE,
|
|
22
|
+
operator_id TEXT NOT NULL REFERENCES oa_operators(id), operator_version INTEGER NOT NULL,
|
|
23
|
+
kind TEXT NOT NULL CHECK(kind IN ('native','review','oidc','password','dev')),
|
|
24
|
+
source_token_id TEXT UNIQUE REFERENCES oa_tokens(id),
|
|
25
|
+
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX oa_sessions_operator ON oa_sessions(operator_id);
|
|
28
|
+
CREATE TABLE oa_external_identities (
|
|
29
|
+
issuer TEXT NOT NULL, subject TEXT NOT NULL, operator_id TEXT NOT NULL REFERENCES oa_operators(id),
|
|
30
|
+
PRIMARY KEY(issuer, subject)
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE oa_limits (key TEXT PRIMARY KEY, window INTEGER NOT NULL, count INTEGER NOT NULL);
|
|
33
|
+
CREATE TRIGGER oa_preserve_last_owner_update BEFORE UPDATE OF status,role ON oa_operators
|
|
34
|
+
WHEN OLD.role='owner' AND OLD.status='active' AND (NEW.role!='owner' OR NEW.status!='active')
|
|
35
|
+
AND (SELECT COUNT(*) FROM oa_operators WHERE role='owner' AND status='active') <= 1
|
|
36
|
+
BEGIN SELECT RAISE(ABORT, 'last_owner'); END;
|
|
37
|
+
CREATE TRIGGER oa_preserve_last_owner_delete BEFORE DELETE ON oa_operators
|
|
38
|
+
WHEN OLD.role='owner' AND OLD.status='active'
|
|
39
|
+
AND (SELECT COUNT(*) FROM oa_operators WHERE role='owner' AND status='active') <= 1
|
|
40
|
+
BEGIN SELECT RAISE(ABORT, 'last_owner'); END;
|
|
41
|
+
-- A status/role change invalidates every preexisting session and link, including
|
|
42
|
+
-- after a later reactivation. Keep this invariant for direct administrative SQL.
|
|
43
|
+
CREATE TRIGGER oa_operator_version AFTER UPDATE OF status,role ON oa_operators
|
|
44
|
+
WHEN OLD.status != NEW.status OR OLD.role != NEW.role
|
|
45
|
+
BEGIN UPDATE oa_operators SET version=OLD.version+1 WHERE id=NEW.id; END;
|
|
46
|
+
|
|
47
|
+
-- Fresh installs include local dev identity. Apply schema/dev-sessions.sql as a
|
|
48
|
+
-- NEW numbered migration in existing stores; it is also safe after this file.
|
|
49
|
+
-- A credential minted while signed in with a dev session is recorded here and,
|
|
50
|
+
-- like the dev session itself, counts only behind the local key gate.
|
|
51
|
+
CREATE TABLE IF NOT EXISTS oa_dev_credentials (
|
|
52
|
+
credential TEXT NOT NULL, id TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
53
|
+
PRIMARY KEY(credential, id)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- Fresh installs include the additive invitation cancellation ledger.
|
|
57
|
+
-- Apply as a NEW numbered migration after operator-auth.sql in existing stores.
|
|
58
|
+
-- This durable record is admission state, not an expiring credential. Never prune it.
|
|
59
|
+
-- Existing active/inactive operators retain their recorded status: no historical
|
|
60
|
+
-- acceptance is inferred. Only a witnessed invited -> inactive transition records
|
|
61
|
+
-- an unaccepted cancellation, including direct administrative SQL transitions.
|
|
62
|
+
CREATE TABLE oa_invitation_cancellations (
|
|
63
|
+
operator_id TEXT PRIMARY KEY REFERENCES oa_operators(id),
|
|
64
|
+
operator_version INTEGER NOT NULL
|
|
65
|
+
);
|
|
66
|
+
CREATE TRIGGER oa_record_invitation_cancellation AFTER UPDATE OF status ON oa_operators
|
|
67
|
+
WHEN OLD.status='invited' AND NEW.status='inactive'
|
|
68
|
+
BEGIN INSERT INTO oa_invitation_cancellations(operator_id,operator_version)
|
|
69
|
+
VALUES(NEW.id,OLD.version+1)
|
|
70
|
+
ON CONFLICT(operator_id) DO UPDATE SET operator_version=excluded.operator_version; END;
|
|
71
|
+
CREATE TRIGGER oa_reject_cancelled_invitation_reactivation BEFORE UPDATE OF status ON oa_operators
|
|
72
|
+
WHEN NEW.status='active' AND EXISTS(SELECT 1 FROM oa_invitation_cancellations WHERE operator_id=OLD.id)
|
|
73
|
+
BEGIN SELECT RAISE(ABORT, 'invitation_required'); END;
|
|
74
|
+
|
|
75
|
+
-- Fresh installs include domain rules and step-up proofs. Apply
|
|
76
|
+
-- schema/domain-rules-step-up.sql as a NEW numbered migration in existing
|
|
77
|
+
-- stores; it is also safe after this file.
|
|
78
|
+
-- A required sign-in provider for an email domain. Sign-ins for that domain by
|
|
79
|
+
-- any other route are refused; app owners are exempt.
|
|
80
|
+
CREATE TABLE IF NOT EXISTS oa_domain_rules (
|
|
81
|
+
domain TEXT PRIMARY KEY, provider_id TEXT NOT NULL,
|
|
82
|
+
created_by TEXT NOT NULL REFERENCES oa_operators(id), created_at INTEGER NOT NULL
|
|
83
|
+
);
|
|
84
|
+
-- A step-up proof: an app-local email link sent to the operator's own address,
|
|
85
|
+
-- bound to the session that asked and completed only in that session. No
|
|
86
|
+
-- foreign key names oa_sessions, which schema/dev-sessions.sql rebuilds.
|
|
87
|
+
CREATE TABLE IF NOT EXISTS oa_step_up_challenges (
|
|
88
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE,
|
|
89
|
+
session_id TEXT NOT NULL, operator_id TEXT NOT NULL REFERENCES oa_operators(id),
|
|
90
|
+
operator_version INTEGER NOT NULL, action TEXT NOT NULL, return_to TEXT NOT NULL,
|
|
91
|
+
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS oa_step_up_session ON oa_step_up_challenges(session_id);
|
|
94
|
+
|
|
95
|
+
-- Additive provider controls. An absent setting means off, including upgrades.
|
|
96
|
+
CREATE TABLE IF NOT EXISTS oa_auth_settings (
|
|
97
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton=1), idp_sign_in INTEGER NOT NULL CHECK(idp_sign_in IN (0,1)), generation INTEGER NOT NULL DEFAULT 0, first_setup INTEGER NOT NULL DEFAULT 0
|
|
98
|
+
);
|
|
99
|
+
CREATE TABLE IF NOT EXISTS oa_auth_audit (
|
|
100
|
+
id TEXT PRIMARY KEY, actor_id TEXT NOT NULL, action TEXT NOT NULL, value TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
101
|
+
);
|
|
102
|
+
CREATE TABLE IF NOT EXISTS oa_provider_sessions (
|
|
103
|
+
session_id TEXT PRIMARY KEY, issuer TEXT NOT NULL, subject TEXT NOT NULL, sid TEXT, generation INTEGER NOT NULL
|
|
104
|
+
);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS oa_provider_session_identity ON oa_provider_sessions(issuer,subject,sid);
|
|
106
|
+
CREATE TABLE IF NOT EXISTS oa_logout_events (
|
|
107
|
+
issuer TEXT NOT NULL, jti TEXT NOT NULL, consumed_at INTEGER NOT NULL CHECK(consumed_at>=0), PRIMARY KEY(issuer,jti)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TRIGGER IF NOT EXISTS oa_first_setup_provider_setting AFTER INSERT ON oa_bootstrap BEGIN
|
|
111
|
+
INSERT INTO oa_auth_settings(singleton,idp_sign_in,generation,first_setup) VALUES(1,0,0,1) ON CONFLICT DO NOTHING;
|
|
112
|
+
END;
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS oa_credentials (
|
|
115
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, operator_id TEXT NOT NULL REFERENCES oa_operators(id),
|
|
116
|
+
operator_version INTEGER, kind TEXT NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- Session provenance for OIDC pins. Apply as a new migration on existing stores.
|
|
120
|
+
-- External admission has no OIDC pin and does not require OIDC protocol tables.
|
|
121
|
+
CREATE TABLE IF NOT EXISTS oa_provider_session_pins (
|
|
122
|
+
session_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, issuer TEXT NOT NULL,
|
|
123
|
+
client_id TEXT NOT NULL, linked_at INTEGER NOT NULL
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
-- Ephemeral app-local setup attempts are never exported. A native claim binds
|
|
127
|
+
-- only attempts opened before that claim to its exact owner/session authority,
|
|
128
|
+
-- in its own bootstrap transaction.
|
|
129
|
+
CREATE TABLE IF NOT EXISTS oa_setup_links (
|
|
130
|
+
interface TEXT PRIMARY KEY, attempt_id TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
131
|
+
owner_id TEXT, owner_version INTEGER, session_id TEXT,
|
|
132
|
+
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))
|
|
133
|
+
);
|
|
134
|
+
CREATE TRIGGER IF NOT EXISTS oa_native_claim_closes_provider_setup AFTER INSERT ON oa_sessions
|
|
135
|
+
WHEN NEW.kind='native' AND NOT EXISTS(SELECT 1 FROM oa_sessions WHERE operator_id=NEW.operator_id AND id!=NEW.id) AND EXISTS(SELECT 1 FROM oa_bootstrap WHERE claim_id=NEW.operator_id AND claimed_at=NEW.created_at)
|
|
136
|
+
BEGIN
|
|
137
|
+
UPDATE oa_auth_settings SET first_setup=0 WHERE singleton=1 AND NOT EXISTS(
|
|
138
|
+
SELECT 1 FROM oa_setup_links WHERE interface='oidc-provider@1' AND completed=0 AND owner_id IS NULL AND expires_at>NEW.created_at
|
|
139
|
+
);
|
|
140
|
+
UPDATE oa_setup_links SET owner_id=NEW.operator_id,owner_version=NEW.operator_version,session_id=NEW.id
|
|
141
|
+
WHERE owner_id IS NULL AND completed=0 AND expires_at>NEW.created_at;
|
|
142
|
+
END;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Additive provider controls. An absent setting means off, including upgrades.
|
|
2
|
+
CREATE TABLE IF NOT EXISTS oa_auth_settings (
|
|
3
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton=1), idp_sign_in INTEGER NOT NULL CHECK(idp_sign_in IN (0,1)), generation INTEGER NOT NULL DEFAULT 0, first_setup INTEGER NOT NULL DEFAULT 0
|
|
4
|
+
);
|
|
5
|
+
CREATE TABLE IF NOT EXISTS oa_auth_audit (
|
|
6
|
+
id TEXT PRIMARY KEY, actor_id TEXT NOT NULL, action TEXT NOT NULL, value TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
7
|
+
);
|
|
8
|
+
CREATE TABLE IF NOT EXISTS oa_provider_sessions (
|
|
9
|
+
session_id TEXT PRIMARY KEY, issuer TEXT NOT NULL, subject TEXT NOT NULL, sid TEXT, generation INTEGER NOT NULL
|
|
10
|
+
);
|
|
11
|
+
CREATE INDEX IF NOT EXISTS oa_provider_session_identity ON oa_provider_sessions(issuer,subject,sid);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS oa_logout_events (
|
|
13
|
+
issuer TEXT NOT NULL, jti TEXT NOT NULL, consumed_at INTEGER NOT NULL CHECK(consumed_at>=0), PRIMARY KEY(issuer,jti)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TRIGGER IF NOT EXISTS oa_first_setup_provider_setting AFTER INSERT ON oa_bootstrap BEGIN
|
|
17
|
+
INSERT INTO oa_auth_settings(singleton,idp_sign_in,generation,first_setup) VALUES(1,0,0,1) ON CONFLICT DO NOTHING;
|
|
18
|
+
END;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS oa_credentials (
|
|
21
|
+
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, operator_id TEXT NOT NULL REFERENCES oa_operators(id),
|
|
22
|
+
operator_version INTEGER, kind TEXT NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- Session provenance for OIDC pins. Apply as a new migration on existing stores.
|
|
26
|
+
-- External admission has no OIDC pin and does not require OIDC protocol tables.
|
|
27
|
+
CREATE TABLE IF NOT EXISTS oa_provider_session_pins (
|
|
28
|
+
session_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, issuer TEXT NOT NULL,
|
|
29
|
+
client_id TEXT NOT NULL, linked_at INTEGER NOT NULL
|
|
30
|
+
);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Session provenance for OIDC pins. Apply as a new migration on existing stores.
|
|
2
|
+
-- External admission has no OIDC pin and does not require OIDC protocol tables.
|
|
3
|
+
CREATE TABLE IF NOT EXISTS oa_provider_session_pins (
|
|
4
|
+
session_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, issuer TEXT NOT NULL,
|
|
5
|
+
client_id TEXT NOT NULL, linked_at INTEGER NOT NULL
|
|
6
|
+
);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { securityHeaders } from '../http.mjs';
|
|
2
|
+
export function identitySettingsPage({idpSignIn}) {
|
|
3
|
+
return new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sign-in settings</title><style>body{font:18px system-ui;margin:0;padding:24px;color:#152523}main{max-width:640px;margin:8vh auto}label{display:block;padding:20px 0}button{font:inherit;padding:12px 20px}input{width:20px;height:20px;margin-right:12px}</style></head><body><main><a href="/app">Back to app</a><h1>Sign-in settings</h1><p>Allow invited people to sign in through your linked identity provider. Email links remain available to the owner.</p><form action="/auth/settings/identity" method="post"><input type="hidden" name="setting" value="identity"><label><input type="checkbox" name="idpSignIn" value="true" ${idpSignIn?'checked':''}>Allow provider sign-in</label><button>Save settings</button></form></main></body></html>`,{headers:{...securityHeaders(),'Referrer-Policy':'strict-origin','Content-Type':'text/html; charset=utf-8','Content-Security-Policy':"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"}});
|
|
4
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { providerPinFence, recordProviderPin } from './provider-pin.mjs';
|
|
2
|
+
// D1 batches are transactions. All decisions that grant a session or mutate
|
|
3
|
+
// operator authority are repeated inside the same database transaction.
|
|
4
|
+
export const operatorColumns = 'id, email, display_name AS displayName, role, status, version, created_at AS createdAt';
|
|
5
|
+
export class AuthStore {
|
|
6
|
+
constructor(db) { this.db = db; }
|
|
7
|
+
statement(sql, args = []) { return this.db.prepare(sql).bind(...args); }
|
|
8
|
+
async first(sql, args = []) { return this.statement(sql, args).first(); }
|
|
9
|
+
async run(sql, args = []) { return this.statement(sql, args).run(); }
|
|
10
|
+
async batch(statements) { return this.db.batch(statements.map(([sql, args]) => this.statement(sql, args))); }
|
|
11
|
+
operator(id) { return this.first(`SELECT ${operatorColumns} FROM oa_operators WHERE id=?`, [id]); }
|
|
12
|
+
byEmail(email) { return this.first(`SELECT ${operatorColumns} FROM oa_operators WHERE email=?`, [email]); }
|
|
13
|
+
cancellation(operatorId) { return this.first('SELECT operator_version FROM oa_invitation_cancellations WHERE operator_id=?', [operatorId]); }
|
|
14
|
+
async session(hash, now) {
|
|
15
|
+
const pin = await this.first('SELECT p.provider_id AS id,p.issuer,p.client_id AS clientId,p.linked_at AS linkedAt FROM oa_provider_session_pins p JOIN oa_sessions s ON s.id=p.session_id WHERE s.token_hash=?', [hash]);
|
|
16
|
+
const fence = providerPinFence(pin);
|
|
17
|
+
const session = await this.first(`SELECT s.id,s.operator_id AS operatorId,s.created_at AS createdAt,s.expires_at AS expiresAt,s.kind,p.issuer,p.subject,p.sid, o.email,o.display_name AS displayName,o.role,o.status,o.version,o.created_at AS operatorCreatedAt
|
|
18
|
+
FROM oa_sessions s JOIN oa_operators o ON o.id=s.operator_id LEFT JOIN oa_provider_sessions p ON p.session_id=s.id
|
|
19
|
+
WHERE s.token_hash=? AND s.revoked_at IS NULL AND s.expires_at>? AND o.status='active' AND s.operator_version=o.version AND (s.kind!='oidc' OR (p.session_id IS NOT NULL AND EXISTS(SELECT 1 FROM oa_auth_settings a WHERE a.singleton=1 AND a.idp_sign_in=1 AND a.generation=p.generation))) AND ${fence.sql}`, [hash, now, ...fence.args]);
|
|
20
|
+
if (!session) return null;
|
|
21
|
+
if (pin) session.providerPin = pin;
|
|
22
|
+
const { email, displayName, role, status, version, operatorCreatedAt, ...fields } = session;
|
|
23
|
+
return { ...fields, operator: { id: session.operatorId, email, displayName, role, status, version, createdAt: operatorCreatedAt } };
|
|
24
|
+
}
|
|
25
|
+
async limits(keys, now, windowMs) {
|
|
26
|
+
const window = Math.floor(now / windowMs);
|
|
27
|
+
const rows = await this.batch(keys.map((key) => [`INSERT INTO oa_limits(key,window,count) VALUES(?,?,1)
|
|
28
|
+
ON CONFLICT(key) DO UPDATE SET window=excluded.window,count=CASE WHEN oa_limits.window=excluded.window THEN oa_limits.count+1 ELSE 1 END RETURNING count`, [key, window]]));
|
|
29
|
+
return rows.map((r) => r.results[0].count);
|
|
30
|
+
}
|
|
31
|
+
async bootstrap({ id, email, displayName, now, session }) {
|
|
32
|
+
await this.batch([
|
|
33
|
+
[`INSERT INTO oa_bootstrap(singleton,claim_id,claimed_at) SELECT 1,?,? WHERE NOT EXISTS(SELECT 1 FROM oa_operators) ON CONFLICT DO NOTHING`, [id, now]],
|
|
34
|
+
[`INSERT INTO oa_operators(id,email,display_name,role,status,created_at)
|
|
35
|
+
SELECT ?,?,?,'owner','active',? WHERE EXISTS(SELECT 1 FROM oa_bootstrap WHERE claim_id=?) ON CONFLICT DO NOTHING`, [id, email, displayName, now, id]],
|
|
36
|
+
[`INSERT INTO oa_sessions(id,token_hash,operator_id,operator_version,kind,created_at,expires_at)
|
|
37
|
+
SELECT ?,?,id,version,'native',?,? FROM oa_operators WHERE id=?`, [session.id, session.hash, now, session.expiresAt, id]],
|
|
38
|
+
]);
|
|
39
|
+
return this.session(session.hash, now);
|
|
40
|
+
}
|
|
41
|
+
// Snapshot authorization fence: both actor session and operator authority must
|
|
42
|
+
// still be valid when the SQL mutation executes, not merely when JS checked it.
|
|
43
|
+
actorFence(actor, now) {
|
|
44
|
+
const pin = providerPinFence(actor.providerPin);
|
|
45
|
+
return { sql: `${pin.sql} AND EXISTS(SELECT 1 FROM oa_sessions a JOIN oa_operators ao ON ao.id=a.operator_id
|
|
46
|
+
WHERE a.token_hash=? AND a.revoked_at IS NULL AND a.expires_at>? AND ao.status='active'
|
|
47
|
+
AND a.operator_version=ao.version AND ao.id=? AND ao.version=?
|
|
48
|
+
AND (a.kind!='oidc' OR EXISTS(SELECT 1 FROM oa_provider_sessions p JOIN oa_auth_settings c ON c.singleton=1 AND c.idp_sign_in=1 AND c.generation=p.generation WHERE p.session_id=a.id)))`, args: [...pin.args, actor.hash, now, actor.operator.id, actor.operator.version] };
|
|
49
|
+
}
|
|
50
|
+
async token(input, actor) {
|
|
51
|
+
let fence = { sql: '1', args: [] };
|
|
52
|
+
if (actor) fence = this.actorFence(actor, input.now);
|
|
53
|
+
const result = await this.run(`INSERT INTO oa_tokens(id,token_hash,kind,operator_id,operator_version,return_to,created_at,expires_at)
|
|
54
|
+
SELECT ?,?,?,id,version,?,?,? FROM oa_operators WHERE id=? AND status='active' AND ${fence.sql}`,
|
|
55
|
+
[input.id, input.hash, input.kind, input.returnTo, input.now, input.expiresAt, input.operatorId, ...fence.args]);
|
|
56
|
+
return result.meta.changes === 1;
|
|
57
|
+
}
|
|
58
|
+
async invite({ operatorId, expectedVersion, reinviting = false, email, displayName, role, token, now }, actor) {
|
|
59
|
+
const fence = this.actorFence(actor, now);
|
|
60
|
+
// Only a witnessed cancellation may reopen an inactive identity. Keep its
|
|
61
|
+
// durable admission guard until confirmation, including through pruning.
|
|
62
|
+
const prepare = reinviting
|
|
63
|
+
? [`UPDATE oa_operators SET status='invited',display_name=? WHERE id=? AND email=? AND role=? AND status='inactive' AND version=?
|
|
64
|
+
AND EXISTS(SELECT 1 FROM oa_invitation_cancellations WHERE operator_id=?) AND ${fence.sql}`,
|
|
65
|
+
[displayName, operatorId, email, role, expectedVersion, operatorId, ...fence.args]]
|
|
66
|
+
: [`INSERT INTO oa_operators(id,email,display_name,role,status,created_at)
|
|
67
|
+
SELECT ?,?,?,?,'invited',? WHERE ${fence.sql} ON CONFLICT(email) DO NOTHING`, [operatorId, email, displayName, role, now, ...fence.args]];
|
|
68
|
+
const results = await this.batch([
|
|
69
|
+
prepare,
|
|
70
|
+
[`INSERT INTO oa_tokens(id,token_hash,kind,operator_id,operator_version,issuer_id,issuer_version,return_to,created_at,expires_at)
|
|
71
|
+
SELECT ?,?,'invite',id,version,?,?,?,?,? FROM oa_operators WHERE id=? AND status='invited' AND role=? AND version=?
|
|
72
|
+
${reinviting ? 'AND changes()=1' : ''} AND ${fence.sql}`,
|
|
73
|
+
[token.id, token.hash, actor.operator.id, actor.operator.version, token.returnTo, now, token.expiresAt, operatorId, role, expectedVersion + (reinviting ? 1 : 0), ...fence.args]],
|
|
74
|
+
// Bind revocation to this transaction's successful issuance, so a stale
|
|
75
|
+
// reissue cannot invalidate the winner's link or send another credential.
|
|
76
|
+
[`UPDATE oa_tokens SET revoked_at=? WHERE operator_id=? AND id!=? AND consumed_at IS NULL
|
|
77
|
+
AND EXISTS(SELECT 1 FROM oa_tokens WHERE id=?)`, [now, operatorId, token.id, token.id]],
|
|
78
|
+
// A link minted while using a dev session stays a dev credential.
|
|
79
|
+
...(actor.kind === 'dev' ? [[`INSERT INTO oa_dev_credentials(credential,id,created_at)
|
|
80
|
+
SELECT 'oa_tokens',id,? FROM oa_tokens WHERE id=?`, [now, token.id]]] : []),
|
|
81
|
+
]);
|
|
82
|
+
return results[1].meta.changes === 1;
|
|
83
|
+
}
|
|
84
|
+
async preview(hash, now) {
|
|
85
|
+
return this.first(`SELECT t.kind,t.expires_at AS expiresAt,t.return_to AS returnTo,o.email,o.role,t.issuer_id AS issuerId,t.issuer_version AS issuerVersion,
|
|
86
|
+
EXISTS(SELECT 1 FROM oa_dev_credentials d WHERE d.credential='oa_tokens' AND d.id=t.id) AS dev FROM oa_tokens t JOIN oa_operators o ON o.id=t.operator_id
|
|
87
|
+
WHERE t.token_hash=? AND t.consumed_at IS NULL AND t.revoked_at IS NULL AND t.expires_at>? AND t.operator_version=o.version
|
|
88
|
+
AND (o.status='active' OR (o.status='invited' AND t.kind='invite'))`, [hash, now]);
|
|
89
|
+
}
|
|
90
|
+
async consume(hash, now, session, allowedEmails, issuer) {
|
|
91
|
+
const allow = allowedEmails === undefined ? { sql: '1', args: [] } : { sql: `o.email IN (${allowedEmails.map(() => '?').join(',') || 'NULL'})`, args: allowedEmails };
|
|
92
|
+
// JS evaluated the configured policy on this issuer snapshot. Bind that
|
|
93
|
+
// approval to the invitation and repeat its authority check in the session
|
|
94
|
+
// transaction, so a concurrent demotion/deactivation cannot admit anyone.
|
|
95
|
+
const authority = issuer ? { sql: `t.issuer_id=? AND t.issuer_version=? AND EXISTS(
|
|
96
|
+
SELECT 1 FROM oa_operators i WHERE i.id=t.issuer_id AND i.version=t.issuer_version AND i.status='active')`,
|
|
97
|
+
args: [issuer.id, issuer.version] } : { sql: '0', args: [] };
|
|
98
|
+
await this.batch([
|
|
99
|
+
[`INSERT INTO oa_sessions(id,token_hash,operator_id,operator_version,kind,source_token_id,created_at,expires_at)
|
|
100
|
+
SELECT ?,?,o.id,o.version+CASE WHEN o.status='invited' THEN 1 ELSE 0 END,
|
|
101
|
+
CASE WHEN EXISTS(SELECT 1 FROM oa_dev_credentials d WHERE d.credential='oa_tokens' AND d.id=t.id) THEN 'dev' ELSE 'native' END,t.id,?,?
|
|
102
|
+
FROM oa_tokens t JOIN oa_operators o ON o.id=t.operator_id
|
|
103
|
+
WHERE t.token_hash=? AND t.consumed_at IS NULL AND t.revoked_at IS NULL AND t.expires_at>? AND t.operator_version=o.version
|
|
104
|
+
AND (o.status='active' OR (o.status='invited' AND t.kind='invite')) AND ${allow.sql}
|
|
105
|
+
AND (t.kind='magic' OR (${authority.sql}))`,
|
|
106
|
+
[session.id, session.hash, now, session.expiresAt, hash, now, ...allow.args, ...authority.args]],
|
|
107
|
+
// Only the session just admitted by the full token/issuer/allow-list
|
|
108
|
+
// predicate witnesses acceptance. Removing its cancellation guard and
|
|
109
|
+
// activating the operator are atomic with that admission; failures roll
|
|
110
|
+
// back all three writes. An ordinary status update has no such witness.
|
|
111
|
+
[`DELETE FROM oa_invitation_cancellations WHERE operator_id=(
|
|
112
|
+
SELECT s.operator_id FROM oa_sessions s JOIN oa_tokens t ON t.id=s.source_token_id
|
|
113
|
+
JOIN oa_operators o ON o.id=s.operator_id WHERE s.id=? AND t.kind='invite'
|
|
114
|
+
AND t.operator_id=o.id AND o.status='invited' AND t.operator_version=o.version AND s.operator_version=o.version+1)`, [session.id]],
|
|
115
|
+
[`UPDATE oa_operators SET status='active' WHERE status='invited' AND id=(SELECT operator_id FROM oa_sessions WHERE id=?)`, [session.id]],
|
|
116
|
+
[`UPDATE oa_tokens SET consumed_at=? WHERE id=(SELECT source_token_id FROM oa_sessions WHERE id=?)`, [now, session.id]],
|
|
117
|
+
]);
|
|
118
|
+
const admitted = await this.session(session.hash, now);
|
|
119
|
+
if (!admitted) return null;
|
|
120
|
+
const token = await this.first(`SELECT return_to AS returnTo FROM oa_tokens WHERE token_hash=?`, [hash]);
|
|
121
|
+
return { ...admitted, returnTo: token.returnTo };
|
|
122
|
+
}
|
|
123
|
+
async issue({ operatorId, session, now, kind, expectedVersion }) {
|
|
124
|
+
await this.run(`INSERT INTO oa_sessions(id,token_hash,operator_id,operator_version,kind,created_at,expires_at)
|
|
125
|
+
SELECT ?,?,id,version,?,?,? FROM oa_operators WHERE id=? AND status='active' AND version=?`,
|
|
126
|
+
[session.id, session.hash, kind, now, session.expiresAt, operatorId, expectedVersion]);
|
|
127
|
+
return this.session(session.hash, now);
|
|
128
|
+
}
|
|
129
|
+
// The account bound to a provider principal. Bindings are never rebound.
|
|
130
|
+
async binding(issuer, subject) {
|
|
131
|
+
return this.first(`SELECT o.id, o.email, o.display_name AS displayName, o.role, o.status, o.version, o.created_at AS createdAt
|
|
132
|
+
FROM oa_external_identities e JOIN oa_operators o ON o.id=e.operator_id
|
|
133
|
+
WHERE e.issuer=? AND e.subject=?`, [issuer, subject]);
|
|
134
|
+
}
|
|
135
|
+
// Binds (issuer, subject) to an active operator at the version the caller
|
|
136
|
+
// checked. A proven email binds only the operator's first principal at that
|
|
137
|
+
// issuer; an explicit configured association may add another. Binding closes
|
|
138
|
+
// the operator's password sessions in the same transaction.
|
|
139
|
+
async bind({ issuer, subject, operatorId, expectedVersion, associated, now }) {
|
|
140
|
+
await this.batch([
|
|
141
|
+
[`INSERT INTO oa_external_identities(issuer,subject,operator_id)
|
|
142
|
+
SELECT ?,?,o.id FROM oa_operators o WHERE o.id=? AND o.status='active' AND o.version=?
|
|
143
|
+
AND (? OR NOT EXISTS(SELECT 1 FROM oa_external_identities e WHERE e.operator_id=o.id AND e.issuer=?))
|
|
144
|
+
ON CONFLICT DO NOTHING`, [issuer, subject, operatorId, expectedVersion, associated ? 1 : 0, issuer]],
|
|
145
|
+
[`UPDATE oa_sessions SET revoked_at=? WHERE operator_id=? AND kind='password' AND revoked_at IS NULL
|
|
146
|
+
AND EXISTS(SELECT 1 FROM oa_external_identities WHERE issuer=? AND subject=? AND operator_id=?)`, [now, operatorId, issuer, subject, operatorId]],
|
|
147
|
+
]);
|
|
148
|
+
const row = await this.first('SELECT operator_id AS operatorId FROM oa_external_identities WHERE issuer=? AND subject=?', [issuer, subject]);
|
|
149
|
+
return row?.operatorId === operatorId;
|
|
150
|
+
}
|
|
151
|
+
// A provider session exists only while its binding still names the operator.
|
|
152
|
+
async issueBound({ issuer, subject, sid = null, associated = false, establish = false, expectedGeneration = null, providerPin, operatorId, expectedVersion, session, now }) {
|
|
153
|
+
const pin = providerPinFence(providerPin);
|
|
154
|
+
await this.batch([
|
|
155
|
+
...(establish ? [[`INSERT INTO oa_external_identities(issuer,subject,operator_id)
|
|
156
|
+
SELECT ?,?,o.id FROM oa_operators o WHERE o.id=? AND o.status='active' AND o.version=?
|
|
157
|
+
AND (? OR NOT EXISTS(SELECT 1 FROM oa_external_identities e WHERE e.operator_id=o.id AND e.issuer=?))
|
|
158
|
+
AND ${pin.sql} AND EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1 AND (? IS NULL OR generation=?)) ON CONFLICT DO NOTHING`,
|
|
159
|
+
[issuer,subject,operatorId,expectedVersion,associated?1:0,issuer,...pin.args,expectedGeneration,expectedGeneration]]] : []),
|
|
160
|
+
[`INSERT INTO oa_sessions(id,token_hash,operator_id,operator_version,kind,created_at,expires_at)
|
|
161
|
+
SELECT ?,?,o.id,o.version,'oidc',?,? FROM oa_external_identities e JOIN oa_operators o ON o.id=e.operator_id
|
|
162
|
+
WHERE e.issuer=? AND e.subject=? AND o.id=? AND o.status='active' AND o.version=?
|
|
163
|
+
AND ${pin.sql} AND EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1 AND (? IS NULL OR generation=?))`,
|
|
164
|
+
[session.id, session.hash, now, Math.min(session.expiresAt, now+86400000), issuer, subject, operatorId, expectedVersion, ...pin.args, expectedGeneration, expectedGeneration]],
|
|
165
|
+
[`INSERT INTO oa_provider_sessions(session_id,issuer,subject,sid,generation)
|
|
166
|
+
SELECT ?,?,?,?,generation FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1
|
|
167
|
+
AND EXISTS(SELECT 1 FROM oa_sessions WHERE id=?)`, [session.id, issuer, subject, sid, session.id]],
|
|
168
|
+
...recordProviderPin(providerPin, session.id),
|
|
169
|
+
[`UPDATE oa_sessions SET revoked_at=? WHERE operator_id=? AND kind='password' AND revoked_at IS NULL AND EXISTS(SELECT 1 FROM oa_sessions WHERE id=?)`,[now,operatorId,session.id]],
|
|
170
|
+
]);
|
|
171
|
+
return this.session(session.hash, now);
|
|
172
|
+
}
|
|
173
|
+
hasBinding(operatorId) { return this.first('SELECT 1 AS bound FROM oa_external_identities WHERE operator_id=? LIMIT 1', [operatorId]); }
|
|
174
|
+
// The live invitation a provider sign-in may accept. Dev-minted invitations
|
|
175
|
+
// count only behind the local key gate, so a provider never accepts one.
|
|
176
|
+
liveInvitation(operatorId, now) {
|
|
177
|
+
return this.first(`SELECT t.id,t.issuer_id AS issuerId,t.issuer_version AS issuerVersion,t.return_to AS returnTo FROM oa_tokens t JOIN oa_operators o ON o.id=t.operator_id
|
|
178
|
+
WHERE t.operator_id=? AND t.kind='invite' AND t.consumed_at IS NULL AND t.revoked_at IS NULL AND t.expires_at>? AND t.operator_version=o.version AND o.status='invited'
|
|
179
|
+
AND NOT EXISTS(SELECT 1 FROM oa_dev_credentials d WHERE d.credential='oa_tokens' AND d.id=t.id) ORDER BY t.created_at DESC LIMIT 1`, [operatorId, now]);
|
|
180
|
+
}
|
|
181
|
+
// Accepts a pending invitation through a proven provider principal: the same
|
|
182
|
+
// witnessed transaction as an email-link acceptance, plus the binding.
|
|
183
|
+
async acceptByBinding({ issuer, subject, sid = null, expectedGeneration = null, providerPin, tokenId, operatorId, expectedVersion, inviter, session, now }) {
|
|
184
|
+
const pin = providerPinFence(providerPin);
|
|
185
|
+
await this.batch([
|
|
186
|
+
[`INSERT INTO oa_external_identities(issuer,subject,operator_id)
|
|
187
|
+
SELECT ?,?,o.id FROM oa_operators o WHERE o.id=? AND o.status='invited' AND o.version=?
|
|
188
|
+
AND NOT EXISTS(SELECT 1 FROM oa_external_identities e WHERE e.operator_id=o.id AND e.issuer=?)
|
|
189
|
+
AND ${pin.sql} AND EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1 AND (? IS NULL OR generation=?))
|
|
190
|
+
AND EXISTS(SELECT 1 FROM oa_tokens t JOIN oa_operators i ON i.id=t.issuer_id WHERE t.id=? AND t.operator_id=o.id AND t.kind='invite' AND t.consumed_at IS NULL AND t.revoked_at IS NULL AND t.expires_at>? AND t.operator_version=o.version AND t.issuer_id=? AND t.issuer_version=? AND i.version=t.issuer_version AND i.status='active') ON CONFLICT DO NOTHING`,
|
|
191
|
+
[issuer, subject, operatorId, expectedVersion, issuer,...pin.args,expectedGeneration,expectedGeneration,tokenId,now,inviter.id,inviter.version]],
|
|
192
|
+
[`INSERT INTO oa_sessions(id,token_hash,operator_id,operator_version,kind,source_token_id,created_at,expires_at)
|
|
193
|
+
SELECT ?,?,o.id,o.version+1,'oidc',t.id,?,? FROM oa_tokens t JOIN oa_operators o ON o.id=t.operator_id
|
|
194
|
+
JOIN oa_external_identities e ON e.operator_id=o.id AND e.issuer=? AND e.subject=?
|
|
195
|
+
WHERE t.id=? AND t.kind='invite' AND t.consumed_at IS NULL AND t.revoked_at IS NULL AND t.expires_at>? AND t.operator_version=o.version
|
|
196
|
+
AND o.id=? AND o.status='invited' AND o.version=? AND t.issuer_id=? AND t.issuer_version=?
|
|
197
|
+
AND EXISTS(SELECT 1 FROM oa_operators i WHERE i.id=t.issuer_id AND i.version=t.issuer_version AND i.status='active')
|
|
198
|
+
AND ${pin.sql} AND EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1 AND (? IS NULL OR generation=?))`,
|
|
199
|
+
[session.id, session.hash, now, Math.min(session.expiresAt, now+86400000), issuer, subject, tokenId, now, operatorId, expectedVersion, inviter.id, inviter.version, ...pin.args, expectedGeneration, expectedGeneration]],
|
|
200
|
+
[`INSERT INTO oa_provider_sessions(session_id,issuer,subject,sid,generation) SELECT ?,?,?,?,generation FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=1 AND EXISTS(SELECT 1 FROM oa_sessions WHERE id=?)`, [session.id, issuer, subject, sid, session.id]],
|
|
201
|
+
...recordProviderPin(providerPin, session.id),
|
|
202
|
+
[`DELETE FROM oa_invitation_cancellations WHERE operator_id=(SELECT operator_id FROM oa_sessions WHERE id=?)`, [session.id]],
|
|
203
|
+
[`UPDATE oa_operators SET status='active' WHERE status='invited' AND id=(SELECT operator_id FROM oa_sessions WHERE id=?)`, [session.id]],
|
|
204
|
+
[`UPDATE oa_tokens SET consumed_at=? WHERE id=(SELECT source_token_id FROM oa_sessions WHERE id=?)`, [now, session.id]],
|
|
205
|
+
]);
|
|
206
|
+
return this.session(session.hash, now);
|
|
207
|
+
}
|
|
208
|
+
domainRule(domain) { return this.first('SELECT domain,provider_id AS providerId FROM oa_domain_rules WHERE domain=?', [domain]); }
|
|
209
|
+
async domainRules() { return (await this.statement('SELECT domain,provider_id AS providerId,created_at AS createdAt FROM oa_domain_rules ORDER BY domain').all()).results; }
|
|
210
|
+
async setDomainRule({ domain, providerId, now }, actor) {
|
|
211
|
+
const fence = this.actorFence(actor, now);
|
|
212
|
+
const result = providerId === null
|
|
213
|
+
? await this.run(`DELETE FROM oa_domain_rules WHERE domain=? AND ${fence.sql}`, [domain, ...fence.args])
|
|
214
|
+
: await this.run(`INSERT INTO oa_domain_rules(domain,provider_id,created_by,created_at) SELECT ?,?,?,? WHERE ${fence.sql}
|
|
215
|
+
ON CONFLICT(domain) DO UPDATE SET provider_id=excluded.provider_id,created_by=excluded.created_by,created_at=excluded.created_at`,
|
|
216
|
+
[domain, providerId, actor.operator.id, now, ...fence.args]);
|
|
217
|
+
return result.meta.changes;
|
|
218
|
+
}
|
|
219
|
+
liftDomainRules(providerId) { return this.run('DELETE FROM oa_domain_rules WHERE provider_id=?', [providerId]); }
|
|
220
|
+
async stepUpChallenge({ id, hash, action, returnTo, now, expiresAt }, actor) {
|
|
221
|
+
const fence = this.actorFence(actor, now);
|
|
222
|
+
const result = await this.run(`INSERT INTO oa_step_up_challenges(id,token_hash,session_id,operator_id,operator_version,action,return_to,created_at,expires_at)
|
|
223
|
+
SELECT ?,?,?,?,?,?,?,?,? WHERE ${fence.sql}`, [id, hash, actor.id, actor.operator.id, actor.operator.version, action, returnTo, now, expiresAt, ...fence.args]);
|
|
224
|
+
return result.meta.changes === 1;
|
|
225
|
+
}
|
|
226
|
+
stepUpPreview(hash, now) {
|
|
227
|
+
return this.first(`SELECT c.session_id AS sessionId,c.action,c.expires_at AS expiresAt FROM oa_step_up_challenges c JOIN oa_operators o ON o.id=c.operator_id
|
|
228
|
+
WHERE c.token_hash=? AND c.consumed_at IS NULL AND c.expires_at>? AND c.operator_version=o.version AND o.status='active'`, [hash, now]);
|
|
229
|
+
}
|
|
230
|
+
// Completed only in the session that asked, while it is still valid.
|
|
231
|
+
async consumeStepUp(hash, actor, now) {
|
|
232
|
+
const fence = this.actorFence(actor, now);
|
|
233
|
+
return this.first(`UPDATE oa_step_up_challenges SET consumed_at=? WHERE token_hash=? AND session_id=? AND operator_id=? AND operator_version=?
|
|
234
|
+
AND consumed_at IS NULL AND expires_at>? AND ${fence.sql} RETURNING return_to AS returnTo, action`,
|
|
235
|
+
[now, hash, actor.id, actor.operator.id, actor.operator.version, now, ...fence.args]);
|
|
236
|
+
}
|
|
237
|
+
// A fresh app-local email-link proof for this session: a completed step-up
|
|
238
|
+
// challenge, or the email-link sign-in that created it. Provider-issued
|
|
239
|
+
// sessions have neither.
|
|
240
|
+
async steppedUp(actor, since) {
|
|
241
|
+
const row = await this.first(`SELECT EXISTS(SELECT 1 FROM oa_step_up_challenges WHERE session_id=? AND operator_version=? AND consumed_at>=?)
|
|
242
|
+
OR EXISTS(SELECT 1 FROM oa_sessions s JOIN oa_tokens t ON t.id=s.source_token_id WHERE s.id=? AND s.kind='native' AND s.created_at>=?) AS fresh`,
|
|
243
|
+
[actor.id, actor.operator.version, since, actor.id, since]);
|
|
244
|
+
return !!row?.fresh;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AuthError } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
// Canonical_Combining_Class=9 (Virama), Unicode 16. JavaScript regular
|
|
4
|
+
// expressions have no combining-class property, so the scalars are listed.
|
|
5
|
+
const VIRAMA = '\\u{94d}\\u{9cd}\\u{a4d}\\u{acd}\\u{b4d}\\u{bcd}\\u{c4d}\\u{ccd}\\u{d3b}-\\u{d3c}\\u{d4d}\\u{dca}\\u{e3a}\\u{eba}\\u{f84}'
|
|
6
|
+
+ '\\u{1039}-\\u{103a}\\u{1714}-\\u{1715}\\u{1734}\\u{17d2}\\u{1a60}\\u{1b44}\\u{1baa}-\\u{1bab}\\u{1bf2}-\\u{1bf3}\\u{2d7f}\\u{a806}\\u{a82c}'
|
|
7
|
+
+ '\\u{a8c4}\\u{a953}\\u{a9c0}\\u{aaf6}\\u{abed}\\u{10a3f}\\u{11046}\\u{11070}\\u{1107f}\\u{110b9}\\u{11133}-\\u{11134}\\u{111c0}'
|
|
8
|
+
+ '\\u{11235}\\u{112ea}\\u{1134d}\\u{113ce}-\\u{113d0}\\u{11442}\\u{114c2}\\u{115bf}\\u{1163f}\\u{116b6}\\u{1172b}\\u{11839}'
|
|
9
|
+
+ '\\u{1193d}-\\u{1193e}\\u{119e0}\\u{11a34}\\u{11a47}\\u{11a99}\\u{11c3f}\\u{11d44}-\\u{11d45}\\u{11d97}\\u{11f41}-\\u{11f42}\\u{1612f}';
|
|
10
|
+
const ZWJ_AFTER_VIRAMA = new RegExp(`(?<=\\p{L}\\p{M}*[${VIRAMA}])\\u200d`, 'gu');
|
|
11
|
+
|
|
12
|
+
function validProductName(name) {
|
|
13
|
+
// Lone surrogates are not text; U+FFFC is a placeholder for an absent object.
|
|
14
|
+
if (typeof name !== 'string' || name.length > 80 || !name.isWellFormed()) return false;
|
|
15
|
+
// Braille blank is a symbol by category, but cannot make a name visible.
|
|
16
|
+
if (!/(?!\u2800)[\p{L}\p{N}\p{P}\p{S}]/u.test(name)) return false;
|
|
17
|
+
// Inspect a copy: retain script shaping and emoji presentation in the name
|
|
18
|
+
// returned to callers, while rejecting every other default-ignorable scalar.
|
|
19
|
+
const inspected = name
|
|
20
|
+
// ZWNJ after a letter, before a letter or digit, optionally across spaces (Persian "کتاب ها").
|
|
21
|
+
.replace(/(?<=\p{L}\p{M}*)\u200c(?=\s*[\p{L}\p{Nd}])/gu, '')
|
|
22
|
+
.replace(/(?<=\p{L}\p{M}*)\u200d(?=\p{L})/gu, '')
|
|
23
|
+
// ZWJ after a virama forms explicit half forms and Malayalam chillu, also word-final.
|
|
24
|
+
.replace(ZWJ_AFTER_VIRAMA, '')
|
|
25
|
+
.replace(/(?<=\p{Extended_Pictographic}\ufe0f?\p{Emoji_Modifier}?)\u200d(?=\p{Extended_Pictographic})/gu, '')
|
|
26
|
+
// U+FE0F selects emoji presentation of a pictograph, or of a keycap base only inside its keycap.
|
|
27
|
+
.replace(/(?<=\p{Extended_Pictographic})\ufe0f/gu, '')
|
|
28
|
+
.replace(/(?<=[0-9#*])\ufe0f(?=\u20e3)/gu, '');
|
|
29
|
+
return !/[\u0000-\u001f\u007f-\u009f\u2028\u2029\ufffc\p{Default_Ignorable_Code_Point}]/u.test(inspected);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function configureBranding(input = {}) {
|
|
33
|
+
const invalid = () => { throw new AuthError('invalid_configuration', 503); };
|
|
34
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) invalid();
|
|
35
|
+
const { productName = 'your workspace', accentColor = '#334155', signInPath = '/signin', theme } = input;
|
|
36
|
+
if (!validProductName(productName)) invalid();
|
|
37
|
+
if (typeof accentColor !== 'string' || !/^#[0-9a-f]{6}$/i.test(accentColor)) invalid();
|
|
38
|
+
if (typeof signInPath !== 'string' || !signInPath.startsWith('/') || signInPath.startsWith('//') || /[\\\u0000-\u0020\u007f-\u009f]/u.test(signInPath)) invalid();
|
|
39
|
+
if (signInPath.length > 2048) invalid();
|
|
40
|
+
// Only the configured encoding must be well formed. Decoding %25 can produce
|
|
41
|
+
// literal percent data, so subsequent inspection must tolerate it while still
|
|
42
|
+
// revealing nested separators/control characters elsewhere in the route.
|
|
43
|
+
let decoded;
|
|
44
|
+
try { decoded = decodeURIComponent(signInPath); } catch { invalid(); }
|
|
45
|
+
const decoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (decoded.startsWith('//') || /[\\\u0000-\u001f\u007f-\u009f]/u.test(decoded)) invalid();
|
|
48
|
+
// Replacement characters for non-UTF-8 byte sequences are inspection-only;
|
|
49
|
+
// no decoded or repaired value is ever returned to the consumer.
|
|
50
|
+
const next = decoded.replace(/(?:%[0-9a-f]{2})+/giu, (encoded) =>
|
|
51
|
+
decoder.decode(Uint8Array.from(encoded.match(/[0-9a-f]{2}/giu), (byte) => parseInt(byte, 16))));
|
|
52
|
+
if (next === decoded) break;
|
|
53
|
+
decoded = next;
|
|
54
|
+
}
|
|
55
|
+
// A seed theme is its own served rer-theme.css; Base onboarding validates its contents.
|
|
56
|
+
if (theme !== undefined && (typeof theme !== 'string' || !/^\/(?!\/)[A-Za-z0-9._~/-]{1,200}\.css$/.test(theme) || theme.includes('//') || theme.includes('/.'))) invalid();
|
|
57
|
+
return Object.freeze({ productName: productName.trim(), accentColor, signInPath, ...(theme ? { theme } : {}) });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** WCAG relative luminance chooses readable text for any permitted brand color. */
|
|
61
|
+
export function accentInk(color) {
|
|
62
|
+
const channels = [1, 3, 5].map((offset) => parseInt(color.slice(offset, offset + 2), 16) / 255)
|
|
63
|
+
.map((value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
|
|
64
|
+
const luminance = channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
65
|
+
return luminance > 0.179 ? '#000000' : '#ffffff';
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { configureBranding } from './branding.mjs';
|
|
2
|
+
import { AuthError } from './errors.mjs';
|
|
3
|
+
import { normalizeEmail } from './policy.mjs';
|
|
4
|
+
import { isLoopbackHost, localCookieName } from './http.mjs';
|
|
5
|
+
function seconds(value, fallback, max) {
|
|
6
|
+
const n = value ?? fallback;
|
|
7
|
+
if (!Number.isSafeInteger(n) || n < 1 || n > max) throw new AuthError('invalid_configuration', 503);
|
|
8
|
+
return n;
|
|
9
|
+
}
|
|
10
|
+
export function configure(input) {
|
|
11
|
+
let url;
|
|
12
|
+
try { url = new URL(input.origin); } catch { throw new AuthError('invalid_configuration', 503); }
|
|
13
|
+
const local = input.local === true;
|
|
14
|
+
if (url.origin !== input.origin || url.username || url.password || (local ? (!isLoopbackHost(url.hostname) || !['http:', 'https:'].includes(url.protocol)) : url.protocol !== 'https:')) throw new AuthError('invalid_configuration', 503);
|
|
15
|
+
if (typeof input.SESSION_SECRET !== 'string' || input.SESSION_SECRET.length < 32 || input.SESSION_SECRET.length > 1024) throw new AuthError('invalid_configuration', 503);
|
|
16
|
+
if (input.SETUP_TOKEN !== undefined && (typeof input.SETUP_TOKEN !== 'string' || input.SETUP_TOKEN.length < 32 || input.SETUP_TOKEN.length > 1024)) throw new AuthError('invalid_configuration', 503);
|
|
17
|
+
const cookieName = local ? localCookieName('operator_session_local', input.origin) : '__Host-operator_session';
|
|
18
|
+
// Identity providers are validated in full by identity/oidc; here only the
|
|
19
|
+
// shape and the independent transaction secret they need.
|
|
20
|
+
const providers = input.providers ?? [];
|
|
21
|
+
if (!Array.isArray(providers) || providers.length && (typeof input.OIDC_TRANSACTION_SECRET !== 'string' || input.OIDC_TRANSACTION_SECRET.length < 32
|
|
22
|
+
|| input.OIDC_TRANSACTION_SECRET === input.SESSION_SECRET)) throw new AuthError('invalid_configuration', 503);
|
|
23
|
+
const externalIssuers = input.externalIssuers ?? [];
|
|
24
|
+
if (!Array.isArray(externalIssuers) || externalIssuers.some((v) => { try { const u = new URL(v); return u.protocol !== 'https:' || !!u.username || !!u.password || !!u.hash || !!u.search; } catch { return true; } })) throw new AuthError('invalid_configuration', 503);
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
...input, branding: configureBranding(input.branding), local, secure: url.protocol === 'https:', cookieName, OWNER_EMAIL: normalizeEmail(input.OWNER_EMAIL), externalIssuers: Object.freeze([...externalIssuers]), providers: Object.freeze([...providers]),
|
|
27
|
+
review: input.review ? Object.freeze({ ...input.review, operatorEmail: normalizeEmail(input.review.operatorEmail) }) : undefined,
|
|
28
|
+
allowList: input.allowList === undefined ? undefined : input.allowList.map(normalizeEmail),
|
|
29
|
+
sessionSeconds: seconds(input.sessionSeconds, 7 * 86400, 30 * 86400),
|
|
30
|
+
linkSeconds: seconds(input.linkSeconds, 900, 3600),
|
|
31
|
+
reviewSeconds: seconds(input.reviewSeconds, 3600, 3600),
|
|
32
|
+
limitWindowSeconds: seconds(input.limitWindowSeconds, 900, 86400),
|
|
33
|
+
ipLimit: seconds(input.ipLimit, 30, 10000), emailLimit: seconds(input.emailLimit, 5, 1000),
|
|
34
|
+
});
|
|
35
|
+
}
|