@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,144 @@
|
|
|
1
|
+
# Vite SPA adoption
|
|
2
|
+
|
|
3
|
+
Keep the auth module, D1 access, sender construction, and every credential in the
|
|
4
|
+
Worker's server entry. The browser imports no auth module and never receives
|
|
5
|
+
`OWNER_EMAIL`, `SESSION_SECRET`, `SETUP_TOKEN`, `MAIL_*`, D1 bindings, or raw
|
|
6
|
+
confirmation tokens. Serve the SPA and API from the same canonical origin so
|
|
7
|
+
its HTTP-only session cookie is sent naturally.
|
|
8
|
+
|
|
9
|
+
## Worker API
|
|
10
|
+
|
|
11
|
+
This Hono example uses the actual `createOperatorAuth` and
|
|
12
|
+
`createAuthHandler` entry points. `createMailSender` is the real IF-056 factory
|
|
13
|
+
from the selected `modules/mail/index.mjs`; production configuration must include
|
|
14
|
+
the provider's real server-side credentials.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// src/server.ts
|
|
18
|
+
import { Hono, type Context, type Next } from 'hono'
|
|
19
|
+
import { env } from 'cloudflare:workers'
|
|
20
|
+
import { createMailSender } from '../modules/mail/index.mjs'
|
|
21
|
+
import { createOperatorAuth, createAuthHandler, type Session } from '../modules/operator-auth/index.mjs'
|
|
22
|
+
|
|
23
|
+
const origin = 'https://app.example.com'
|
|
24
|
+
// Secrets are Worker secret bindings, never source text.
|
|
25
|
+
const { SESSION_SECRET, SETUP_TOKEN, MAIL_API_KEY: apiKey } = env
|
|
26
|
+
const mailSender = env.MAIL_PROVIDER && apiKey && env.MAIL_FROM
|
|
27
|
+
? createMailSender({
|
|
28
|
+
provider: env.MAIL_PROVIDER,
|
|
29
|
+
mode: 'production',
|
|
30
|
+
apiKey,
|
|
31
|
+
...(env.MAIL_ACCOUNT_ID ? { accountId: env.MAIL_ACCOUNT_ID } : {}),
|
|
32
|
+
})
|
|
33
|
+
: undefined
|
|
34
|
+
const auth = createOperatorAuth({
|
|
35
|
+
db: env.DB,
|
|
36
|
+
config: {
|
|
37
|
+
origin,
|
|
38
|
+
OWNER_EMAIL: env.OWNER_EMAIL,
|
|
39
|
+
SESSION_SECRET,
|
|
40
|
+
SETUP_TOKEN,
|
|
41
|
+
...(env.MAIL_FROM ? { MAIL_FROM: env.MAIL_FROM } : {}),
|
|
42
|
+
},
|
|
43
|
+
mailSender,
|
|
44
|
+
// This is trusted only because Cloudflare is the public Worker boundary.
|
|
45
|
+
clientIp: (request) => request.headers.get('CF-Connecting-IP') ?? '',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const handleAuth = createAuthHandler(auth)
|
|
49
|
+
const app = new Hono<{ Variables: { operatorSession: Session } }>()
|
|
50
|
+
|
|
51
|
+
app.all('/auth/*', async (context) => {
|
|
52
|
+
const response = await handleAuth(context.req.raw)
|
|
53
|
+
return response ?? context.notFound()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const protectPrivate = async (context: Context<{ Variables: { operatorSession: Session } }>, next: Next) => {
|
|
57
|
+
const session = await auth.getSession(context.req.raw)
|
|
58
|
+
if (!session) return context.json({ error: 'unauthenticated' }, 401)
|
|
59
|
+
context.set('operatorSession', session)
|
|
60
|
+
await next()
|
|
61
|
+
}
|
|
62
|
+
app.use('/api/private', protectPrivate)
|
|
63
|
+
app.use('/api/private/*', protectPrivate)
|
|
64
|
+
|
|
65
|
+
app.get('/api/private/profile', (context) => {
|
|
66
|
+
const session = context.get('operatorSession')
|
|
67
|
+
return context.json({ operator: { id: session.operator.id, role: session.operator.role } })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export default { fetch: (request: Request) => app.fetch(request, env) }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Keep auth dispatch ahead of protected route matching so private unknown routes
|
|
74
|
+
are gated before their 404. For browser mutations, check exact Origin and
|
|
75
|
+
Fetch Metadata, validate input, and call `getSession(context.req.raw)` before
|
|
76
|
+
the product's real persistence operation.
|
|
77
|
+
`getSession(request)` must protect each private API request independently; a
|
|
78
|
+
client-side route guard or an earlier profile request does not authorize another
|
|
79
|
+
request.
|
|
80
|
+
|
|
81
|
+
When first-owner setup is pending, omit `mailSender` and `MAIL_FROM` from the
|
|
82
|
+
auth construction; bootstrap needs neither. Keep `SETUP_TOKEN` in the
|
|
83
|
+
server-only setup operation and invoke POST `/auth/bootstrap` there. Never embed
|
|
84
|
+
the token in the SPA bundle, form, or browser request. The endpoint creates no
|
|
85
|
+
product data, only the configured owner and its session. Remove the token binding
|
|
86
|
+
after verifying first-owner access.
|
|
87
|
+
|
|
88
|
+
## SPA sign-in and private requests
|
|
89
|
+
|
|
90
|
+
The sign-in page requests a link; it does not receive one. The public response is
|
|
91
|
+
uniform for unknown or disallowed addresses. The browser never gets the token
|
|
92
|
+
from the email and cannot consume a link through GET or HEAD: the confirmation
|
|
93
|
+
page posts its hidden value only when the operator presses Confirm.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export async function requestOperatorLink(email: string) {
|
|
97
|
+
const response = await fetch('/auth/magic-link', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
credentials: 'same-origin',
|
|
100
|
+
headers: { 'content-type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ email, returnTo: '/app' }),
|
|
102
|
+
})
|
|
103
|
+
if (response.status !== 202) throw new Error('Sign-in is unavailable; try again later.')
|
|
104
|
+
return 'If this address is eligible, a sign-in link will arrive shortly.'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function loadOperatorProfile() {
|
|
108
|
+
const response = await fetch('/api/private/profile', { credentials: 'same-origin' })
|
|
109
|
+
if (response.status === 401) {
|
|
110
|
+
window.location.assign('/signin')
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
if (!response.ok) throw new Error('The operator profile could not be loaded.')
|
|
114
|
+
return response.json() as Promise<{ operator: { id: string; role: 'owner' | 'admin' | 'member' } }>
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The same-origin cookie is `HttpOnly`, `SameSite=Lax`, and `Secure` in production;
|
|
119
|
+
JavaScript does not read or write it. The auth handler enforces POST and exact
|
|
120
|
+
Origin for its mutations. The product's own state-changing APIs keep their
|
|
121
|
+
Origin/Fetch-Metadata checks too. Never use a client role or cached profile as
|
|
122
|
+
the server's authorization decision.
|
|
123
|
+
|
|
124
|
+
## Real sender outcomes and OIDC
|
|
125
|
+
|
|
126
|
+
Pass the actual `createMailSender` result directly to the auth core. An
|
|
127
|
+
`accepted` result records provider submission only. `rejected`, `rate_limited`,
|
|
128
|
+
`uncertain`, and production `development` results revoke the new link and become
|
|
129
|
+
`503 mail_unavailable`; there is no fallback or automatic retry. Resolve an
|
|
130
|
+
uncertain attempt before requesting another link.
|
|
131
|
+
|
|
132
|
+
Optional OIDC remains server-side: configure one exact HTTPS issuer, use the
|
|
133
|
+
same `auth` and D1 for `createOidcClient`, dispatch its handler before
|
|
134
|
+
`createAuthHandler`, and forward every Set-Cookie header. The client constructs
|
|
135
|
+
its trusted admission closure from `auth.createIdentityAdmission({ issuer,
|
|
136
|
+
subjectEmails })`; custom auth adapters must implement the same server-only
|
|
137
|
+
contract. An existing issuer/subject binding may sign in again, while a
|
|
138
|
+
different principal cannot join an already-bound operator by presenting the same
|
|
139
|
+
verified email. Configure any subject-to-existing-email association from trusted
|
|
140
|
+
server configuration, never callback or browser input. Do not expose its
|
|
141
|
+
provider response, tokens or callback query string. The conformance suite's
|
|
142
|
+
optional provider-key-free case exercises only a first verified identity
|
|
143
|
+
admission into the shared native session store; the separate OIDC tests prove
|
|
144
|
+
the protocol.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class AuthError extends Error {
|
|
2
|
+
constructor(code, status = 400) {
|
|
3
|
+
super(code);
|
|
4
|
+
this.name = 'AuthError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function deny(code = 'forbidden', status = 403) { throw new AuthError(code, status); }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { deny } from './errors.mjs';
|
|
2
|
+
import { validToken } from './crypto.mjs';
|
|
3
|
+
export function isLoopbackHost(hostname) {
|
|
4
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
5
|
+
}
|
|
6
|
+
// Browsers keep cookies per host, not per port, so checkouts side by side on
|
|
7
|
+
// one loopback host would overwrite each other's cookies. A local cookie name
|
|
8
|
+
// therefore carries its origin's port; hosted names never change. Seeds on
|
|
9
|
+
// separate `<name>.localhost` hosts stay apart by host as well.
|
|
10
|
+
export function localCookieName(name, origin) {
|
|
11
|
+
const url = new URL(origin);
|
|
12
|
+
return `${name}_${url.port || (url.protocol === 'https:' ? '443' : '80')}`;
|
|
13
|
+
}
|
|
14
|
+
export function requireLoopback(request) {
|
|
15
|
+
const url = new URL(request.url);
|
|
16
|
+
const host = request.headers.get('host');
|
|
17
|
+
if (!isLoopbackHost(url.hostname) || !host || host.toLowerCase() !== url.host.toLowerCase()) deny('loopback_required');
|
|
18
|
+
}
|
|
19
|
+
export function requireOrigin(request, config) {
|
|
20
|
+
const url = new URL(request.url);
|
|
21
|
+
const host = request.headers.get('host');
|
|
22
|
+
if (url.origin !== config.origin || (host && host.toLowerCase() !== url.host.toLowerCase())) deny('invalid_host');
|
|
23
|
+
if (config.local) requireLoopback(request);
|
|
24
|
+
}
|
|
25
|
+
export function requirePost(request, config) {
|
|
26
|
+
requireOrigin(request, config);
|
|
27
|
+
if (request.method !== 'POST') deny('method_not_allowed', 405);
|
|
28
|
+
if (request.headers.get('origin') !== config.origin || request.headers.get('sec-fetch-site') === 'cross-site') deny('invalid_origin');
|
|
29
|
+
}
|
|
30
|
+
export function readCookie(request, config) {
|
|
31
|
+
const entries = (request.headers.get('cookie') || '').split(';').map((p) => p.trim()).filter((p) => p.startsWith(`${config.cookieName}=`));
|
|
32
|
+
if (entries.length !== 1) return null;
|
|
33
|
+
const value = entries[0].slice(config.cookieName.length + 1);
|
|
34
|
+
return validToken(value) ? value : null;
|
|
35
|
+
}
|
|
36
|
+
export function sessionCookie(token, config, maxAge = config.sessionSeconds) {
|
|
37
|
+
return `${config.cookieName}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${config.secure ? '; Secure' : ''}`;
|
|
38
|
+
}
|
|
39
|
+
export function securityHeaders() {
|
|
40
|
+
return { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer', 'X-Content-Type-Options': 'nosniff', 'X-Robots-Tag': 'noindex', 'Content-Security-Policy': "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" };
|
|
41
|
+
}
|
|
42
|
+
export function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AuthError, deny } from '../errors.mjs';
|
|
2
|
+
import { normalizeEmail } from '../policy.mjs';
|
|
3
|
+
import { requireOrigin } from '../http.mjs';
|
|
4
|
+
import { accountsAdapter, person as checked } from '../accounts/adapter.mjs';
|
|
5
|
+
import { trustedEmail } from './principal.mjs';
|
|
6
|
+
import { completionHook } from './after-confirm.mjs';
|
|
7
|
+
|
|
8
|
+
// Turns a verified principal into a session through an accounts adapter.
|
|
9
|
+
// Protocol verification belongs to the identity method that produced the
|
|
10
|
+
// principal; account association belongs to closed-over server configuration.
|
|
11
|
+
//
|
|
12
|
+
// A principal is { issuer, subject, provider?, email?, emailVerified?,
|
|
13
|
+
// emailVerifiedAt?, amr?, associatedEmail? }. associatedEmail comes only from
|
|
14
|
+
// trusted configuration (subjectEmails), never from a token or a request.
|
|
15
|
+
export function createAdmission({ accounts: adapter, config, clock = Date.now, hooks = {} }) {
|
|
16
|
+
const accounts = accountsAdapter(adapter);
|
|
17
|
+
const afterConfirm = completionHook({ hooks, revokeSession: id => accounts.revokeSession(id) });
|
|
18
|
+
async function admit(request, principal, { returnTo } = {}) {
|
|
19
|
+
requireOrigin(request, config);
|
|
20
|
+
if (!principal || typeof principal.issuer !== 'string' || typeof principal.subject !== 'string' || !principal.subject || principal.subject.length > 255) deny('invalid_identity');
|
|
21
|
+
if (!accounts.identityEnabled || !await accounts.identityEnabled()) deny('provider_sign_in_disabled');
|
|
22
|
+
const associated = principal.associatedEmail === undefined ? undefined : normalizeEmail(principal.associatedEmail);
|
|
23
|
+
const age = principal.emailVerifiedAt;
|
|
24
|
+
// An unused, unverified email claim is not an asserted proof. A configured
|
|
25
|
+
// association still cannot override an asserted stale or malformed proof.
|
|
26
|
+
if (principal.emailVerified === true && age !== undefined && (!Number.isSafeInteger(age) || age > clock()+300000 || clock()-age > 30*86400000) || associated === undefined && principal.datedEmailRequired && (age === undefined || principal.emailVerified !== true)) deny('email_proof_required');
|
|
27
|
+
const proven = trustedEmail(principal, clock());
|
|
28
|
+
// A durable binding identifies the account; it does not renew email proof.
|
|
29
|
+
if (!proven && associated === undefined) deny('invalid_identity');
|
|
30
|
+
let person = checked(await accounts.findBinding(principal));
|
|
31
|
+
if (person) {
|
|
32
|
+
// A binding names one account for good: neither an association nor a
|
|
33
|
+
// changed provider email moves it to another existing account.
|
|
34
|
+
if (associated !== undefined && associated !== person.email) deny();
|
|
35
|
+
if (proven && proven !== person.email && await accounts.findByEmail(proven)) deny();
|
|
36
|
+
} else {
|
|
37
|
+
if (!proven && associated === undefined) deny('email_proof_required');
|
|
38
|
+
const email = associated ?? proven;
|
|
39
|
+
if (!email) deny('invalid_identity');
|
|
40
|
+
person = checked(await accounts.findByEmail(email));
|
|
41
|
+
if (!person) deny();
|
|
42
|
+
}
|
|
43
|
+
await requireRoute(person, principal.provider);
|
|
44
|
+
if (person.status === 'invited' && typeof accounts.acceptInvitation === 'function') {
|
|
45
|
+
const accepted = await accounts.acceptInvitation({ person, principal, returnTo, request });
|
|
46
|
+
if (!accepted) deny();
|
|
47
|
+
return afterConfirm(request, accepted, 'oidc');
|
|
48
|
+
}
|
|
49
|
+
if (person.status !== 'active') deny();
|
|
50
|
+
if (accounts.bindAndIssueSession) {
|
|
51
|
+
const completed=await accounts.bindAndIssueSession({person,principal,method:'oidc',returnTo,request});
|
|
52
|
+
if(!completed)deny();
|
|
53
|
+
return afterConfirm(request,completed,'oidc');
|
|
54
|
+
}
|
|
55
|
+
if (!await accounts.bind({ issuer: principal.issuer, subject: principal.subject, person, associated: associated !== undefined })) deny();
|
|
56
|
+
const completed = await accounts.issueSession({ person, principal, method: 'oidc', returnTo, request });
|
|
57
|
+
if (!completed) deny();
|
|
58
|
+
return afterConfirm(request, completed, 'oidc');
|
|
59
|
+
}
|
|
60
|
+
// A domain's required provider refuses every other route; owners are exempt.
|
|
61
|
+
async function requireRoute(person, provider) {
|
|
62
|
+
if (person.owner || typeof accounts.requiredProvider !== 'function') return;
|
|
63
|
+
const required = await accounts.requiredProvider(person.email);
|
|
64
|
+
if (required && required !== provider) deny('provider_required');
|
|
65
|
+
}
|
|
66
|
+
return { admit, afterConfirm, requireRoute, accounts };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The original server seams, kept for existing callers: verified email may
|
|
70
|
+
// establish a first binding, and createIdentityAdmission snapshots an exact
|
|
71
|
+
// issuer's subject associations.
|
|
72
|
+
export function legacyAdmission({ admission, config, policyVersion }) {
|
|
73
|
+
async function principalOf(identity) {
|
|
74
|
+
// Transaction pins belong to the OIDC verifier. These server-verified seams
|
|
75
|
+
// instead snapshot the native app policy before admission reads authority.
|
|
76
|
+
const { associatedEmail: _, providerPin: _pin, providerGeneration: _generation, ...principal } = identity;
|
|
77
|
+
return { ...principal, ...(policyVersion ? { providerGeneration: await policyVersion() } : {}) };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
async admitVerifiedIdentity(request, identity, input) {
|
|
81
|
+
if (!identity || !config.externalIssuers.includes(identity.issuer) || identity.emailVerified !== true) deny('invalid_identity');
|
|
82
|
+
return admission.admit(request, await principalOf(identity), input);
|
|
83
|
+
},
|
|
84
|
+
createIdentityAdmission(input) {
|
|
85
|
+
try {
|
|
86
|
+
const issuer = input?.issuer;
|
|
87
|
+
if (!config.externalIssuers.includes(issuer)) throw new Error();
|
|
88
|
+
const entries = input.subjectEmails ?? {};
|
|
89
|
+
if (typeof entries !== 'object' || Array.isArray(entries) || entries === null) throw new Error();
|
|
90
|
+
const subjects = new Map(Object.entries(entries).map(([subject, email]) => {
|
|
91
|
+
if (!subject || subject.length > 255) throw new Error();
|
|
92
|
+
return [subject, normalizeEmail(email)];
|
|
93
|
+
}));
|
|
94
|
+
// Neither request input nor token claims can replace this snapshot or opt
|
|
95
|
+
// into association. The returned function is bound to this exact issuer.
|
|
96
|
+
return async (request, identity, options) => {
|
|
97
|
+
if (!identity || identity.issuer !== issuer) deny('invalid_identity');
|
|
98
|
+
const associatedEmail = subjects.get(identity.subject);
|
|
99
|
+
if (associatedEmail === undefined && identity.emailVerified !== true) deny('invalid_identity');
|
|
100
|
+
return admission.admit(request, { ...await principalOf(identity), ...(associatedEmail === undefined ? {} : { associatedEmail }) }, options);
|
|
101
|
+
};
|
|
102
|
+
} catch { throw new AuthError('invalid_configuration', 503); }
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthError } from '../errors.mjs';
|
|
2
|
+
import { safeReturnTo } from '../policy.mjs';
|
|
3
|
+
|
|
4
|
+
// The owner of the session store supplies revocation; a refused sign-in must
|
|
5
|
+
// lose its persisted authority before its error is returned to the caller.
|
|
6
|
+
export function completionHook({ hooks, revokeSession }) {
|
|
7
|
+
return async (request, completed, method) => {
|
|
8
|
+
completed = { ...completed, returnTo: safeReturnTo(completed.returnTo) };
|
|
9
|
+
if (typeof hooks.afterConfirm !== 'function') return completed;
|
|
10
|
+
try {
|
|
11
|
+
const result = await hooks.afterConfirm({ request, operator: completed.operator, session: completed.session, method, returnTo: completed.returnTo });
|
|
12
|
+
return result?.returnTo === undefined ? completed : { ...completed, returnTo: safeReturnTo(result.returnTo) };
|
|
13
|
+
} catch (error) {
|
|
14
|
+
await revokeSession(completed.session.id);
|
|
15
|
+
throw error instanceof AuthError ? error : new AuthError('sign_in_refused', 403);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { escapeHtml } from '../../http.mjs';
|
|
2
|
+
import { pageResponse } from '../../page.mjs';
|
|
3
|
+
|
|
4
|
+
const HEADINGS = { invite: 'Accept your invitation to', magic: 'Confirm sign-in to', step_up: "Confirm it's you in" };
|
|
5
|
+
|
|
6
|
+
export async function confirmationResponse({ branding, preview, token, method }) {
|
|
7
|
+
const product = escapeHtml(branding.productName);
|
|
8
|
+
const heading = preview ? `${HEADINGS[preview.kind]} ${product}` : 'This link is invalid or expired';
|
|
9
|
+
const content = preview
|
|
10
|
+
? `<p>This link works once and expires in ${preview.expiresInMinutes} ${preview.expiresInMinutes === 1 ? 'minute' : 'minutes'}.</p><form method="post" action="/auth/confirm"><input type="hidden" name="token" value="${escapeHtml(token)}"><button type="submit">Confirm</button></form>`
|
|
11
|
+
: `<p>This link may have expired or already been used. Request a new link to continue.</p><a class="action" href="${escapeHtml(branding.signInPath)}">Request a new link</a>`;
|
|
12
|
+
// Native form POSTs need their Origin; never disclose the token path/query.
|
|
13
|
+
return pageResponse({ branding, heading, content, status: preview ? 200 : 400, method, referrer: preview ? 'strict-origin' : undefined });
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AuthConfig, AuthHooks, CompletedSession, Session, StepUpAction, StepUpConfirmed, LinkPreview, MailSender, OperatorAuth } from '../../index.mjs';
|
|
2
|
+
import type { NativePerson } from '../../accounts/native.mjs';
|
|
3
|
+
export const STEP_UP_ACTIONS: readonly StepUpAction[];
|
|
4
|
+
export const STEP_UP_SECONDS: number;
|
|
5
|
+
export interface LinkRecord { id: string; hash: string; kind: 'magic'; returnTo: string; expiresAt: number; operatorId: string; expectedVersion: number; now: number }
|
|
6
|
+
export interface NativeLinkPreview { kind: 'magic'; email: string; operatorId: string; version: number; expiresAt: number; returnTo: string; dev?: boolean }
|
|
7
|
+
export interface StepUpChallenge { id: string; hash: string; action: StepUpAction; returnTo: string; now: number; expiresAt: number }
|
|
8
|
+
/** Persistence is the authority boundary. Consumption + current authority check + issuance are one transaction, not separate read/write calls. */
|
|
9
|
+
export interface NativeProofPersistence<R extends string = string> {
|
|
10
|
+
limits(keys: string[], now: number, windowMs: number): Promise<number[]>;
|
|
11
|
+
token(record: LinkRecord): Promise<boolean>;
|
|
12
|
+
preview(digest: string, now: number): Promise<NativeLinkPreview | null>;
|
|
13
|
+
consumeLink(input: {request: Request; digest: string; person: NativePerson<R>; now: number; returnTo: string}): Promise<CompletedSession<R> | null>;
|
|
14
|
+
revokeLink(id: string, now: number): Promise<unknown>;
|
|
15
|
+
stepUpPreview(digest: string, now: number): Promise<{expiresAt: number} | null>;
|
|
16
|
+
stepUpChallenge(record: StepUpChallenge, actor: Session<R>): Promise<boolean>;
|
|
17
|
+
consumeStepUp(digest: string, actor: Session<R>, now: number): Promise<{action: StepUpAction; returnTo: string} | null>;
|
|
18
|
+
revokeStepUp(id: string, now: number): Promise<unknown>;
|
|
19
|
+
/** Recheck asking session, current person version/status and persisted local proof; never infer freshness from session kind. */
|
|
20
|
+
steppedUp(actor: Session<R>, since: number, now: number): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
export interface EmailLinkPersistence<R extends string = string> extends Omit<NativeProofPersistence<R>, 'preview' | 'consumeLink'> {
|
|
23
|
+
/** Required when supplying hooks.afterConfirm directly; durably revoke the issued session before reporting refusal. */
|
|
24
|
+
revokeSession?(id: string): Promise<unknown>;
|
|
25
|
+
authenticate(request: Request): Promise<Session<R> | null>;
|
|
26
|
+
requiredProvider(email: string): Promise<string | null>;
|
|
27
|
+
byEmail(email: string): Promise<NativePerson<R> | null>;
|
|
28
|
+
authorizedLink(request: Request, digest: string): Promise<{link: NativeLinkPreview; person?: NativePerson<R>} | null>;
|
|
29
|
+
consumeLink(input: {request: Request; digest: string; approval: {link: NativeLinkPreview; person?: NativePerson<R>}; now: number}): Promise<CompletedSession<R> | null>;
|
|
30
|
+
}
|
|
31
|
+
/** A direct veto hook requires revocation authority. A composed callback owns its hooks and revocation; never supply the same hook through both paths. */
|
|
32
|
+
export type EmailLinkCompletion<R extends string> = {
|
|
33
|
+
persistence: EmailLinkPersistence<R> & { revokeSession(id: string): Promise<unknown> };
|
|
34
|
+
hooks?: AuthHooks<R>;
|
|
35
|
+
afterConfirm?: never;
|
|
36
|
+
} | {
|
|
37
|
+
persistence: EmailLinkPersistence<R>;
|
|
38
|
+
hooks?: Omit<AuthHooks<R>, 'afterConfirm'> & { afterConfirm?: never };
|
|
39
|
+
afterConfirm?: (request: Request, completed: CompletedSession<R>, method: 'email') => Promise<CompletedSession<R>>;
|
|
40
|
+
};
|
|
41
|
+
/** Standalone composition normalizes the same AuthConfig used by createOperatorAuth. */
|
|
42
|
+
export function createEmailLinkLifecycle<R extends string>(options: {
|
|
43
|
+
config: AuthConfig;
|
|
44
|
+
clock?: () => number; mailSender?: MailSender; clientIp: (request: Request) => string | Promise<string>;
|
|
45
|
+
hash: (purpose: string, token: string) => Promise<string>;
|
|
46
|
+
} & EmailLinkCompletion<R>): Pick<OperatorAuth<R>, 'requestMagicLink' | 'previewLink' | 'confirmLink' | 'requestStepUp' | 'requireStepUp'> & {
|
|
47
|
+
requireFreshProof(actor: Session<R>): Promise<void>;
|
|
48
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { configure } from '../../config.mjs';
|
|
2
|
+
import { AuthError, deny } from '../../errors.mjs';
|
|
3
|
+
import { opaqueToken, validToken } from '../../crypto.mjs';
|
|
4
|
+
import { safeReturnTo, normalizeEmail } from '../../policy.mjs';
|
|
5
|
+
import { requirePost, requireOrigin } from '../../http.mjs';
|
|
6
|
+
import { isActiveOperator } from '../../accounts/roles.mjs';
|
|
7
|
+
import { isLocalCli } from '../../local-cli.mjs';
|
|
8
|
+
import { linkMessage } from './message.mjs';
|
|
9
|
+
import { completionHook } from '../after-confirm.mjs';
|
|
10
|
+
export const STEP_UP_ACTIONS = Object.freeze(['providers:unlink', 'providers:relink', 'domains:change', 'owner:transfer', 'export', 'delete', 'credentials:mint']);
|
|
11
|
+
export const STEP_UP_SECONDS = 600;
|
|
12
|
+
|
|
13
|
+
// Persistence owns the atomic authority check, proof consumption and session
|
|
14
|
+
// issuance. It never receives a raw email token; only its keyed digest.
|
|
15
|
+
export function createEmailLinkLifecycle({ config: input, clock = Date.now, mailSender, clientIp, hash, persistence, hooks = {}, afterConfirm }) {
|
|
16
|
+
const config = configure(input);
|
|
17
|
+
const required = ['limits', 'token', 'revokeLink', 'authorizedLink', 'consumeLink', 'byEmail', 'authenticate', 'requiredProvider', 'stepUpPreview', 'stepUpChallenge', 'consumeStepUp', 'revokeStepUp', 'steppedUp'];
|
|
18
|
+
if (!persistence || required.some(k => typeof persistence[k] !== 'function') || typeof hash !== 'function' || typeof clientIp !== 'function') throw new AuthError('invalid_proof_persistence', 503);
|
|
19
|
+
if (afterConfirm !== undefined && typeof afterConfirm !== 'function' || hooks.afterConfirm !== undefined && (typeof hooks.afterConfirm !== 'function' || afterConfirm !== undefined || typeof persistence.revokeSession !== 'function')) throw new AuthError('invalid_confirmation_hooks', 503);
|
|
20
|
+
const complete = afterConfirm ?? completionHook({ hooks, revokeSession: id => persistence.revokeSession(id) });
|
|
21
|
+
const store = persistence;
|
|
22
|
+
async function limit(request, email, scope) {
|
|
23
|
+
// The CLI's own loopback sign-in and owner claim are not a person's attempts.
|
|
24
|
+
if ((scope === 'magic' || scope === 'bootstrap') && await isLocalCli(request, config)) return;
|
|
25
|
+
const ip = await clientIp(request);
|
|
26
|
+
if (typeof ip !== 'string' || !ip || ip.length > 128) deny('client_ip_unavailable', 503);
|
|
27
|
+
const keys = await Promise.all([hash('rate', `${scope}:ip:${ip}`), hash('rate', `${scope}:email:${email}`)]);
|
|
28
|
+
const counts = await store.limits(keys, clock(), config.limitWindowSeconds * 1000);
|
|
29
|
+
if (!Array.isArray(counts) || counts.length !== keys.length || counts.some(value => !Number.isSafeInteger(value) || value < 0)) deny('invalid_proof_persistence', 503);
|
|
30
|
+
if (counts[0] > config.ipLimit || counts[1] > config.emailLimit) deny('rate_limited', 429);
|
|
31
|
+
}
|
|
32
|
+
async function newToken(kind, returnTo) {
|
|
33
|
+
const token = opaqueToken();
|
|
34
|
+
return { id: crypto.randomUUID(), token, hash: await hash('link', token), kind, returnTo, expiresAt: clock() + config.linkSeconds * 1000 };
|
|
35
|
+
}
|
|
36
|
+
// The email link: the app's own proof that a person holds an address.
|
|
37
|
+
async function sendLink(email, token, revoke = () => store.revokeLink(token.id, clock())) {
|
|
38
|
+
if (!mailSender || !config.MAIL_FROM) { await revoke(); deny('mail_unavailable', 503); }
|
|
39
|
+
const url = new URL('/auth/confirm', config.origin);
|
|
40
|
+
url.searchParams.set('token', token.token);
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = await mailSender.send({ deliveryId: token.id, from: config.MAIL_FROM, to: [email],
|
|
44
|
+
...linkMessage(config, token, url.href),
|
|
45
|
+
headers: { 'Auto-Submitted': 'auto-generated' } });
|
|
46
|
+
} catch { await revoke(); deny('mail_unavailable', 503); }
|
|
47
|
+
if (result?.status !== 'accepted' && !(result?.status === 'development' && config.local)) {
|
|
48
|
+
// Even ambiguous delivery cannot leave a valid, unreported auth link.
|
|
49
|
+
await revoke(); deny('mail_unavailable', 503);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function requireFreshProof(actor) {
|
|
54
|
+
if (!await store.steppedUp(actor, clock() - STEP_UP_SECONDS * 1000, clock())) deny('step_up_required', 403);
|
|
55
|
+
}
|
|
56
|
+
const authenticate = request => store.authenticate(request);
|
|
57
|
+
return {
|
|
58
|
+
limit, newToken, sendLink, requireFreshProof,
|
|
59
|
+
async requestMagicLink(request, { email: raw, returnTo }) {
|
|
60
|
+
requirePost(request, config);
|
|
61
|
+
const email = normalizeEmail(raw);
|
|
62
|
+
await limit(request, email, 'magic');
|
|
63
|
+
// Configuration failure is uniform for all addresses; unknown addresses
|
|
64
|
+
// otherwise have the same successful public response as admitted ones.
|
|
65
|
+
if (!mailSender || !config.MAIL_FROM) deny('mail_unavailable', 503);
|
|
66
|
+
// A domain rule is said for the whole domain, whoever asks.
|
|
67
|
+
const required = await store.requiredProvider(email);
|
|
68
|
+
const accepted = { status: 'accepted', ...(required ? { requiredProvider: required } : {}) };
|
|
69
|
+
const operator = await store.byEmail(email);
|
|
70
|
+
if (!isActiveOperator(operator, config.allowList)) return accepted;
|
|
71
|
+
if (required && !operator.owner && operator.role !== 'owner') return accepted;
|
|
72
|
+
if (hooks.beforeMagicLink && await hooks.beforeMagicLink({ request, email, operator }) === false) return accepted;
|
|
73
|
+
const token = await newToken('magic', safeReturnTo(returnTo));
|
|
74
|
+
const { token: _secret, ...record } = token;
|
|
75
|
+
if (await store.token({ ...record, operatorId: operator.id, expectedVersion: operator.version, now: clock() })) await sendLink(email, token);
|
|
76
|
+
return accepted;
|
|
77
|
+
},
|
|
78
|
+
async previewLink(request, token) {
|
|
79
|
+
requireOrigin(request, config);
|
|
80
|
+
if (!['GET', 'HEAD'].includes(request.method)) deny('method_not_allowed', 405);
|
|
81
|
+
if (!validToken(token)) return null;
|
|
82
|
+
const digest = await hash('link', token);
|
|
83
|
+
const result = await store.authorizedLink(request, digest);
|
|
84
|
+
if (result) return { kind: result.link.kind, expiresInMinutes: Math.max(1, Math.ceil((result.link.expiresAt - clock()) / 60000)) };
|
|
85
|
+
const challenge = await store.stepUpPreview(digest, clock());
|
|
86
|
+
return challenge ? { kind: 'step_up', expiresInMinutes: Math.max(1, Math.ceil((challenge.expiresAt - clock()) / 60000)) } : null;
|
|
87
|
+
},
|
|
88
|
+
async confirmLink(request, { token }) {
|
|
89
|
+
requirePost(request, config);
|
|
90
|
+
await limit(request, typeof token === 'string' ? token.slice(0, 128) : '', 'confirmation');
|
|
91
|
+
if (!validToken(token)) deny('invalid_or_expired', 400);
|
|
92
|
+
const digest = await hash('link', token);
|
|
93
|
+
const approval = await store.authorizedLink(request, digest);
|
|
94
|
+
if (!approval) {
|
|
95
|
+
// A step-up link completes only in the session that asked for it.
|
|
96
|
+
const actor = await authenticate(request);
|
|
97
|
+
const proof = actor && await store.consumeStepUp(digest, actor, clock());
|
|
98
|
+
if (!proof) deny('invalid_or_expired', 400);
|
|
99
|
+
return { stepUp: proof.action, returnTo: safeReturnTo(proof.returnTo) };
|
|
100
|
+
}
|
|
101
|
+
const completed = await store.consumeLink({ request, digest, approval, now: clock() });
|
|
102
|
+
if (!completed) deny('invalid_or_expired', 400);
|
|
103
|
+
const result = await complete(request, { ...completed, returnTo: safeReturnTo(completed.returnTo) }, 'email');
|
|
104
|
+
return { ...result, returnTo: safeReturnTo(result.returnTo) };
|
|
105
|
+
},
|
|
106
|
+
/** Ask for a step-up proof: an email link to the signed-in operator's own address. */
|
|
107
|
+
async requestStepUp(request, { action, returnTo } = {}) {
|
|
108
|
+
requirePost(request, config);
|
|
109
|
+
if (!STEP_UP_ACTIONS.includes(action)) throw new AuthError('invalid_action');
|
|
110
|
+
const actor = await authenticate(request);
|
|
111
|
+
if (!actor) deny('unauthenticated', 401);
|
|
112
|
+
await limit(request, actor.operator.email, 'step-up');
|
|
113
|
+
const token = await newToken('step_up', safeReturnTo(returnTo));
|
|
114
|
+
if (!await store.stepUpChallenge({ id: token.id, hash: token.hash, action, returnTo: token.returnTo, now: clock(), expiresAt: token.expiresAt }, actor)) deny('authority_changed', 409);
|
|
115
|
+
await sendLink(actor.operator.email, token, () => store.revokeStepUp(token.id, clock()));
|
|
116
|
+
return { status: 'accepted' };
|
|
117
|
+
},
|
|
118
|
+
/** Server-side guard for a listed action: the session holds a fresh email-link proof. */
|
|
119
|
+
async requireStepUp(request, action) {
|
|
120
|
+
if (!STEP_UP_ACTIONS.includes(action)) throw new AuthError('invalid_action');
|
|
121
|
+
const actor = await authenticate(request);
|
|
122
|
+
if (!actor) deny('unauthenticated', 401);
|
|
123
|
+
await requireFreshProof(actor);
|
|
124
|
+
const { hash: _, ...session } = actor;
|
|
125
|
+
return session;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { accentInk } from '../../branding.mjs';
|
|
2
|
+
import { escapeHtml } from '../../http.mjs';
|
|
3
|
+
|
|
4
|
+
export function linkMessage(config, token, url) {
|
|
5
|
+
const { productName, accentColor } = config.branding;
|
|
6
|
+
const invitation = token.kind === 'invite', stepUp = token.kind === 'step_up';
|
|
7
|
+
const action = invitation ? 'accept your invitation to' : stepUp ? 'confirm a sensitive change in' : 'sign in to';
|
|
8
|
+
const minutes = config.linkSeconds / 60;
|
|
9
|
+
const expiry = `This link works once and expires in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}.`;
|
|
10
|
+
const heading = invitation ? `Accept your invitation to ${productName}` : stepUp ? `Confirm it's you in ${productName}` : `Confirm sign-in to ${productName}`;
|
|
11
|
+
return {
|
|
12
|
+
subject: invitation ? `You're invited to ${productName}` : stepUp ? `Confirm it's you in ${productName}` : `Your sign-in link for ${productName}`,
|
|
13
|
+
text: `Open this link to review and confirm. On the confirmation page, press Confirm to ${action} ${productName}. ${expiry}\n\n${url}`,
|
|
14
|
+
html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(heading)}</title></head><body style="margin:0;font-family:Arial,sans-serif;background:#fff;color:#172033"><main style="max-width:480px;margin:0 auto;padding:32px 24px"><h1>${escapeHtml(heading)}</h1><p>Open this link to review and confirm. On the confirmation page, press Confirm to ${action} ${escapeHtml(productName)}.</p><p>${expiry}</p><p><a href="${escapeHtml(url)}" style="display:inline-block;padding:14px 24px;border:1px solid #64748b;border-radius:8px;background:${accentColor};color:${accentInk(accentColor)};font-family:Arial,sans-serif;font-weight:bold;text-decoration:none">Confirm</a></p></main></body></html>`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AccountsAdapter, Admission, AuthHooks, Principal } from '../index.mjs';
|
|
2
|
+
export { createOidcProviders, createOidcHandler, OidcError, PRESETS, MINIMUM_PROVIDER_VERSION } from './oidc/index.mjs';
|
|
3
|
+
export const EMAIL_PROOF_DAYS: number;
|
|
4
|
+
/** Admission over any accounts adapter: a verified principal in, a session out. */
|
|
5
|
+
export function createAdmission<R extends string>(input: { accounts: AccountsAdapter<R>; config: { origin: string; local?: boolean }; clock?: () => number; hooks?: AuthHooks<R> }): Admission<R>;
|
|
6
|
+
export function trustedEmail(principal: Principal, now: number): string | null;
|
|
7
|
+
export function refuseInProduction(principal: Principal, config: { local?: boolean }): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Identity: proves who a person is and outputs a verified principal. It never
|
|
2
|
+
// decides who may enter; an accounts adapter does (../accounts/adapter.mjs).
|
|
3
|
+
export { createAdmission } from './admission.mjs';
|
|
4
|
+
export { trustedEmail, refuseInProduction, EMAIL_PROOF_DAYS } from './principal.mjs';
|
|
5
|
+
export { createOidcProviders, createOidcHandler, OidcError, PRESETS, MINIMUM_PROVIDER_VERSION } from './oidc/index.mjs';
|