@runeditrun/rer 0.0.0-stage → 0.1.0
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 +154 -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 +177 -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 +155 -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 +129 -0
- package/global/launch/facts.mjs +93 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +61 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +97 -0
- package/global/launch/steps.mjs +81 -0
- package/global/launch/worker.mjs +43 -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,81 @@
|
|
|
1
|
+
# Operator-auth conformance
|
|
2
|
+
|
|
3
|
+
`index.mjs` exports `registerOperatorAuthConformance({ test, adapter })`. Import
|
|
4
|
+
that function once in a seed's Node test file and pass its existing Node test
|
|
5
|
+
registrar plus a test-only adapter for the seed's real Worker. The suite registers
|
|
6
|
+
checks for the HTTP contract, the same native session store, the seed's protected
|
|
7
|
+
private API, test-captured mail outcomes, and its configured limits. It does not
|
|
8
|
+
replace `createOperatorAuth`, use an in-memory auth fake, or require IdP credentials.
|
|
9
|
+
|
|
10
|
+
The frozen adapter contract is declared in `index.d.mts`. Its `open` method
|
|
11
|
+
receives a fresh test-only `AuthConfig`, one IF-056-shaped mail result, and an
|
|
12
|
+
explicit no-mail switch. It returns `call` for the actual Worker `/auth/*`
|
|
13
|
+
surface, `privateRequest` for one actual protected consumer API (with optional
|
|
14
|
+
test-only request headers for Host-boundary probes), `mails` for
|
|
15
|
+
messages captured by the test-only sender, and `close` for disposing all runtime
|
|
16
|
+
and D1 state. The test runtime must apply the consumer's actual numbered core
|
|
17
|
+
schema and use real workerd/D1; keep synthetic mail capture inside the test
|
|
18
|
+
adapter. Never expose this adapter or its fixture routes in application builds.
|
|
19
|
+
|
|
20
|
+
The default presentation remains asset-free. A consumer that intentionally adds
|
|
21
|
+
self-hosted fonts sets `adapter.confirmationFonts: true`. That profile requires
|
|
22
|
+
same-origin stylesheet and font-preload links, verifies the exact inline style
|
|
23
|
+
hash plus only same-origin style/font permissions, and still rejects external
|
|
24
|
+
origins, scripts, images and unsafe inline attributes. Mail stays self-contained.
|
|
25
|
+
|
|
26
|
+
A consumer that configures `branding.productName` or a seed theme
|
|
27
|
+
(`branding.theme`, its served `rer-theme.css`) declares the same values as
|
|
28
|
+
`adapter.branding: { productName, theme }`. Mail and confirmation must then
|
|
29
|
+
carry that name, and a themed confirmation must hold exactly that one theme
|
|
30
|
+
stylesheet link and the CSP `style-src 'self' 'sha256-…'; font-src 'self';
|
|
31
|
+
img-src 'self'`; every other resource rule above still applies.
|
|
32
|
+
|
|
33
|
+
The Base conformance registration is executable at
|
|
34
|
+
[`../tests/conformance.test.ts`](../tests/conformance.test.ts), and its D1/Worker
|
|
35
|
+
adapter lives in this directory's `tests/worker.mjs` and `tests/runtime.mjs`.
|
|
36
|
+
An adopting seed should map these methods to its actual Hono/Worker dispatcher
|
|
37
|
+
and native private API, then import and call the exported registration from its
|
|
38
|
+
existing test discovery path. No Base manifest or workflow is changed here.
|
|
39
|
+
|
|
40
|
+
The registered cases prove:
|
|
41
|
+
|
|
42
|
+
- first-owner bootstrap with no mail dependency, one owner, a native session,
|
|
43
|
+
no duplicate owner claim, and no private API access without a session;
|
|
44
|
+
- GET and HEAD confirmation previews without cookie issuance, followed by one
|
|
45
|
+
POST confirmation to the original safe `returnTo` and rejection of replay;
|
|
46
|
+
- neutral branding in magic/invitation previews and both mail parts,
|
|
47
|
+
exact-byte stylesheet CSP, HEAD parity, single hidden POST token, plain subjects,
|
|
48
|
+
one confirmation URL per mail part and configured invalid-link recovery;
|
|
49
|
+
- owner/admin/member boundaries, session revocation and session invalidation
|
|
50
|
+
after an operator role change;
|
|
51
|
+
- local review issuance and subsequent session/private-API checks reject
|
|
52
|
+
attacker-controlled Host values even when forwarded-Host headers claim
|
|
53
|
+
loopback;
|
|
54
|
+
- allow-list masking and independent IP/email rate limits before mail effects;
|
|
55
|
+
- accepted, rejected and uncertain IF-056 mail results. Only `accepted` makes
|
|
56
|
+
the link usable; rejected or uncertain delivery revokes it and returns
|
|
57
|
+
`mail_unavailable`;
|
|
58
|
+
- optionally, owner-authorized provider sign-in enablement through the public
|
|
59
|
+
settings route, then a first verified OIDC admission into an existing unbound owner
|
|
60
|
+
through `admitVerifiedIdentity`, followed by access through the same native
|
|
61
|
+
session route. This is a no-provider-key seam check, not an OIDC protocol
|
|
62
|
+
check. The protocol, keys, PKCE, state, nonce and callback behavior remain
|
|
63
|
+
covered by `modules/operator-auth/identity/oidc/tests/`.
|
|
64
|
+
|
|
65
|
+
The optional seam is enabled by setting `adapter.oidcIssuer` to one exact HTTPS
|
|
66
|
+
issuer and implementing `runtime.admitVerifiedIdentity`. It must call the real
|
|
67
|
+
trusted core seam with the issuer included in `externalIssuers`; the conformance
|
|
68
|
+
case starts from a fresh owner with no prior external identity binding. It makes
|
|
69
|
+
no claim that a changed issuer/subject can bind by matching email. Do not route
|
|
70
|
+
request-body identity data to this seam in the application.
|
|
71
|
+
|
|
72
|
+
The test sender returns only the IF-056 result shape. `accepted` is API
|
|
73
|
+
acceptance, not inbox delivery, and may have a null provider message ID. A real
|
|
74
|
+
adopter uses the selected `modules/mail/index.mjs` `createMailSender` with its
|
|
75
|
+
configured provider; a test sender is never a production path.
|
|
76
|
+
|
|
77
|
+
Custom branding is additionally checked against real D1 in the module runtime
|
|
78
|
+
suite. These shared cases are also consumed by default-seed; no provider sends are
|
|
79
|
+
required. Browser responsiveness, keyboard focus and pinned axe-core 4.10.3
|
|
80
|
+
audits run in `../tests/presentation-browser.test.mjs` at 1440 and 390 pixels,
|
|
81
|
+
with light/dark schemes and black/white accents, retaining production CSP.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AuthConfig, CompletedSession, MailMessage, VerifiedIdentity } from '../index.mjs';
|
|
2
|
+
|
|
3
|
+
export type ConformanceMailStatus = 'accepted' | 'rejected' | 'uncertain';
|
|
4
|
+
export interface ConformanceRequestOptions {
|
|
5
|
+
method?: string;
|
|
6
|
+
input?: unknown;
|
|
7
|
+
cookie?: string;
|
|
8
|
+
headers?: HeadersInit;
|
|
9
|
+
ip?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ConformanceOpenOptions {
|
|
12
|
+
/** Test-only owner and secret values supplied to the disposable Worker. */
|
|
13
|
+
config: AuthConfig;
|
|
14
|
+
/** Synthetic IF-056-shaped test result. This adapter never sends real mail. */
|
|
15
|
+
mailStatus: ConformanceMailStatus;
|
|
16
|
+
noMail: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface OperatorAuthConformanceRuntime {
|
|
19
|
+
/** Dispatch through the consumer's real Worker and mounted `/auth/*` handler. */
|
|
20
|
+
call(path: string, options?: ConformanceRequestOptions): Promise<Response>;
|
|
21
|
+
/** Invoke a native protected HTTP API that authenticates with this same module instance. */
|
|
22
|
+
privateRequest(cookie?: string, options?: Omit<ConformanceRequestOptions, 'cookie' | 'input' | 'method'>): Promise<Response>;
|
|
23
|
+
/** Return only messages captured by the test-only synthetic sender. */
|
|
24
|
+
mails(): Promise<MailMessage[]>;
|
|
25
|
+
/** Optional test-only call through the trusted, already-verified OIDC admission seam. */
|
|
26
|
+
admitVerifiedIdentity?(identity: VerifiedIdentity, returnTo?: string): Promise<CompletedSession>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface OperatorAuthConformanceAdapter {
|
|
30
|
+
/** Verify the consumer's same-origin font links and their bounded CSP permissions. */
|
|
31
|
+
confirmationFonts?: boolean;
|
|
32
|
+
/** The consumer's configured branding: its product name and served rer-theme.css path, when it declares them. */
|
|
33
|
+
branding?: { productName?: string; theme?: string };
|
|
34
|
+
/** Set only when the runtime also implements the provider-key-free admission seam case. */
|
|
35
|
+
oidcIssuer?: string;
|
|
36
|
+
/** Must create disposable real workerd/D1 state and mount the consumer's actual auth and private API routes. */
|
|
37
|
+
open(options: ConformanceOpenOptions): Promise<OperatorAuthConformanceRuntime>;
|
|
38
|
+
}
|
|
39
|
+
export type ConformanceTestRegistrar = (name: string, fn: () => void | Promise<void>) => unknown;
|
|
40
|
+
export function registerOperatorAuthConformance(input: {
|
|
41
|
+
test: ConformanceTestRegistrar;
|
|
42
|
+
adapter: OperatorAuthConformanceAdapter;
|
|
43
|
+
}): void;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { checkConfirmation, checkMail } from './presentation.mjs';
|
|
2
|
+
const BASE_CONFIG = Object.freeze({
|
|
3
|
+
origin: 'https://auth.conformance.test',
|
|
4
|
+
OWNER_EMAIL: 'owner@example.test',
|
|
5
|
+
SESSION_SECRET: 'example-conformance-session-secret-with-32-bytes',
|
|
6
|
+
SETUP_TOKEN: 'example-conformance-owner-setup-token-with-32-bytes',
|
|
7
|
+
MAIL_FROM: 'Operator Auth <auth@example.test>',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const MAIL_STATUSES = new Set(['accepted', 'rejected', 'uncertain']);
|
|
11
|
+
|
|
12
|
+
function fixture(adapter, overrides = {}) {
|
|
13
|
+
const config = {
|
|
14
|
+
...BASE_CONFIG,
|
|
15
|
+
...(adapter.oidcIssuer ? { externalIssuers: [adapter.oidcIssuer] } : {}),
|
|
16
|
+
...(overrides.allowList ? { allowList: overrides.allowList } : {}),
|
|
17
|
+
...(overrides.ipLimit ? { ipLimit: overrides.ipLimit } : {}),
|
|
18
|
+
...(overrides.emailLimit ? { emailLimit: overrides.emailLimit } : {}),
|
|
19
|
+
};
|
|
20
|
+
if (overrides.localReview) {
|
|
21
|
+
Object.assign(config, {
|
|
22
|
+
origin: 'http://localhost:8787',
|
|
23
|
+
local: true,
|
|
24
|
+
review: { enabled: true, operatorEmail: config.OWNER_EMAIL },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
config,
|
|
29
|
+
mailStatus: overrides.mailStatus ?? 'accepted',
|
|
30
|
+
noMail: overrides.noMail ?? false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cookie(response) {
|
|
35
|
+
const value = response.headers.get('set-cookie');
|
|
36
|
+
if (!value) throw new Error('Expected the auth response to set a session cookie.');
|
|
37
|
+
return value.split(';', 1)[0];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function post(input, cookieValue, extra = {}) {
|
|
41
|
+
return { method: 'POST', input, ...(cookieValue ? { cookie: cookieValue } : {}), ...extra };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function bootstrap(runtime, config) {
|
|
45
|
+
const response = await runtime.call('/auth/bootstrap', post({ token: config.SETUP_TOKEN, displayName: 'Conformance Owner' }));
|
|
46
|
+
if (response.status !== 303) throw new Error(`Owner bootstrap returned ${response.status}: ${await response.clone().text()}`);
|
|
47
|
+
return cookie(response);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function capturedLink(runtime, index = -1) {
|
|
51
|
+
const messages = await runtime.mails();
|
|
52
|
+
const message = messages.at(index);
|
|
53
|
+
if (!message) throw new Error('Expected the test-only mail adapter to capture a message.');
|
|
54
|
+
const url = message.text.match(/https?:\/\/[^\s]+/)?.[0];
|
|
55
|
+
if (!url) throw new Error('The captured auth message has no confirmation URL.');
|
|
56
|
+
return new URL(url);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function confirmCaptured(runtime, index = -1) {
|
|
60
|
+
const link = await capturedLink(runtime, index);
|
|
61
|
+
const response = await runtime.call('/auth/confirm', post({ token: link.searchParams.get('token') }));
|
|
62
|
+
return { link, response, cookie: response.status === 303 ? cookie(response) : null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requireRuntime(runtime) {
|
|
66
|
+
for (const name of ['call', 'privateRequest', 'mails', 'close']) {
|
|
67
|
+
if (typeof runtime?.[name] !== 'function') throw new TypeError(`Conformance runtime must provide ${name}().`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Register black-box checks for a real operator-auth Worker and its native private API.
|
|
73
|
+
* The adapter is test infrastructure: it must mount this module against disposable D1,
|
|
74
|
+
* capture synthetic mail only, and route privateRequest through the consumer's real guard.
|
|
75
|
+
*/
|
|
76
|
+
export function registerOperatorAuthConformance({ test, adapter }) {
|
|
77
|
+
if (typeof test !== 'function') throw new TypeError('A test registrar is required.');
|
|
78
|
+
if (!adapter || typeof adapter.open !== 'function') throw new TypeError('A conformance runtime adapter is required.');
|
|
79
|
+
if (adapter.oidcIssuer !== undefined && (typeof adapter.oidcIssuer !== 'string' || !adapter.oidcIssuer.startsWith('https://'))) {
|
|
80
|
+
throw new TypeError('oidcIssuer must be the exact HTTPS issuer configured by the test adapter.');
|
|
81
|
+
}
|
|
82
|
+
if (adapter.confirmationFonts !== undefined && typeof adapter.confirmationFonts !== 'boolean') {
|
|
83
|
+
throw new TypeError('confirmationFonts must be boolean.');
|
|
84
|
+
}
|
|
85
|
+
const { branding = {} } = adapter;
|
|
86
|
+
if (typeof branding !== 'object' || Object.keys(branding).some(key => !['productName', 'theme'].includes(key))
|
|
87
|
+
|| (branding.productName !== undefined && typeof branding.productName !== 'string')
|
|
88
|
+
|| (branding.theme !== undefined && !/^\/(?!\/)[A-Za-z0-9._~/-]+\.css$/.test(branding.theme))) {
|
|
89
|
+
throw new TypeError('branding declares only the consumer\'s productName and same-origin theme stylesheet.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function scenario(name, options, run) {
|
|
93
|
+
test(name, async () => {
|
|
94
|
+
const opened = fixture(adapter, options);
|
|
95
|
+
const runtime = await adapter.open(opened);
|
|
96
|
+
try {
|
|
97
|
+
requireRuntime(runtime);
|
|
98
|
+
await run(runtime, opened.config);
|
|
99
|
+
} finally {
|
|
100
|
+
if (typeof runtime?.close === 'function') await runtime.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
scenario('bootstrap claims one owner without mail or product seed data', { noMail: true }, async (runtime, config) => {
|
|
106
|
+
assertStatus(await runtime.call('/auth/bootstrap', post({ token: 'example-wrong-conformance-setup-token', displayName: 'Wrong Owner' })), 403);
|
|
107
|
+
assertStatus(await runtime.privateRequest(), 401);
|
|
108
|
+
if ((await runtime.mails()).length !== 0) throw new Error('Bootstrap must not invoke mail.');
|
|
109
|
+
|
|
110
|
+
const ownerCookie = await bootstrap(runtime, config);
|
|
111
|
+
const session = await json(await runtime.call('/auth/session', { cookie: ownerCookie }));
|
|
112
|
+
equal(session.operator.email, config.OWNER_EMAIL, 'bootstrap owner email');
|
|
113
|
+
equal(session.operator.role, 'owner', 'bootstrap owner role');
|
|
114
|
+
equal(session.kind, 'native', 'bootstrap session kind');
|
|
115
|
+
assertStatus(await runtime.privateRequest(ownerCookie), 200);
|
|
116
|
+
assertStatus(await runtime.call('/auth/bootstrap', post({ token: config.SETUP_TOKEN, displayName: 'Second Owner' })), 409);
|
|
117
|
+
const operators = await json(await runtime.call('/auth/operators', { cookie: ownerCookie }));
|
|
118
|
+
equal(operators.length, 1, 'only the configured operator exists');
|
|
119
|
+
equal((await runtime.mails()).length, 0, 'bootstrap remains mail-free');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
scenario('magic-link preview is read-only; POST confirms once and keeps its stored returnTo', {}, async (runtime, config) => {
|
|
123
|
+
const ownerCookie = await bootstrap(runtime, config);
|
|
124
|
+
const issued = await runtime.call('/auth/magic-link', post({ email: config.OWNER_EMAIL, returnTo: '/app/queue?sort=week' }));
|
|
125
|
+
assertStatus(issued, 202);
|
|
126
|
+
const link = await capturedLink(runtime);
|
|
127
|
+
const path = `${link.pathname}${link.search}`;
|
|
128
|
+
|
|
129
|
+
for (const method of ['GET', 'HEAD']) {
|
|
130
|
+
const preview = await runtime.call(path, { method });
|
|
131
|
+
assertStatus(preview, 200);
|
|
132
|
+
equal(preview.headers.get('set-cookie'), null, `${method} preview has no session cookie`);
|
|
133
|
+
}
|
|
134
|
+
const confirmed = await runtime.call('/auth/confirm', post({ token: link.searchParams.get('token'), returnTo: 'https://attacker.example/' }));
|
|
135
|
+
assertStatus(confirmed, 303);
|
|
136
|
+
equal(confirmed.headers.get('location'), '/app/queue?sort=week', 'confirmation uses the issued safe returnTo');
|
|
137
|
+
const confirmedCookie = cookie(confirmed);
|
|
138
|
+
const session = await json(await runtime.call('/auth/session', { cookie: confirmedCookie }));
|
|
139
|
+
equal(session.operator.email, config.OWNER_EMAIL, 'link session owner');
|
|
140
|
+
assertStatus(await runtime.call('/auth/confirm', post({ token: link.searchParams.get('token') })), 400);
|
|
141
|
+
assertStatus(await runtime.privateRequest(confirmedCookie), 200);
|
|
142
|
+
assertStatus(await runtime.privateRequest('operator_session_missing=invalid'), 401);
|
|
143
|
+
if (!ownerCookie.startsWith('__Host-operator_session=')) throw new Error('Production auth cookie must use the frozen __Host name.');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
scenario('confirmation and multipart mail share the consumer\'s public branding', {}, async (runtime, config) => {
|
|
147
|
+
const owner = await bootstrap(runtime, config);
|
|
148
|
+
const productName = branding.productName ?? 'your workspace';
|
|
149
|
+
for (const kind of ['magic', 'invite']) {
|
|
150
|
+
const response = await runtime.call(kind === 'magic' ? '/auth/magic-link' : '/auth/invite', post(kind === 'magic'
|
|
151
|
+
? { email: config.OWNER_EMAIL } : { email: 'presentation@example.test', displayName: 'Presentation' }, owner));
|
|
152
|
+
assertStatus(response, 202);
|
|
153
|
+
const link = await capturedLink(runtime);
|
|
154
|
+
const token = link.searchParams.get('token');
|
|
155
|
+
checkMail((await runtime.mails()).at(-1), { kind, productName, token });
|
|
156
|
+
const path = `${link.pathname}${link.search}`;
|
|
157
|
+
const preview = await runtime.call(path);
|
|
158
|
+
const headers = [...preview.headers];
|
|
159
|
+
await checkConfirmation(preview, { kind, productName, token, confirmationFonts: adapter.confirmationFonts === true, theme: branding.theme });
|
|
160
|
+
const head = await runtime.call(path, { method: 'HEAD' });
|
|
161
|
+
assertStatus(head, 200);
|
|
162
|
+
equal(await head.text(), '', 'HEAD has no body');
|
|
163
|
+
for (const name of ['content-security-policy', 'referrer-policy', 'content-type', 'cache-control']) {
|
|
164
|
+
equal(head.headers.get(name), headers.find(([key]) => key === name)?.[1], 'HEAD presentation headers');
|
|
165
|
+
}
|
|
166
|
+
assertStatus(await runtime.call('/auth/confirm', post({ token })), 303);
|
|
167
|
+
await checkConfirmation(await runtime.call(path), { productName, confirmationFonts: adapter.confirmationFonts === true, theme: branding.theme });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
scenario('default roles constrain administration and revoke sessions on role change and explicit revoke', {}, async (runtime, config) => {
|
|
172
|
+
const ownerCookie = await bootstrap(runtime, config);
|
|
173
|
+
const owner = await json(await runtime.call('/auth/session', { cookie: ownerCookie }));
|
|
174
|
+
|
|
175
|
+
assertStatus(await runtime.call('/auth/invite', post({ email: 'admin@example.test', displayName: 'Admin', role: 'admin' }, ownerCookie)), 202);
|
|
176
|
+
const adminJoin = await confirmCaptured(runtime);
|
|
177
|
+
assertStatus(adminJoin.response, 303);
|
|
178
|
+
const admin = await json(await runtime.call('/auth/session', { cookie: adminJoin.cookie }));
|
|
179
|
+
equal(admin.operator.role, 'admin', 'invited admin role');
|
|
180
|
+
assertStatus(await runtime.call('/auth/operators', { cookie: adminJoin.cookie }), 200);
|
|
181
|
+
assertStatus(await runtime.call('/auth/confirm', post({ token: adminJoin.link.searchParams.get('token') })), 400);
|
|
182
|
+
|
|
183
|
+
assertStatus(await runtime.call('/auth/operator/role', post({ operatorId: owner.operator.id, role: 'admin' }, adminJoin.cookie)), 403);
|
|
184
|
+
assertStatus(await runtime.call('/auth/invite', post({ email: 'other-admin@example.test', displayName: 'Other Admin', role: 'admin' }, adminJoin.cookie)), 403);
|
|
185
|
+
assertStatus(await runtime.call('/auth/invite', post({ email: 'member@example.test', displayName: 'Member' }, adminJoin.cookie)), 202);
|
|
186
|
+
const staleMemberLink = await capturedLink(runtime);
|
|
187
|
+
|
|
188
|
+
assertStatus(await runtime.call('/auth/operator/role', post({ operatorId: admin.operator.id, role: 'member' }, ownerCookie)), 200);
|
|
189
|
+
assertStatus(await runtime.call('/auth/session', { cookie: adminJoin.cookie }), 401);
|
|
190
|
+
assertStatus(await runtime.privateRequest(adminJoin.cookie), 401);
|
|
191
|
+
assertStatus(await runtime.call('/auth/confirm', post({ token: staleMemberLink.searchParams.get('token') })), 400);
|
|
192
|
+
|
|
193
|
+
assertStatus(await runtime.call('/auth/invite', post({ email: 'member@example.test', displayName: 'Member' }, ownerCookie)), 202);
|
|
194
|
+
const memberJoin = await confirmCaptured(runtime);
|
|
195
|
+
assertStatus(memberJoin.response, 303);
|
|
196
|
+
const member = await json(await runtime.call('/auth/session', { cookie: memberJoin.cookie }));
|
|
197
|
+
equal(member.operator.role, 'member', 'invitation default role');
|
|
198
|
+
assertStatus(await runtime.call('/auth/operators', { cookie: memberJoin.cookie }), 403);
|
|
199
|
+
assertStatus(await runtime.call('/auth/confirm', post({ token: memberJoin.link.searchParams.get('token') })), 400);
|
|
200
|
+
|
|
201
|
+
const revoke = await runtime.call('/auth/session/revoke', post({ sessionId: member.id }, ownerCookie));
|
|
202
|
+
assertStatus(revoke, 204);
|
|
203
|
+
assertStatus(await runtime.call('/auth/session', { cookie: memberJoin.cookie }), 401);
|
|
204
|
+
assertStatus(await runtime.privateRequest(memberJoin.cookie), 401);
|
|
205
|
+
const sessions = await json(await runtime.call(`/auth/sessions?operatorId=${encodeURIComponent(member.operator.id)}`, { cookie: ownerCookie }));
|
|
206
|
+
equal(sessions.some((session) => session.id === member.id && session.active === false), true, 'revoked session is visible only as inactive metadata');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
scenario('allow-list masking and independent IP/email limits fail closed before mail', { allowList: ['owner@example.test'], ipLimit: 1, emailLimit: 1 }, async (runtime, config) => {
|
|
210
|
+
await bootstrap(runtime, config);
|
|
211
|
+
const blocked = await runtime.call('/auth/magic-link', post({ email: 'blocked@example.test' }, undefined, { ip: '192.0.2.20' }));
|
|
212
|
+
assertStatus(blocked, 202);
|
|
213
|
+
equal((await runtime.mails()).length, 0, 'disallowed address is masked and not mailed');
|
|
214
|
+
|
|
215
|
+
const admitted = await runtime.call('/auth/magic-link', post({ email: config.OWNER_EMAIL }, undefined, { ip: '192.0.2.30' }));
|
|
216
|
+
assertStatus(admitted, 202);
|
|
217
|
+
const emailLimit = await runtime.call('/auth/magic-link', post({ email: config.OWNER_EMAIL }, undefined, { ip: '192.0.2.31' }));
|
|
218
|
+
assertStatus(emailLimit, 429);
|
|
219
|
+
const ipLimit = await runtime.call('/auth/magic-link', post({ email: 'another@example.test' }, undefined, { ip: '192.0.2.30' }));
|
|
220
|
+
assertStatus(ipLimit, 429);
|
|
221
|
+
equal((await runtime.mails()).length, 1, 'limited or disallowed requests do not send');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
scenario('local review rejects forwarded-Host spoofing at issue and session admission', { localReview: true, noMail: true }, async (runtime, config) => {
|
|
225
|
+
const ownerCookie = await bootstrap(runtime, config);
|
|
226
|
+
for (const host of ['evil.test', 'localhost.evil.test', '127.0.0.2:8787']) {
|
|
227
|
+
assertStatus(await runtime.call('/auth/review-session', post({}, ownerCookie, {
|
|
228
|
+
headers: { Host: host, 'X-Forwarded-Host': 'localhost:8787', Forwarded: 'host=localhost:8787' },
|
|
229
|
+
})), 403);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const issued = await runtime.call('/auth/review-session', post({ returnTo: '/app/review' }, ownerCookie));
|
|
233
|
+
assertStatus(issued, 303);
|
|
234
|
+
equal(issued.headers.get('location'), '/app/review', 'review session safe returnTo');
|
|
235
|
+
const reviewCookie = cookie(issued);
|
|
236
|
+
if (!reviewCookie.startsWith(`operator_session_local_${new URL(config.origin).port}=`)) throw new Error('Local review must use the local-only cookie name for its own port.');
|
|
237
|
+
const reviewSession = await json(await runtime.call('/auth/session', { cookie: reviewCookie }));
|
|
238
|
+
equal(reviewSession.kind, 'review', 'review session kind');
|
|
239
|
+
assertStatus(await runtime.privateRequest(reviewCookie), 200);
|
|
240
|
+
assertStatus(await runtime.call('/auth/session', { cookie: reviewCookie, headers: {
|
|
241
|
+
Host: 'evil.test', 'X-Forwarded-Host': 'localhost:8787', Forwarded: 'host=localhost:8787',
|
|
242
|
+
} }), 403);
|
|
243
|
+
assertStatus(await runtime.privateRequest(reviewCookie, { headers: {
|
|
244
|
+
Host: 'evil.test', 'X-Forwarded-Host': 'localhost:8787', Forwarded: 'host=localhost:8787',
|
|
245
|
+
} }), 403);
|
|
246
|
+
equal((await runtime.mails()).length, 0, 'local review has no mail side effect');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
for (const mailStatus of MAIL_STATUSES) {
|
|
250
|
+
scenario(`IF-056 ${mailStatus} mail result is handled explicitly`, { mailStatus }, async (runtime, config) => {
|
|
251
|
+
await bootstrap(runtime, config);
|
|
252
|
+
equal((await runtime.mails()).length, 0, 'owner bootstrap does not use the mail adapter');
|
|
253
|
+
const response = await runtime.call('/auth/magic-link', post({ email: config.OWNER_EMAIL }));
|
|
254
|
+
if (mailStatus === 'accepted') {
|
|
255
|
+
assertStatus(response, 202);
|
|
256
|
+
const link = await capturedLink(runtime);
|
|
257
|
+
assertStatus(await runtime.call(`${link.pathname}${link.search}`), 200);
|
|
258
|
+
} else {
|
|
259
|
+
assertStatus(response, 503);
|
|
260
|
+
equal((await json(response)).error, 'mail_unavailable', `${mailStatus} is not reported as accepted`);
|
|
261
|
+
const link = await capturedLink(runtime);
|
|
262
|
+
assertStatus(await runtime.call(`${link.pathname}${link.search}`), 400);
|
|
263
|
+
}
|
|
264
|
+
equal((await runtime.mails()).length, 1, 'test-only adapter captured exactly one attempted message');
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (adapter.oidcIssuer) {
|
|
269
|
+
scenario('optional verified OIDC admission shares native owner sessions without provider credentials', {}, async (runtime, config) => {
|
|
270
|
+
if (typeof runtime.admitVerifiedIdentity !== 'function') {
|
|
271
|
+
throw new TypeError('An adapter with oidcIssuer must implement test-only admitVerifiedIdentity().');
|
|
272
|
+
}
|
|
273
|
+
const ownerCookie = await bootstrap(runtime, config);
|
|
274
|
+
assertStatus(await runtime.call('/auth/settings/identity', post({ idpSignIn: true }, ownerCookie)), 200);
|
|
275
|
+
const completed = await runtime.admitVerifiedIdentity({
|
|
276
|
+
issuer: adapter.oidcIssuer,
|
|
277
|
+
subject: 'conformance-subject',
|
|
278
|
+
email: config.OWNER_EMAIL,
|
|
279
|
+
emailVerified: true,
|
|
280
|
+
}, '/app/oidc');
|
|
281
|
+
equal(completed.session.kind, 'oidc', 'OIDC admission uses the shared session table');
|
|
282
|
+
equal(completed.returnTo, '/app/oidc', 'OIDC admission preserves safe returnTo');
|
|
283
|
+
const session = await json(await runtime.call('/auth/session', { cookie: completed.cookie.split(';', 1)[0] }));
|
|
284
|
+
equal(session.id, completed.session.id, 'OIDC and native endpoints read the same session');
|
|
285
|
+
assertStatus(await runtime.privateRequest(completed.cookie.split(';', 1)[0]), 200);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function json(response) {
|
|
291
|
+
return response.json();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function assertStatus(response, expected) {
|
|
295
|
+
if (response.status !== expected) throw new Error(`Expected HTTP ${expected}; received ${response.status}.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function equal(actual, expected, label) {
|
|
299
|
+
if (!Object.is(actual, expected)) throw new Error(`${label}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`);
|
|
300
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shared black-box presentation checks run against each consumer's actual Worker.
|
|
2
|
+
export async function checkConfirmation(response, { kind, productName, minutes = 15, token, signInPath = '/signin', confirmationFonts = false, theme }) {
|
|
3
|
+
const valid = !!kind;
|
|
4
|
+
expect(response.status === (valid ? 200 : 400), 'confirmation status');
|
|
5
|
+
expect(response.headers.get('set-cookie') === null, 'preview must be cookie-free');
|
|
6
|
+
expect(response.headers.get('referrer-policy') === (valid ? 'strict-origin' : 'no-referrer'), 'preview referrer policy');
|
|
7
|
+
const html = await response.text();
|
|
8
|
+
const styles = [...html.matchAll(/<style>([\s\S]*?)<\/style>/g)];
|
|
9
|
+
expect(styles.length === 1, 'one self-contained stylesheet');
|
|
10
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(styles[0][1]));
|
|
11
|
+
const hash = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
|
12
|
+
const fontsPolicy = confirmationFonts ? " 'self'; font-src 'self'" : '';
|
|
13
|
+
// A declared seed theme is one same-origin stylesheet whose fonts and logo the app serves.
|
|
14
|
+
const sources = theme ? `style-src 'self' 'sha256-${hash}'; font-src 'self'; img-src 'self'` : `style-src 'sha256-${hash}'${fontsPolicy}`;
|
|
15
|
+
expect(response.headers.get('content-security-policy') === `default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'; ${sources}`, 'only approved stylesheet and font CSP sources');
|
|
16
|
+
const themeLink = theme && `<link rel="stylesheet" href="${escape(theme)}">`;
|
|
17
|
+
const allLinks = [...html.matchAll(/<link\b[^>]*>/gi)].map(match => match[0]);
|
|
18
|
+
expect(allLinks.filter(link => link === themeLink).length === (theme ? 1 : 0), 'the declared theme stylesheet only');
|
|
19
|
+
expect(!theme || html.startsWith('<!doctype html><html lang="en" class="rer-themed">'), 'themed document');
|
|
20
|
+
const links = allLinks.filter(link => link !== themeLink);
|
|
21
|
+
if (confirmationFonts) {
|
|
22
|
+
expect(links.length === 2, 'one font stylesheet and one primary preload');
|
|
23
|
+
const resources = links.map(link => {
|
|
24
|
+
const attributes = [...link.matchAll(/\s([a-z-]+)="([^"]*)"/g)];
|
|
25
|
+
const attrs = Object.fromEntries(attributes.map(match => [match[1], match[2]]));
|
|
26
|
+
expect(attributes.length === Object.keys(attrs).length && Object.keys(attrs).every(name => ['rel', 'href', 'as', 'type', 'crossorigin'].includes(name)), 'font links have no event or unexpected attributes');
|
|
27
|
+
expect(link.replace(/\s[a-z-]+="[^"]*"/g, '').match(/^<link\s*\/?>$/i), 'font links contain only parsed attributes');
|
|
28
|
+
expect(typeof attrs.href === 'string' && attrs.href.startsWith('/') && !attrs.href.startsWith('//') && !/[\\&?#]/.test(attrs.href) && new URL(attrs.href, 'https://font.conformance.test').origin === 'https://font.conformance.test', 'font resources use fixed same-origin paths without credentials');
|
|
29
|
+
return attrs;
|
|
30
|
+
});
|
|
31
|
+
expect(resources.filter(attrs => attrs.rel === 'stylesheet' && /\.css$/i.test(attrs.href) && Object.keys(attrs).length === 2).length === 1, 'one same-origin font stylesheet');
|
|
32
|
+
expect(resources.filter(attrs => attrs.rel === 'preload' && attrs.as === 'font' && /^font\//.test(attrs.type ?? '') && attrs.crossorigin === 'anonymous' && /\.(woff2?|ttf|otf)$/i.test(attrs.href)).length === 1, 'one reusable primary font preload');
|
|
33
|
+
} else expect(links.length === 0, 'standalone confirmation remains self-contained');
|
|
34
|
+
expect(!/<script|<img|\bon[a-z]+\s*=|style=|url\s*\(|@import|https?:\/\//i.test(html.replace(/<link\b[^>]*>/gi, '')), 'no other resources or inline attributes');
|
|
35
|
+
expect(html.includes(escape(productName)), 'escaped product name');
|
|
36
|
+
if (valid) {
|
|
37
|
+
expect(html.includes(`<h1>${kind === 'invite' ? 'Accept your invitation to' : 'Confirm sign-in to'} ${escape(productName)}</h1>`), 'plain confirmation heading');
|
|
38
|
+
expect(html.includes(`This link works once and expires in ${minutes} minutes.`), 'preview lifetime');
|
|
39
|
+
expect(html.includes('<button type="submit">Confirm</button>'), 'explicit confirmation control');
|
|
40
|
+
expect(html.includes(`<input type="hidden" name="token" value="${token}">`) && html.split(token).length === 2, 'token appears only in hidden field');
|
|
41
|
+
} else {
|
|
42
|
+
expect(html.includes(`href="${escape(signInPath)}">Request a new link`), 'configured replacement link');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkMail(message, { kind, productName, token }) {
|
|
47
|
+
expect(message.subject === (kind === 'invite' ? `You're invited to ${productName}` : `Your sign-in link for ${productName}`), 'plain mail subject');
|
|
48
|
+
expect(!message.subject.includes(token), 'subject contains no credential');
|
|
49
|
+
for (const part of [message.text, message.html]) {
|
|
50
|
+
expect(typeof part === 'string' && part.includes('This link works once and expires in 15 minutes.'), 'multipart expiry');
|
|
51
|
+
expect(part.split(token).length === 2 && (part.match(/https?:\/\//g) ?? []).length === 1, 'one confirmation URL per part');
|
|
52
|
+
}
|
|
53
|
+
expect((message.html.match(/<a /g) ?? []).length === 1 && message.html.includes('>Confirm</a>'), 'one confirmation mail button');
|
|
54
|
+
expect(message.html.includes(escape(productName)), 'escaped mail product name');
|
|
55
|
+
}
|
|
56
|
+
function escape(value) { return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
57
|
+
function expect(value, label) { if (!value) throw new Error(`Operator-auth presentation contract failed: ${label}`); }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
export function opaqueToken() {
|
|
3
|
+
return btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))
|
|
4
|
+
.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
|
|
5
|
+
}
|
|
6
|
+
export function validToken(value) { return typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value); }
|
|
7
|
+
export async function hashToken(secret, purpose, token) {
|
|
8
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
9
|
+
const bytes = await crypto.subtle.sign('HMAC', key, encoder.encode(`${purpose}\0${token}`));
|
|
10
|
+
return Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
11
|
+
}
|
|
12
|
+
export function constantEqual(a, b) {
|
|
13
|
+
if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) return false;
|
|
14
|
+
let mismatch = 0;
|
|
15
|
+
for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
16
|
+
return mismatch === 0;
|
|
17
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EXCHANGE_SCRIPT, exchangeHashes } from '../dev-gate.mjs';
|
|
2
|
+
import { escapeHtml } from '../http.mjs';
|
|
3
|
+
|
|
4
|
+
// A slim bar at the top of the document, in normal flow so the page below it
|
|
5
|
+
// starts after it and nothing is ever covered. A grid body (the frames that
|
|
6
|
+
// centre their card) gives the bar its own auto row and keeps the card centred
|
|
7
|
+
// in the rest.
|
|
8
|
+
const STYLE = 'body:has(>#rer-dev-badge){grid-template-rows:auto 1fr}#rer-dev-badge{grid-column:1/-1;justify-self:stretch;align-self:start;position:relative;z-index:2147483647;box-sizing:border-box;width:100%;margin:0 0 12px;padding:6px 12px;border-radius:8px;background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;text-align:left}#rer-dev-badge summary{cursor:pointer;overflow-wrap:anywhere}#rer-dev-badge ul{margin:6px 0 0;padding:0;list-style:none;display:flex;flex-wrap:wrap;gap:4px 16px}#rer-dev-badge li{margin:0}#rer-dev-badge a{display:inline-block;min-height:24px;color:#bfdbfe;text-decoration:underline}#rer-dev-badge a[aria-current]{color:#fff;font-weight:700}#rer-dev-badge a:focus-visible,#rer-dev-badge summary:focus-visible{outline:2px solid #fff;outline-offset:2px}';
|
|
9
|
+
let styleHash;
|
|
10
|
+
async function sha256(text) {
|
|
11
|
+
const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
12
|
+
return `'sha256-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}'`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function badgeHtml({ session, choices }) {
|
|
16
|
+
const current = session ? choices.find(choice => choice.email === session.operator.email)?.as : 'signed-out';
|
|
17
|
+
const label = session ? `Dev: ${escapeHtml(session.operator.email)} (${escapeHtml(session.operator.role)})` : 'Dev: signed out';
|
|
18
|
+
const links = [...choices.map(choice => choice.as), 'signed-out'].map(as => `<li><a href="/auth/dev/enter?as=${encodeURIComponent(as)}"${as === current ? ' aria-current="true"' : ''}>${as === 'signed-out' ? 'Sign out' : `Switch to ${escapeHtml(as)}`}</a></li>`).join('');
|
|
19
|
+
return `<style>${STYLE}</style><aside id="rer-dev-badge" aria-label="Local development identity"><details><summary>${label}</summary><ul>${links}</ul></details></aside>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Allow exactly one more inline source in a directive the document already
|
|
23
|
+
// restricts. Inline content an existing directive already permits stays as is.
|
|
24
|
+
function allow(policy, directive, source) {
|
|
25
|
+
const parts = policy.split(';').map(part => part.trim()).filter(Boolean);
|
|
26
|
+
const find = name => parts.findIndex(part => part.split(/\s+/)[0].toLowerCase() === name);
|
|
27
|
+
let index = find(directive);
|
|
28
|
+
if (index < 0) {
|
|
29
|
+
const fallback = find('default-src');
|
|
30
|
+
if (fallback < 0) return policy;
|
|
31
|
+
parts.push([directive, ...parts[fallback].split(/\s+/).slice(1)].join(' '));
|
|
32
|
+
index = parts.length - 1;
|
|
33
|
+
}
|
|
34
|
+
const tokens = parts[index].split(/\s+/);
|
|
35
|
+
if (tokens.includes("'unsafe-inline'") && !tokens.some(token => /^'(?:nonce|sha256|sha384|sha512)-/.test(token))) return parts.join('; ');
|
|
36
|
+
parts[index] = [...tokens.filter(token => token !== "'none'"), source].join(' ');
|
|
37
|
+
return parts.join('; ');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adds the fragment key exchange (root only) and the badge to an HTML document. */
|
|
41
|
+
export async function decorate(response, { exchange, badge }) {
|
|
42
|
+
if ((!exchange && !badge) || !response.headers.get('content-type')?.toLowerCase().startsWith('text/html') || !response.body) return response;
|
|
43
|
+
let bar = '', scripts = '';
|
|
44
|
+
const headers = new Headers(response.headers);
|
|
45
|
+
let policy = headers.get('content-security-policy');
|
|
46
|
+
if (badge) {
|
|
47
|
+
bar = badgeHtml(badge);
|
|
48
|
+
if (policy) policy = allow(policy, 'style-src', styleHash ??= await sha256(STYLE));
|
|
49
|
+
}
|
|
50
|
+
if (exchange) {
|
|
51
|
+
scripts = `<script>${EXCHANGE_SCRIPT}</script>`;
|
|
52
|
+
if (policy) policy = allow(allow(policy, 'script-src', (await exchangeHashes()).script), 'connect-src', "'self'");
|
|
53
|
+
}
|
|
54
|
+
if (policy) headers.set('content-security-policy', policy);
|
|
55
|
+
headers.delete('content-length');
|
|
56
|
+
// The bar opens the body; the key exchange script stays at its end.
|
|
57
|
+
let html = await response.text();
|
|
58
|
+
const open = /<body\b[^>]*>/i.exec(html), at = open ? open.index + open[0].length : 0, end = html.lastIndexOf('</body>');
|
|
59
|
+
if (end >= at) html = html.slice(0, end) + scripts + html.slice(end); else html += scripts;
|
|
60
|
+
html = html.slice(0, at) + bar + html.slice(at);
|
|
61
|
+
return new Response(html, { status: response.status, statusText: response.statusText, headers });
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthConfig, Role } from '../index.mjs';
|
|
2
|
+
export const DEV_LAYER_MARKER: string;
|
|
3
|
+
export interface DevPersona { as: string; role: Role; displayName?: string }
|
|
4
|
+
export interface DevIdentityOptions<Env> {
|
|
5
|
+
/** The seed's own binding normalization (for example its RER_DEV_ORIGIN override). */
|
|
6
|
+
bindings?: (env: Env) => Env;
|
|
7
|
+
/** The seed's own operator-auth configuration. */
|
|
8
|
+
config: (env: Env) => AuthConfig;
|
|
9
|
+
/** The seed's devPersonas(): real operators at `<as>@dev.localhost`, created only in local dev. */
|
|
10
|
+
personas?: () => DevPersona[];
|
|
11
|
+
}
|
|
12
|
+
export function managedInstallation(env: Record<string, unknown>): boolean;
|
|
13
|
+
/** Local-only Worker wrapper. Never import from a deployed entry. */
|
|
14
|
+
export function withDevIdentity<Env, Handler extends { fetch?: (request: never, env: Env, execution: never) => unknown }>(worker: Handler, options: DevIdentityOptions<Env>): Handler;
|