@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,159 @@
|
|
|
1
|
+
import { AuthError, deny } from '../errors.mjs';
|
|
2
|
+
import { normalizeEmail, safeReturnTo } from '../policy.mjs';
|
|
3
|
+
import { requirePost } from '../http.mjs';
|
|
4
|
+
import { operatorColumns } from './store.mjs';
|
|
5
|
+
import { DEFAULT_ROLES, emailAllowed } from './roles.mjs';
|
|
6
|
+
const DOMAIN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
|
|
7
|
+
export function administration(context) {
|
|
8
|
+
const { config, store, clock, authorize, authenticate, newToken, sendLink, limit, requireFreshProof, providers } = context;
|
|
9
|
+
async function actorFor(request, action, target, role, mutation = true) {
|
|
10
|
+
if (mutation) requirePost(request, config);
|
|
11
|
+
const actor = await authenticate(request);
|
|
12
|
+
if (!actor) deny('unauthenticated', 401);
|
|
13
|
+
if (!await authorize({ actor: actor.operator, action, target, role })) deny();
|
|
14
|
+
// Every administrative owner-authority write passes this shared guard,
|
|
15
|
+
// including pending grants and removal by invitation cancellation.
|
|
16
|
+
const unchanged = (action === 'operators:role' && role === target?.role)
|
|
17
|
+
|| (action === 'operators:deactivate' && target?.status === 'inactive')
|
|
18
|
+
|| (action === 'operators:reactivate' && target?.status === 'active');
|
|
19
|
+
if (mutation && action.startsWith('operators:')
|
|
20
|
+
&& !unchanged
|
|
21
|
+
&& (role === 'owner' || target?.role === 'owner')) await requireFreshProof(actor);
|
|
22
|
+
return actor;
|
|
23
|
+
}
|
|
24
|
+
async function targetFor(id) {
|
|
25
|
+
const target = typeof id === 'string' ? await store.operator(id) : null;
|
|
26
|
+
if (!target) deny('operator_not_found', 404);
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async invite(request, input) {
|
|
31
|
+
const role = input.role ?? 'member';
|
|
32
|
+
if (!DEFAULT_ROLES.includes(role)) throw new AuthError('invalid_role');
|
|
33
|
+
const actor = await actorFor(request, 'operators:invite', undefined, role);
|
|
34
|
+
const email = normalizeEmail(input.email);
|
|
35
|
+
await limit(request, email, 'invite');
|
|
36
|
+
if (!emailAllowed(email, config.allowList)) deny('email_not_allowed');
|
|
37
|
+
const existing = await store.byEmail(email);
|
|
38
|
+
const reinviting = existing?.status === 'inactive' && !!await store.cancellation(existing.id);
|
|
39
|
+
if (existing && (existing.role !== role || (!reinviting && existing.status !== 'invited'))) deny('invitation_conflict', 409);
|
|
40
|
+
const now = clock();
|
|
41
|
+
const token = await newToken('invite', safeReturnTo(input.returnTo));
|
|
42
|
+
const created = await store.invite({ operatorId: existing?.id ?? crypto.randomUUID(), expectedVersion: existing?.version ?? 1, reinviting, email, displayName: displayName(input.displayName), role, token, now }, actor);
|
|
43
|
+
if (!created) deny('invitation_conflict', 409);
|
|
44
|
+
await sendLink(email, token);
|
|
45
|
+
return { status: 'accepted' };
|
|
46
|
+
},
|
|
47
|
+
async listOperators(request) {
|
|
48
|
+
const actor = await actorFor(request, 'operators:list', undefined, undefined, false);
|
|
49
|
+
const fence = store.actorFence(actor, clock());
|
|
50
|
+
const rows = await store.statement(`SELECT ${operatorColumns} FROM oa_operators WHERE ${fence.sql} ORDER BY created_at,id`, fence.args).all();
|
|
51
|
+
return rows.results;
|
|
52
|
+
},
|
|
53
|
+
async cancelInvitation(request, operatorId) {
|
|
54
|
+
const target = await targetFor(operatorId);
|
|
55
|
+
const actor = await actorFor(request, 'operators:deactivate', target);
|
|
56
|
+
if (target.status !== 'invited') deny('invitation_conflict', 409);
|
|
57
|
+
const fence = store.actorFence(actor, clock());
|
|
58
|
+
// The guarded write and version trigger are one transaction: cancellation
|
|
59
|
+
// invalidates every outstanding link and cannot deactivate a raced join.
|
|
60
|
+
// Read the ledger to fail closed if its migration is missing. A previous
|
|
61
|
+
// cancellation remains guarded throughout reinvitation; cancelling again
|
|
62
|
+
// refreshes that marker through the existing database trigger.
|
|
63
|
+
await store.cancellation(target.id);
|
|
64
|
+
const result = await store.run(`UPDATE oa_operators SET status='inactive' WHERE id=? AND status='invited' AND version=?
|
|
65
|
+
AND ${fence.sql}`,
|
|
66
|
+
[target.id, target.version, ...fence.args]);
|
|
67
|
+
if (result.meta.changes < 1) deny('authority_changed', 409);
|
|
68
|
+
return store.operator(target.id);
|
|
69
|
+
},
|
|
70
|
+
async setOperatorStatus(request, operatorId, status) {
|
|
71
|
+
if (status !== 'active' && status !== 'inactive') throw new AuthError('invalid_status');
|
|
72
|
+
const target = await targetFor(operatorId);
|
|
73
|
+
const actor = await actorFor(request, status === 'active' ? 'operators:reactivate' : 'operators:deactivate', target);
|
|
74
|
+
if (target.status === 'invited') deny('invitation_required', 409);
|
|
75
|
+
const fence = store.actorFence(actor, clock());
|
|
76
|
+
try {
|
|
77
|
+
const result = await store.run(`UPDATE oa_operators SET status=? WHERE id=? AND version=? AND ${fence.sql}`, [status, target.id, target.version, ...fence.args]);
|
|
78
|
+
if (result.meta.changes < 1) deny('authority_changed', 409);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (String(e).includes('last_owner')) deny('last_owner', 409);
|
|
81
|
+
if (String(e).includes('invitation_required')) deny('invitation_required', 409);
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
return store.operator(target.id);
|
|
85
|
+
},
|
|
86
|
+
async setRole(request, operatorId, role) {
|
|
87
|
+
if (!DEFAULT_ROLES.includes(role)) throw new AuthError('invalid_role');
|
|
88
|
+
const target = await targetFor(operatorId);
|
|
89
|
+
const actor = await actorFor(request, 'operators:role', target, role);
|
|
90
|
+
const fence = store.actorFence(actor, clock());
|
|
91
|
+
try {
|
|
92
|
+
const result = await store.run(`UPDATE oa_operators SET role=? WHERE id=? AND version=? AND ${fence.sql}`, [role, target.id, target.version, ...fence.args]);
|
|
93
|
+
if (result.meta.changes < 1) deny('authority_changed', 409);
|
|
94
|
+
} catch (e) { if (String(e).includes('last_owner')) deny('last_owner', 409); throw e; }
|
|
95
|
+
return store.operator(target.id);
|
|
96
|
+
},
|
|
97
|
+
async listSessions(request, operatorId) {
|
|
98
|
+
const current = await authenticate(request);
|
|
99
|
+
if (!current) deny('unauthenticated', 401);
|
|
100
|
+
const target = await targetFor(operatorId ?? current.operator.id);
|
|
101
|
+
const actor = await actorFor(request, 'sessions:list', target, undefined, false);
|
|
102
|
+
const now = clock();
|
|
103
|
+
const fence = store.actorFence(actor, now);
|
|
104
|
+
const rows = await store.statement(`SELECT s.id,s.kind,s.created_at AS createdAt,s.expires_at AS expiresAt,
|
|
105
|
+
CASE WHEN s.revoked_at IS NOT NULL OR s.operator_version!=o.version OR o.status!='active' OR s.expires_at<=? THEN 0 ELSE 1 END AS active
|
|
106
|
+
FROM oa_sessions s JOIN oa_operators o ON o.id=s.operator_id WHERE s.operator_id=? AND o.version=? AND ${fence.sql} ORDER BY s.created_at DESC,s.id`, [now, target.id, target.version, ...fence.args]).all();
|
|
107
|
+
return rows.results.map((r) => ({ ...r, active: !!r.active }));
|
|
108
|
+
},
|
|
109
|
+
async revokeSession(request, sessionId) {
|
|
110
|
+
const row = await store.first('SELECT operator_id AS operatorId FROM oa_sessions WHERE id=?', [sessionId]);
|
|
111
|
+
if (!row) deny('session_not_found', 404);
|
|
112
|
+
const target = await targetFor(row.operatorId);
|
|
113
|
+
const actor = await actorFor(request, 'sessions:revoke', target);
|
|
114
|
+
const now = clock();
|
|
115
|
+
const fence = store.actorFence(actor, now);
|
|
116
|
+
const result = await store.run(`UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE id=? AND EXISTS(SELECT 1 FROM oa_operators WHERE id=? AND version=?) AND ${fence.sql}`, [now, sessionId, target.id, target.version, ...fence.args]);
|
|
117
|
+
if (result.meta.changes < 1) deny('authority_changed', 409);
|
|
118
|
+
},
|
|
119
|
+
async listDomainRules(request) {
|
|
120
|
+
await actorFor(request, 'domains:manage', undefined, undefined, false);
|
|
121
|
+
return store.domainRules();
|
|
122
|
+
},
|
|
123
|
+
// An owner requires a provider for an email domain, or lifts the rule (provider null).
|
|
124
|
+
async setDomainRule(request, input) {
|
|
125
|
+
const actor = await actorFor(request, 'domains:manage');
|
|
126
|
+
const domain = typeof input?.domain === 'string' ? input.domain.trim().toLowerCase() : '';
|
|
127
|
+
if (!DOMAIN.test(domain)) throw new AuthError('invalid_domain');
|
|
128
|
+
const providerId = input.provider ?? null;
|
|
129
|
+
if (providerId !== null && !providers?.ids.includes(providerId)) throw new AuthError('unknown_provider');
|
|
130
|
+
await requireFreshProof(actor);
|
|
131
|
+
await store.setDomainRule({ domain, providerId, now: clock() }, actor);
|
|
132
|
+
return store.domainRules();
|
|
133
|
+
},
|
|
134
|
+
async listProviders(request) {
|
|
135
|
+
await actorFor(request, 'providers:manage', undefined, undefined, false);
|
|
136
|
+
return providers ? providers.links() : [];
|
|
137
|
+
},
|
|
138
|
+
async unlinkProvider(request, input) {
|
|
139
|
+
const actor = await actorFor(request, 'providers:manage');
|
|
140
|
+
if (!providers?.ids.includes(input?.provider)) throw new AuthError('unknown_provider');
|
|
141
|
+
await requireFreshProof(actor);
|
|
142
|
+
await providers.unlink(input.provider);
|
|
143
|
+
// A rule naming a provider that is gone would lock its domain out.
|
|
144
|
+
await store.liftDomainRules(input.provider);
|
|
145
|
+
return providers.links();
|
|
146
|
+
},
|
|
147
|
+
async relinkProvider(request, input) {
|
|
148
|
+
const actor = await actorFor(request, 'providers:manage');
|
|
149
|
+
if (!providers?.ids.includes(input?.provider)) throw new AuthError('unknown_provider');
|
|
150
|
+
await requireFreshProof(actor);
|
|
151
|
+
await providers.relink(input.provider);
|
|
152
|
+
return providers.links();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function displayName(value) {
|
|
157
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 120 || /[\x00-\x1f]/.test(value)) throw new AuthError('invalid_display_name');
|
|
158
|
+
return value.trim();
|
|
159
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { devAdmitted } from '../dev-admission.mjs';
|
|
2
|
+
import { opaqueToken } from '../crypto.mjs';
|
|
3
|
+
import { requirePost } from '../http.mjs';
|
|
4
|
+
import { deny } from '../errors.mjs';
|
|
5
|
+
// A product may expose mintCredential after selecting its scopes. Every use
|
|
6
|
+
// resolves the current person fence; older credentials with no fence refuse.
|
|
7
|
+
export function fencedCredentials({store,authenticate,requireFreshProof,config,clock,hash}) {
|
|
8
|
+
return {
|
|
9
|
+
async mintCredential(request,{expiresInSeconds=86400}={}) {
|
|
10
|
+
requirePost(request,config);const actor=await authenticate(request);if(!actor)deny('unauthenticated',401);
|
|
11
|
+
await requireFreshProof(actor);
|
|
12
|
+
if(!Number.isSafeInteger(expiresInSeconds)||expiresInSeconds<1||expiresInSeconds>365*86400)deny('invalid_input');
|
|
13
|
+
const token=opaqueToken(),id=crypto.randomUUID(),fence=store.actorFence(actor,clock());
|
|
14
|
+
const inserted=await store.run(`INSERT INTO oa_credentials(id,token_hash,operator_id,operator_version,kind,expires_at) SELECT ?,?,?,?,?,? WHERE ${fence.sql}`,
|
|
15
|
+
[id,await hash('credential',token),actor.operator.id,actor.operator.version,actor.kind,clock()+expiresInSeconds*1000,...fence.args]);
|
|
16
|
+
if(inserted.meta.changes!==1)deny('authority_changed',409);
|
|
17
|
+
return {id,token,expiresAt:clock()+expiresInSeconds*1000};
|
|
18
|
+
},
|
|
19
|
+
async authenticateCredential(request) {
|
|
20
|
+
const match=/^Bearer ([A-Za-z0-9_-]{40,100})$/.exec(request.headers.get('authorization')??'');if(!match)return null;
|
|
21
|
+
const row=await store.first(`SELECT c.id,c.operator_id AS personId,c.operator_version AS fence,c.kind,o.status,o.version
|
|
22
|
+
FROM oa_credentials c JOIN oa_operators o ON o.id=c.operator_id WHERE c.token_hash=? AND c.expires_at>? AND c.revoked_at IS NULL`,[await hash('credential',match[1]),clock()]);
|
|
23
|
+
if(!row || !Number.isSafeInteger(row.fence) || row.fence!==row.version || row.status!=='active' || row.kind==='dev' && !await devAdmitted(request))return null;
|
|
24
|
+
return {id:row.id,personId:row.personId,fence:row.fence,kind:row.kind};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { deny } from '../errors.mjs';
|
|
2
|
+
// Product adapters keep their own tables. The editor checks the current install
|
|
3
|
+
// owner and, for OIDC, the owner's current principal at this exact issuer.
|
|
4
|
+
export async function requireEditorOwner({request,getSession,accounts}) {
|
|
5
|
+
const session=await getSession(request),owner=await accounts.owner();
|
|
6
|
+
const personId=session?.operator?.id??session?.personId;
|
|
7
|
+
if(!session || !owner || owner.status!=='active' || personId!==owner.id)deny('owner_required',403);
|
|
8
|
+
if(session.kind==='oidc') {
|
|
9
|
+
if(!session.issuer || !session.subject || typeof accounts.currentBoundSubject!=='function' ||
|
|
10
|
+
await accounts.currentBoundSubject({personId:owner.id,issuer:session.issuer})!==session.subject)deny('owner_subject_required',403);
|
|
11
|
+
}
|
|
12
|
+
return session;
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AccountsAdapter, Action, Operator, PolicyInput, Role } from '../index.mjs';
|
|
2
|
+
export function accountsAdapter<T extends AccountsAdapter<string>>(adapter: T): T;
|
|
3
|
+
export const DEFAULT_ROLES: readonly Role[];
|
|
4
|
+
export const ACTIONS: readonly Action[];
|
|
5
|
+
export function defaultRolePolicy(input: PolicyInput): boolean;
|
|
6
|
+
export function isActiveOperator(operator: Operator | null | undefined, allowList?: string[]): boolean;
|
|
7
|
+
export function emailAllowed(email: string, allowList?: string[]): boolean;
|
|
8
|
+
|
|
9
|
+
export function requireEditorOwner<R extends string>(options:{request:Request;getSession:(request:Request)=>Promise<import('../index.mjs').Session<R>|null>;accounts:import('../index.mjs').AccountsAdapter<R>}):Promise<import('../index.mjs').Session<R>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Accounts: who a person is in this app and what they may do. Base's own
|
|
2
|
+
// implementation is the oa_ store; a seed with its own tables implements the
|
|
3
|
+
// accounts adapter interface instead (./adapter.mjs).
|
|
4
|
+
export { accountsAdapter } from './adapter.mjs';
|
|
5
|
+
export { createOaAccounts, oaSessions } from './oa.mjs';
|
|
6
|
+
export { AuthStore } from './store.mjs';
|
|
7
|
+
export { DEFAULT_ROLES, ACTIONS, defaultRolePolicy, isActiveOperator, emailAllowed } from './roles.mjs';
|
|
8
|
+
|
|
9
|
+
export { requireEditorOwner } from './editor-owner.mjs';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AccountsAdapter, AccountPerson, Operator, Role, Session, CompletedSession, AuthOptions, Principal, OperatorAuth } from '../index.mjs';
|
|
2
|
+
import type { NativeProofPersistence } from '../identity/email-link/index.mjs';
|
|
3
|
+
import type { NativeDependencyAuthority } from '../../dependencies/index.mjs';
|
|
4
|
+
export type NativePerson<R extends string = string> = AccountPerson & Operator<R>;
|
|
5
|
+
/** All reads and mutations are scoped to the tenant selected by server composition. */
|
|
6
|
+
export interface NativeAccountsAdapter<R extends string = string> extends AccountsAdapter<R> {
|
|
7
|
+
findByEmail(email: string): Promise<NativePerson<R> | null>;
|
|
8
|
+
findBinding(principal: {issuer: string; subject: string}): Promise<NativePerson<R> | null>;
|
|
9
|
+
findById(id: string): Promise<NativePerson<R> | null>;
|
|
10
|
+
getSession(request: Request, input: {now: number}): Promise<Session<R> | null>;
|
|
11
|
+
/** Revoke the request's native cookie token durably and return its native clearing cookie. */
|
|
12
|
+
signOut(request: Request, input: {now: number}): Promise<{cookie: string}>;
|
|
13
|
+
identityEnabled(): Promise<boolean>;
|
|
14
|
+
policyVersion(): Promise<number>;
|
|
15
|
+
requiredProvider(email: string): Promise<string | null>;
|
|
16
|
+
/** Atomically check person version/status/email, domain, binding and policy generation; check the provider pin when present (server-verified seams have none). Bind, revoke password credentials AND existing password sessions, and issue a <=24h native session. */
|
|
17
|
+
bindAndIssueSession(input: {person: NativePerson<R>; principal: Principal & {providerGeneration: number}; method: 'oidc'; returnTo?: string; request: Request}): Promise<CompletedSession<R> | null>;
|
|
18
|
+
/** Atomically check current version/status/domain policy and, for password, absence of any provider binding. */
|
|
19
|
+
issueLocalSession?(input: {request: Request; person: NativePerson<R>; kind: 'password' | 'review'; now: number; returnTo: string}): Promise<CompletedSession<R> | null>;
|
|
20
|
+
/** Atomically claim the native owner. Without supported native dependency setup provenance, close first-setup identity eligibility. */
|
|
21
|
+
bootstrap?(input: {request: Request; email: string; displayName: string; returnTo: string; now: number}): Promise<CompletedSession<R> | null>;
|
|
22
|
+
completeLocalSetup?(request: Request): Promise<{complete: boolean}>;
|
|
23
|
+
/** Atomically fence the current owner/session, audit, bump generation, and revoke all provider sessions when disabled. */
|
|
24
|
+
setIdentitySettings?(input: {actor: Session<R>; idpSignIn: boolean; now: number}): Promise<boolean>;
|
|
25
|
+
checkCredentialFence?(credential: {personId: string; fence: number; kind?: string}, input: {now: number}): Promise<boolean>;
|
|
26
|
+
dependencies?: NativeDependencyAuthority<R>;
|
|
27
|
+
}
|
|
28
|
+
export interface NativeAuthOptions<R extends string = string> extends Omit<AuthOptions<R>, 'rolePolicy'> {
|
|
29
|
+
accounts: NativeAccountsAdapter<R>;
|
|
30
|
+
proofs: NativeProofPersistence<R>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface NativeOperatorAuth<R extends string = string> extends Omit<OperatorAuth<R>, 'accounts'> {
|
|
34
|
+
readonly native: true;
|
|
35
|
+
readonly accounts: NativeAccountsAdapter<R>;
|
|
36
|
+
/** Explicitly refuses with native_capability_unavailable (501); no native setup provenance capability is supplied. */
|
|
37
|
+
authorizeDependencySetup(request: Request, input: {token?: string}): Promise<never>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Native composition never constructs an OA account/session store. The seed
|
|
2
|
+
// selects its tenant in these adapters, before any request is handled.
|
|
3
|
+
import { AuthError, deny } from '../errors.mjs';
|
|
4
|
+
import { accountsAdapter, person } from './adapter.mjs';
|
|
5
|
+
import { emailAllowed } from './roles.mjs';
|
|
6
|
+
import { requireEditorOwner } from './editor-owner.mjs';
|
|
7
|
+
import { requirePost, requireOrigin, requireLoopback } from '../http.mjs';
|
|
8
|
+
import { normalizeEmail, safeReturnTo } from '../policy.mjs';
|
|
9
|
+
import { hashToken, constantEqual } from '../crypto.mjs';
|
|
10
|
+
import { devAdmitted } from '../dev-admission.mjs';
|
|
11
|
+
import { createAdmission, legacyAdmission } from '../identity/admission.mjs';
|
|
12
|
+
import { createEmailLinkLifecycle } from '../identity/email-link/index.mjs';
|
|
13
|
+
import { createOidcProviders } from '../identity/oidc/index.mjs';
|
|
14
|
+
import { isLocalCli } from '../local-cli.mjs';
|
|
15
|
+
import { nativeDependencyAuthority } from '../../dependencies/native.mjs';
|
|
16
|
+
import { linkedProviders } from '../../dependencies/auth.mjs';
|
|
17
|
+
|
|
18
|
+
const ACCOUNT_METHODS = ['findById', 'getSession', 'signOut', 'policyVersion', 'identityEnabled', 'requiredProvider', 'bindAndIssueSession'];
|
|
19
|
+
const PROOF_METHODS = ['limits', 'token', 'preview', 'consumeLink', 'revokeLink', 'stepUpPreview', 'stepUpChallenge', 'consumeStepUp', 'revokeStepUp', 'steppedUp'];
|
|
20
|
+
const OPTIONAL = ['issueLocalSession', 'bootstrap', 'completeLocalSetup', 'setIdentitySettings', 'currentBoundSubject', 'checkCredentialFence'];
|
|
21
|
+
const unavailable = () => deny('native_capability_unavailable', 501);
|
|
22
|
+
function validate(value, required, optional = []) {
|
|
23
|
+
if (!value || typeof value !== 'object' || required.some(k => typeof value[k] !== 'function') || optional.some(k => value[k] !== undefined && typeof value[k] !== 'function')) throw new AuthError('invalid_native_adapter', 503);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createNativeAuth({ accounts, proofs, db, config, clock, hooks, mailSender, clientIp, fetch, waitUntil }) {
|
|
27
|
+
accountsAdapter(accounts);
|
|
28
|
+
validate(accounts, ACCOUNT_METHODS, OPTIONAL);
|
|
29
|
+
validate(proofs, PROOF_METHODS);
|
|
30
|
+
if (config.dependencies || accounts.dependencies !== undefined) nativeDependencyAuthority(accounts.dependencies);
|
|
31
|
+
const hash = (purpose, token) => hashToken(config.SESSION_SECRET, purpose, token);
|
|
32
|
+
const visible = value => {
|
|
33
|
+
const p = person(value);
|
|
34
|
+
if (p && (!Number.isSafeInteger(p.version) || p.version < 0 || typeof p.role !== 'string' || !p.role)) throw new AuthError('invalid_native_adapter', 503);
|
|
35
|
+
return p && emailAllowed(p.email, config.allowList) ? p : null;
|
|
36
|
+
};
|
|
37
|
+
// Filtering is shared; the adapter must repeat these authority conditions in
|
|
38
|
+
// every mutation transaction (a read here alone cannot fence a racing change).
|
|
39
|
+
const admittedAccounts = Object.fromEntries(['findByEmail', 'findBinding', 'bind', 'issueSession', 'revokeSession', 'owner', 'bindAndIssueSession', 'acceptInvitation', 'requiredProvider', 'currentBoundSubject', 'identityEnabled'].filter(key => typeof accounts[key] === 'function').map(key => [key, (...args) => accounts[key](...args)]));
|
|
40
|
+
admittedAccounts.identityEnabled = async () => await accounts.identityEnabled() === true;
|
|
41
|
+
admittedAccounts.findByEmail = async email => visible(await accounts.findByEmail(email));
|
|
42
|
+
admittedAccounts.findBinding = async principal => visible(await accounts.findBinding(principal));
|
|
43
|
+
const admission = createAdmission({ accounts: admittedAccounts, config, clock, hooks });
|
|
44
|
+
async function authenticate(request) {
|
|
45
|
+
requireOrigin(request, config);
|
|
46
|
+
const session = await accounts.getSession(request, { now: clock() });
|
|
47
|
+
if (!session) return null;
|
|
48
|
+
if (!session.operator || session.operator.status !== 'active' || !emailAllowed(session.operator.email, config.allowList)
|
|
49
|
+
|| !Number.isSafeInteger(session.operator.version) || !Number.isFinite(session.expiresAt) || session.expiresAt <= clock()) return null;
|
|
50
|
+
if (session.kind === 'oidc' && (!Number.isFinite(session.createdAt) || clock() >= session.createdAt + 86400000 || (await accounts.identityEnabled()) !== true)) return null;
|
|
51
|
+
if (session.kind === 'dev' && !await devAdmitted(request)) return null;
|
|
52
|
+
if (session.kind === 'review') {
|
|
53
|
+
requireLoopback(request);
|
|
54
|
+
if (config.review?.enabled !== true || session.operator.email !== config.review.operatorEmail) return null;
|
|
55
|
+
}
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
const email = createEmailLinkLifecycle({ config, clock, mailSender, clientIp, hash, hooks: { beforeMagicLink: hooks.beforeMagicLink }, afterConfirm: admission.afterConfirm, persistence: {
|
|
59
|
+
limits: (...a) => proofs.limits(...a), authenticate, requiredProvider: email => accounts.requiredProvider(email),
|
|
60
|
+
byEmail: async email => visible(await accounts.findByEmail(email)),
|
|
61
|
+
token: input => proofs.token(input), revokeLink: (...a) => proofs.revokeLink(...a),
|
|
62
|
+
async authorizedLink(request, digest) {
|
|
63
|
+
const link = await proofs.preview(digest, clock());
|
|
64
|
+
if (!link || !emailAllowed(link.email, config.allowList)) return null;
|
|
65
|
+
const current = visible(await accounts.findById(link.operatorId));
|
|
66
|
+
if (!current || current.status !== 'active' || current.version !== link.version || current.email !== link.email) return null;
|
|
67
|
+
if (link.dev && !await devAdmitted(request)) return null;
|
|
68
|
+
return { link, person: current };
|
|
69
|
+
},
|
|
70
|
+
async consumeLink({ request, digest, approval, now }) {
|
|
71
|
+
await admission.requireRoute(approval.person);
|
|
72
|
+
return proofs.consumeLink({ request, digest, person: approval.person, now, returnTo: safeReturnTo(approval.link.returnTo) });
|
|
73
|
+
},
|
|
74
|
+
stepUpPreview: (...a) => proofs.stepUpPreview(...a), stepUpChallenge: (...a) => proofs.stepUpChallenge(...a),
|
|
75
|
+
consumeStepUp: (...a) => proofs.consumeStepUp(...a), revokeStepUp: (...a) => proofs.revokeStepUp(...a), steppedUp: (...a) => proofs.steppedUp(...a),
|
|
76
|
+
} });
|
|
77
|
+
const policyVersion = async () => {
|
|
78
|
+
const value = await accounts.policyVersion();
|
|
79
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new AuthError('invalid_native_adapter', 503);
|
|
80
|
+
return value;
|
|
81
|
+
};
|
|
82
|
+
const enabled = admittedAccounts.identityEnabled;
|
|
83
|
+
const staticProviders = config.providers.length ? createOidcProviders({ admission, db, clientIp, clock, waitUntil, policyVersion, enabled, ...(fetch ? { fetch } : {}),
|
|
84
|
+
config: { origin: config.origin, local: config.local, transactionSecret: config.OIDC_TRANSACTION_SECRET, providers: config.providers,
|
|
85
|
+
signInPath: config.branding.signInPath, timeoutMs: config.identityTimeoutMs, startLimit: config.identityStartLimit } }) : null;
|
|
86
|
+
const providers = linkedProviders({ db, config, admission, clientIp, clock, waitUntil, fetch, staticProviders, policyVersion, enabled, native: accounts.dependencies });
|
|
87
|
+
async function owner(request, post = false) {
|
|
88
|
+
if (post) requirePost(request, config);
|
|
89
|
+
const actor = await authenticate(request), current = await accounts.owner();
|
|
90
|
+
if (!actor) deny('unauthenticated', 401);
|
|
91
|
+
if (!current || current.status !== 'active' || actor.operator.id !== current.id) deny('owner_required', 403);
|
|
92
|
+
return actor;
|
|
93
|
+
}
|
|
94
|
+
async function localSession(request, id, kind, returnTo) {
|
|
95
|
+
if (!accounts.issueLocalSession) unavailable();
|
|
96
|
+
const current = visible(await accounts.findById(id));
|
|
97
|
+
if (!current || current.status !== 'active') deny();
|
|
98
|
+
await admission.requireRoute(current);
|
|
99
|
+
const result = await accounts.issueLocalSession({ request, person: current, kind, now: clock(), returnTo: safeReturnTo(returnTo) });
|
|
100
|
+
if (!result) deny('invalid_or_expired', 400);
|
|
101
|
+
return { ...result, returnTo: safeReturnTo(result.returnTo) };
|
|
102
|
+
}
|
|
103
|
+
async function getSession(request) {
|
|
104
|
+
const actor = await authenticate(request);
|
|
105
|
+
if (!actor) return null;
|
|
106
|
+
const { hash: _, ...session } = actor;
|
|
107
|
+
return session;
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
// Account administration belongs to the seed; unsupported operations are
|
|
111
|
+
// explicit refusals, never apparently successful writes to an unused store.
|
|
112
|
+
...Object.fromEntries(['invite', 'listOperators', 'cancelInvitation', 'setOperatorStatus', 'setRole', 'listSessions', 'revokeSession',
|
|
113
|
+
'listDomainRules', 'setDomainRule', 'unlinkProvider', 'relinkProvider', 'mintCredential', 'authenticateCredential'].map(k => [k, unavailable])),
|
|
114
|
+
native: true, presentation: config.branding, accounts, admission, providers,
|
|
115
|
+
requestMagicLink: email.requestMagicLink, previewLink: email.previewLink, confirmLink: email.confirmLink,
|
|
116
|
+
requestStepUp: email.requestStepUp, requireStepUp: email.requireStepUp,
|
|
117
|
+
...legacyAdmission({ admission, config, policyVersion }),
|
|
118
|
+
getSession,
|
|
119
|
+
async authorizeDependencySetup() { unavailable(); },
|
|
120
|
+
async signOut(request) { requirePost(request, config); return accounts.signOut(request, { now: clock() }); },
|
|
121
|
+
async bootstrap(request, { token, displayName, returnTo }) {
|
|
122
|
+
requirePost(request, config);
|
|
123
|
+
if (!accounts.bootstrap) unavailable();
|
|
124
|
+
await email.limit(request, config.OWNER_EMAIL, 'bootstrap');
|
|
125
|
+
if (!config.SETUP_TOKEN || typeof token !== 'string' || token.length > 1024 || !constantEqual(await hash('setup', token), await hash('setup', config.SETUP_TOKEN))) deny('invalid_setup_token', 403);
|
|
126
|
+
if (!emailAllowed(config.OWNER_EMAIL, config.allowList)) deny('email_not_allowed');
|
|
127
|
+
const result = await accounts.bootstrap({ request, email: config.OWNER_EMAIL, displayName, returnTo: safeReturnTo(returnTo), now: clock() });
|
|
128
|
+
if (!result) deny('already_initialized', 409);
|
|
129
|
+
return { ...result, returnTo: safeReturnTo(result.returnTo) };
|
|
130
|
+
},
|
|
131
|
+
async completeLocalSetup(request) {
|
|
132
|
+
requirePost(request, config);
|
|
133
|
+
if (!accounts.completeLocalSetup) unavailable();
|
|
134
|
+
if (!await isLocalCli(request, config) || !await accounts.owner()) deny('owner_required', 403);
|
|
135
|
+
return accounts.completeLocalSetup(request);
|
|
136
|
+
},
|
|
137
|
+
async issueReviewSession(request, { returnTo } = {}) {
|
|
138
|
+
requireLoopback(request); requirePost(request, config);
|
|
139
|
+
if (config.review?.enabled !== true) deny('review_disabled', 403);
|
|
140
|
+
const emailAddress = normalizeEmail(config.review.operatorEmail);
|
|
141
|
+
await email.limit(request, emailAddress, 'review');
|
|
142
|
+
const current = visible(await accounts.findByEmail(emailAddress));
|
|
143
|
+
if (!current) deny();
|
|
144
|
+
return localSession(request, current.id, 'review', returnTo);
|
|
145
|
+
},
|
|
146
|
+
async admitVerifiedPassword(request, id, { returnTo } = {}) {
|
|
147
|
+
requirePost(request, config); return localSession(request, id, 'password', returnTo);
|
|
148
|
+
},
|
|
149
|
+
async identitySettings(request) { await owner(request); return { idpSignIn: await enabled() }; },
|
|
150
|
+
async setIdentitySettings(request, { idpSignIn }) {
|
|
151
|
+
const actor = await owner(request, true);
|
|
152
|
+
if (typeof idpSignIn !== 'boolean') deny('invalid_input', 400);
|
|
153
|
+
if (!accounts.setIdentitySettings) unavailable();
|
|
154
|
+
if (!await accounts.setIdentitySettings({ actor, idpSignIn, now: clock() })) deny('authority_changed', 409);
|
|
155
|
+
return { idpSignIn };
|
|
156
|
+
},
|
|
157
|
+
async listProviders(request) { await owner(request); return providers ? providers.links() : []; },
|
|
158
|
+
requireEditorOwner: request => requireEditorOwner({ request, getSession, accounts }),
|
|
159
|
+
async credentialFence(request) {
|
|
160
|
+
const actor = await authenticate(request);
|
|
161
|
+
if (!actor) deny('unauthenticated', 401);
|
|
162
|
+
if (!accounts.checkCredentialFence) unavailable();
|
|
163
|
+
return { personId: actor.operator.id, fence: actor.operator.version, kind: actor.kind };
|
|
164
|
+
},
|
|
165
|
+
async checkCredentialFence(credential, request) {
|
|
166
|
+
if (!accounts.checkCredentialFence) unavailable();
|
|
167
|
+
if (!credential || !Number.isSafeInteger(credential.fence) || credential.kind === 'dev' && (!request || !await devAdmitted(request))) return false;
|
|
168
|
+
return accounts.checkCredentialFence(credential, { now: clock() });
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Base's own accounts: operators, roles, invitations and sessions in the oa_
|
|
2
|
+
// store, behind the accounts adapter interface (./adapter.mjs).
|
|
3
|
+
import { deny } from '../errors.mjs';
|
|
4
|
+
import { opaqueToken, hashToken } from '../crypto.mjs';
|
|
5
|
+
import { safeReturnTo, normalizeEmail, emailDomain } from '../policy.mjs';
|
|
6
|
+
import { requireOrigin, requireLoopback, readCookie, sessionCookie } from '../http.mjs';
|
|
7
|
+
import { devAdmitted } from '../dev-admission.mjs';
|
|
8
|
+
import { emailAllowed, isActiveOperator } from './roles.mjs';
|
|
9
|
+
|
|
10
|
+
export function oaSessions({ config, store, clock }) {
|
|
11
|
+
const hash = (purpose, token) => hashToken(config.SESSION_SECRET, purpose, token);
|
|
12
|
+
async function newSession(kind = 'native') {
|
|
13
|
+
const token = opaqueToken();
|
|
14
|
+
const seconds = kind === 'review' ? config.reviewSeconds : kind === 'oidc' ? Math.min(config.sessionSeconds, 86400) : config.sessionSeconds;
|
|
15
|
+
return { id: crypto.randomUUID(), token, hash: await hash('session', token), expiresAt: clock() + seconds * 1000 };
|
|
16
|
+
}
|
|
17
|
+
function completed(session, record, returnTo) {
|
|
18
|
+
if (!record) deny('invalid_or_expired', 400);
|
|
19
|
+
return { session: { id: record.id, kind: record.kind, createdAt: record.createdAt, expiresAt: record.expiresAt }, operator: record.operator,
|
|
20
|
+
cookie: sessionCookie(session.token, config, Math.max(0, Math.floor((record.expiresAt - clock()) / 1000))), returnTo: safeReturnTo(returnTo) };
|
|
21
|
+
}
|
|
22
|
+
async function authenticate(request) {
|
|
23
|
+
requireOrigin(request, config);
|
|
24
|
+
const token = readCookie(request, config);
|
|
25
|
+
if (!token) return null;
|
|
26
|
+
const digest = await hash('session', token);
|
|
27
|
+
const session = await store.session(digest, clock());
|
|
28
|
+
if (!session || !isActiveOperator(session.operator, config.allowList)) return null;
|
|
29
|
+
// A dev session counts only behind the local key gate, on every request.
|
|
30
|
+
if (session.kind === 'dev' && !await devAdmitted(request)) return null;
|
|
31
|
+
if (session.kind === 'review') {
|
|
32
|
+
requireLoopback(request);
|
|
33
|
+
if (config.review?.enabled !== true || session.operator.email !== normalizeEmail(config.review.operatorEmail)) return null;
|
|
34
|
+
}
|
|
35
|
+
return { ...session, hash: digest };
|
|
36
|
+
}
|
|
37
|
+
return { hash, newSession, completed, authenticate };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function personOf(operator, ownerFlag = operator.role === 'owner' && operator.status === 'active') {
|
|
41
|
+
return { id: operator.id, email: operator.email, status: operator.status, owner: ownerFlag, role: operator.role, version: operator.version };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createOaAccounts({ config, store, clock, sessions, rolePolicy }) {
|
|
45
|
+
const visible = operator => operator && emailAllowed(operator.email, config.allowList) ? personOf(operator) : null;
|
|
46
|
+
return {
|
|
47
|
+
async identityEnabled() { return !!(await store.first('SELECT idp_sign_in FROM oa_auth_settings WHERE singleton=1'))?.idp_sign_in; },
|
|
48
|
+
async currentBoundSubject({ personId, issuer }) {
|
|
49
|
+
const rows = (await store.statement('SELECT subject FROM oa_external_identities WHERE operator_id=? AND issuer=?', [personId, issuer]).all()).results;
|
|
50
|
+
return rows.length === 1 ? rows[0].subject : null;
|
|
51
|
+
},
|
|
52
|
+
async findByEmail(email) { return visible(await store.byEmail(email)); },
|
|
53
|
+
async findBinding({ issuer, subject }) { return visible(await store.binding(issuer, subject)); },
|
|
54
|
+
async bind({ issuer, subject, person, associated }) {
|
|
55
|
+
return store.bind({ issuer, subject, operatorId: person.id, expectedVersion: person.version, associated, now: clock() });
|
|
56
|
+
},
|
|
57
|
+
async issueSession({ person, principal, returnTo }) {
|
|
58
|
+
const session = await sessions.newSession('oidc');
|
|
59
|
+
const record = await store.issueBound({ issuer: principal.issuer, subject: principal.subject, sid: principal.sid ?? null, expectedGeneration: principal.providerGeneration ?? null, providerPin: principal.providerPin, operatorId: person.id, expectedVersion: person.version, session, now: clock() });
|
|
60
|
+
return record ? sessions.completed(session, record, returnTo) : null;
|
|
61
|
+
},
|
|
62
|
+
async bindAndIssueSession({ person, principal, returnTo }) {
|
|
63
|
+
const session = await sessions.newSession('oidc');
|
|
64
|
+
const record = await store.issueBound({ issuer: principal.issuer, subject: principal.subject, sid: principal.sid ?? null, associated: principal.associatedEmail !== undefined, establish: true, expectedGeneration: principal.providerGeneration ?? null, providerPin: principal.providerPin, operatorId: person.id, expectedVersion: person.version, session, now: clock() });
|
|
65
|
+
return record ? sessions.completed(session, record, returnTo) : null;
|
|
66
|
+
},
|
|
67
|
+
async acceptInvitation({ person, principal, returnTo }) {
|
|
68
|
+
const invitation = await store.liveInvitation(person.id, clock());
|
|
69
|
+
if (!invitation) return null;
|
|
70
|
+
const inviter = await store.operator(invitation.issuerId);
|
|
71
|
+
if (!isActiveOperator(inviter, config.allowList) || inviter.version !== invitation.issuerVersion
|
|
72
|
+
|| !await rolePolicy({ actor: inviter, action: 'operators:invite', role: person.role })) return null;
|
|
73
|
+
const session = await sessions.newSession('oidc');
|
|
74
|
+
const record = await store.acceptByBinding({ issuer: principal.issuer, subject: principal.subject, sid: principal.sid ?? null, expectedGeneration: principal.providerGeneration ?? null, providerPin: principal.providerPin, tokenId: invitation.id, operatorId: person.id,
|
|
75
|
+
expectedVersion: person.version, inviter, session, now: clock() });
|
|
76
|
+
return record ? sessions.completed(session, record, returnTo ?? invitation.returnTo) : null;
|
|
77
|
+
},
|
|
78
|
+
async revokeSession(sessionId) {
|
|
79
|
+
await store.run('UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE id=?', [clock(), sessionId]);
|
|
80
|
+
},
|
|
81
|
+
async owner() {
|
|
82
|
+
const row = await store.first(`SELECT o.id,o.email,o.display_name AS displayName,o.role,o.status,o.version,o.created_at AS createdAt FROM oa_operators o
|
|
83
|
+
LEFT JOIN oa_bootstrap b ON b.claim_id=o.id WHERE o.role='owner' AND o.status='active' ORDER BY b.claim_id IS NULL, o.created_at, o.id LIMIT 1`);
|
|
84
|
+
return row ? personOf(row) : null;
|
|
85
|
+
},
|
|
86
|
+
async requiredProvider(email) {
|
|
87
|
+
return (await store.domainRule(emailDomain(email)))?.providerId ?? null;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { devAdmitted } from '../dev-admission.mjs';
|
|
2
|
+
import { deny } from '../errors.mjs';
|
|
3
|
+
import { requirePost } from '../http.mjs';
|
|
4
|
+
|
|
5
|
+
export function providerControl({ store, accounts, authenticate, config, clock }) {
|
|
6
|
+
async function owner(request, post = false) {
|
|
7
|
+
if (post) requirePost(request, config);
|
|
8
|
+
const actor = await authenticate(request), current = await accounts.owner();
|
|
9
|
+
if (!actor) deny('unauthenticated', 401);
|
|
10
|
+
if (!current || current.id !== actor.operator.id) deny('owner_required', 403);
|
|
11
|
+
return actor;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
async identitySettings(request) { await owner(request); return { idpSignIn: await accounts.identityEnabled() }; },
|
|
15
|
+
async setIdentitySettings(request, { idpSignIn }) {
|
|
16
|
+
const actor = await owner(request, true);
|
|
17
|
+
if (typeof idpSignIn !== 'boolean') deny('invalid_input', 400);
|
|
18
|
+
const fence = store.actorFence(actor, clock());
|
|
19
|
+
const currentOwner = `?=(SELECT o.id FROM oa_operators o LEFT JOIN oa_bootstrap b ON b.claim_id=o.id WHERE o.role='owner' AND o.status='active' ORDER BY b.claim_id IS NULL,o.created_at,o.id LIMIT 1)`;
|
|
20
|
+
const auditId=crypto.randomUUID();
|
|
21
|
+
const results = await store.batch([
|
|
22
|
+
[`INSERT INTO oa_auth_settings(singleton,idp_sign_in,generation,first_setup) SELECT 1,0,0,0 WHERE ${fence.sql} AND ${currentOwner} ON CONFLICT(singleton) DO UPDATE SET first_setup=0`, [...fence.args, actor.operator.id]],
|
|
23
|
+
[`INSERT INTO oa_auth_settings(singleton,idp_sign_in,generation) SELECT 1,?,1 WHERE ${fence.sql} AND ${currentOwner} AND NOT EXISTS(SELECT 1 FROM oa_auth_settings WHERE singleton=1 AND idp_sign_in=?)
|
|
24
|
+
ON CONFLICT(singleton) DO UPDATE SET idp_sign_in=excluded.idp_sign_in,generation=oa_auth_settings.generation+1,first_setup=0`, [idpSignIn ? 1 : 0, ...fence.args, actor.operator.id, idpSignIn ? 1 : 0]],
|
|
25
|
+
[`INSERT INTO oa_auth_audit(id,actor_id,action,value,created_at) SELECT ?,?,'idpSignIn',?,? WHERE changes()=1`, [auditId, actor.operator.id, String(idpSignIn), clock()]],
|
|
26
|
+
[`UPDATE oa_sessions SET revoked_at=COALESCE(revoked_at,?) WHERE kind='oidc' AND EXISTS(SELECT 1 FROM oa_auth_audit WHERE id=? AND value='false')`, [clock(), auditId]],
|
|
27
|
+
]);
|
|
28
|
+
if (results[0].meta.changes !== 1 && results[1].meta.changes !== 1) deny('authority_changed', 409);
|
|
29
|
+
return { idpSignIn };
|
|
30
|
+
},
|
|
31
|
+
async requireEditorOwner(request) {
|
|
32
|
+
const actor = await owner(request);
|
|
33
|
+
if (actor.kind === 'oidc') {
|
|
34
|
+
if (!actor.issuer || !actor.subject || await accounts.currentBoundSubject({ personId: actor.operator.id, issuer: actor.issuer }) !== actor.subject) deny('owner_subject_required', 403);
|
|
35
|
+
}
|
|
36
|
+
const { hash, ...session } = actor;
|
|
37
|
+
return session;
|
|
38
|
+
},
|
|
39
|
+
async credentialFence(request) {
|
|
40
|
+
const actor = await authenticate(request);
|
|
41
|
+
if (!actor) deny('unauthenticated', 401);
|
|
42
|
+
return { personId: actor.operator.id, fence: actor.operator.version, kind: actor.kind };
|
|
43
|
+
},
|
|
44
|
+
async checkCredentialFence(credential,request) {
|
|
45
|
+
if (!credential || !Number.isSafeInteger(credential.fence)) return false;
|
|
46
|
+
const person = await store.operator(credential.personId);
|
|
47
|
+
return !!person && person.status === 'active' && person.version === credential.fence && (credential.kind !== 'dev' || request && await devAdmitted(request));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Only protocol-created principals carry a pin. External admission works without
|
|
2
|
+
// the OIDC protocol tables; a pinned callback repeats this fence in its batch.
|
|
3
|
+
export function providerPinFence(pin) {
|
|
4
|
+
return pin ? {
|
|
5
|
+
sql: `EXISTS(SELECT 1 FROM oa_identity_providers WHERE provider_id=? AND status='linked' AND issuer=? AND client_id=? AND linked_at=?)`,
|
|
6
|
+
args: [pin.id, pin.issuer, pin.clientId, pin.linkedAt],
|
|
7
|
+
} : { sql: '1', args: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordProviderPin(pin, sessionId) {
|
|
11
|
+
return pin ? [[`INSERT INTO oa_provider_session_pins(session_id,provider_id,issuer,client_id,linked_at)
|
|
12
|
+
SELECT ?,?,?,?,? WHERE EXISTS(SELECT 1 FROM oa_sessions WHERE id=?)`,
|
|
13
|
+
[sessionId, pin.id, pin.issuer, pin.clientId, pin.linkedAt, sessionId]]] : [];
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Roles and the pure authorization policy of Base's own accounts. A product can
|
|
2
|
+
// replace defaultRolePolicy, but never the active-session guard or the
|
|
3
|
+
// database's final-owner protection.
|
|
4
|
+
export const DEFAULT_ROLES = Object.freeze(['owner', 'admin', 'member']);
|
|
5
|
+
export const ACTIONS = Object.freeze(['operators:list', 'operators:invite', 'operators:deactivate', 'operators:reactivate', 'operators:role',
|
|
6
|
+
'sessions:list', 'sessions:revoke', 'providers:manage', 'domains:manage']);
|
|
7
|
+
export function emailAllowed(email, allowList) {
|
|
8
|
+
return allowList === undefined || allowList.includes(email);
|
|
9
|
+
}
|
|
10
|
+
export function isActiveOperator(operator, allowList) {
|
|
11
|
+
return !!operator && operator.status === 'active' && emailAllowed(operator.email, allowList);
|
|
12
|
+
}
|
|
13
|
+
// Default applied: owner/admin/member. Provider links and domain rules are the
|
|
14
|
+
// owner's alone.
|
|
15
|
+
export function defaultRolePolicy({ actor, action, target, role }) {
|
|
16
|
+
if (!isActiveOperator(actor) || !ACTIONS.includes(action)) return false;
|
|
17
|
+
if (action === 'sessions:list' || action === 'sessions:revoke') {
|
|
18
|
+
if (target?.id === actor.id) return true;
|
|
19
|
+
}
|
|
20
|
+
if (actor.role === 'owner') return true;
|
|
21
|
+
if (actor.role !== 'admin' || action === 'providers:manage' || action === 'domains:manage') return false;
|
|
22
|
+
if (action === 'operators:list') return true;
|
|
23
|
+
if (action === 'operators:invite') return role === 'member';
|
|
24
|
+
if (target?.role !== 'member') return false;
|
|
25
|
+
return action !== 'operators:role' || role === 'member';
|
|
26
|
+
}
|