@antasphere/chassis-server 1.0.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 +82 -0
- package/dist/accounts/deletion.d.ts +163 -0
- package/dist/accounts/deletion.d.ts.map +1 -0
- package/dist/accounts/deletion.js +360 -0
- package/dist/accounts/deletion.js.map +1 -0
- package/dist/accounts/erasure-log.d.ts +66 -0
- package/dist/accounts/erasure-log.d.ts.map +1 -0
- package/dist/accounts/erasure-log.js +117 -0
- package/dist/accounts/erasure-log.js.map +1 -0
- package/dist/accounts/index.d.ts +6 -0
- package/dist/accounts/index.d.ts.map +1 -0
- package/dist/accounts/index.js +6 -0
- package/dist/accounts/index.js.map +1 -0
- package/dist/accounts/mint-refusal.d.ts +47 -0
- package/dist/accounts/mint-refusal.d.ts.map +1 -0
- package/dist/accounts/mint-refusal.js +59 -0
- package/dist/accounts/mint-refusal.js.map +1 -0
- package/dist/accounts/signup-duplicate.d.ts +14 -0
- package/dist/accounts/signup-duplicate.d.ts.map +1 -0
- package/dist/accounts/signup-duplicate.js +25 -0
- package/dist/accounts/signup-duplicate.js.map +1 -0
- package/dist/accounts/superadmin.d.ts +27 -0
- package/dist/accounts/superadmin.d.ts.map +1 -0
- package/dist/accounts/superadmin.js +46 -0
- package/dist/accounts/superadmin.js.map +1 -0
- package/dist/api/apikeys.d.ts +14 -0
- package/dist/api/apikeys.d.ts.map +1 -0
- package/dist/api/apikeys.js +124 -0
- package/dist/api/apikeys.js.map +1 -0
- package/dist/api/audit.d.ts +4 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +75 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/break-glass.d.ts +48 -0
- package/dist/api/break-glass.d.ts.map +1 -0
- package/dist/api/break-glass.js +217 -0
- package/dist/api/break-glass.js.map +1 -0
- package/dist/api/cli-auth.d.ts +84 -0
- package/dist/api/cli-auth.d.ts.map +1 -0
- package/dist/api/cli-auth.js +177 -0
- package/dist/api/cli-auth.js.map +1 -0
- package/dist/api/create-api.d.ts +123 -0
- package/dist/api/create-api.d.ts.map +1 -0
- package/dist/api/create-api.js +1165 -0
- package/dist/api/create-api.js.map +1 -0
- package/dist/api/demo-passes.d.ts +45 -0
- package/dist/api/demo-passes.d.ts.map +1 -0
- package/dist/api/demo-passes.js +143 -0
- package/dist/api/demo-passes.js.map +1 -0
- package/dist/api/errors.d.ts +5 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +5 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/export.d.ts +63 -0
- package/dist/api/export.d.ts.map +1 -0
- package/dist/api/export.js +393 -0
- package/dist/api/export.js.map +1 -0
- package/dist/api/files.d.ts +40 -0
- package/dist/api/files.d.ts.map +1 -0
- package/dist/api/files.js +150 -0
- package/dist/api/files.js.map +1 -0
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/invitations.d.ts +50 -0
- package/dist/api/invitations.d.ts.map +1 -0
- package/dist/api/invitations.js +313 -0
- package/dist/api/invitations.js.map +1 -0
- package/dist/api/members.d.ts +25 -0
- package/dist/api/members.d.ts.map +1 -0
- package/dist/api/members.js +522 -0
- package/dist/api/members.js.map +1 -0
- package/dist/api/onboarding.d.ts +26 -0
- package/dist/api/onboarding.d.ts.map +1 -0
- package/dist/api/onboarding.js +23 -0
- package/dist/api/onboarding.js.map +1 -0
- package/dist/api/openapi-doc.d.ts +21 -0
- package/dist/api/openapi-doc.d.ts.map +1 -0
- package/dist/api/openapi-doc.js +25 -0
- package/dist/api/openapi-doc.js.map +1 -0
- package/dist/api/projects.d.ts +24 -0
- package/dist/api/projects.d.ts.map +1 -0
- package/dist/api/projects.js +624 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/scope-routes.d.ts +12 -0
- package/dist/api/scope-routes.d.ts.map +1 -0
- package/dist/api/scope-routes.js +2 -0
- package/dist/api/scope-routes.js.map +1 -0
- package/dist/api/sso-connect.d.ts +70 -0
- package/dist/api/sso-connect.d.ts.map +1 -0
- package/dist/api/sso-connect.js +164 -0
- package/dist/api/sso-connect.js.map +1 -0
- package/dist/api/sso-logout.d.ts +41 -0
- package/dist/api/sso-logout.d.ts.map +1 -0
- package/dist/api/sso-logout.js +34 -0
- package/dist/api/sso-logout.js.map +1 -0
- package/dist/api/teams.d.ts +43 -0
- package/dist/api/teams.d.ts.map +1 -0
- package/dist/api/teams.js +447 -0
- package/dist/api/teams.js.map +1 -0
- package/dist/api/workspaces.d.ts +114 -0
- package/dist/api/workspaces.d.ts.map +1 -0
- package/dist/api/workspaces.js +462 -0
- package/dist/api/workspaces.js.map +1 -0
- package/dist/apikeys/index.d.ts +3 -0
- package/dist/apikeys/index.d.ts.map +1 -0
- package/dist/apikeys/index.js +3 -0
- package/dist/apikeys/index.js.map +1 -0
- package/dist/apikeys/peppers.d.ts +53 -0
- package/dist/apikeys/peppers.d.ts.map +1 -0
- package/dist/apikeys/peppers.js +97 -0
- package/dist/apikeys/peppers.js.map +1 -0
- package/dist/apikeys/service.d.ts +56 -0
- package/dist/apikeys/service.d.ts.map +1 -0
- package/dist/apikeys/service.js +196 -0
- package/dist/apikeys/service.js.map +1 -0
- package/dist/app.d.ts +65 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +142 -0
- package/dist/app.js.map +1 -0
- package/dist/audit/filters.d.ts +38 -0
- package/dist/audit/filters.d.ts.map +1 -0
- package/dist/audit/filters.js +97 -0
- package/dist/audit/filters.js.map +1 -0
- package/dist/audit/index.d.ts +3 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +3 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/service.d.ts +65 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +103 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/boot.d.ts +8 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +822 -0
- package/dist/boot.js.map +1 -0
- package/dist/constant-time.d.ts +3 -0
- package/dist/constant-time.d.ts.map +1 -0
- package/dist/constant-time.js +10 -0
- package/dist/constant-time.js.map +1 -0
- package/dist/email/driver.d.ts +16 -0
- package/dist/email/driver.d.ts.map +1 -0
- package/dist/email/driver.js +136 -0
- package/dist/email/driver.js.map +1 -0
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +4 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/shell.d.ts +72 -0
- package/dist/email/shell.d.ts.map +1 -0
- package/dist/email/shell.js +146 -0
- package/dist/email/shell.js.map +1 -0
- package/dist/email/templates.d.ts +83 -0
- package/dist/email/templates.d.ts.map +1 -0
- package/dist/email/templates.js +96 -0
- package/dist/email/templates.js.map +1 -0
- package/dist/entitlements/check.d.ts +140 -0
- package/dist/entitlements/check.d.ts.map +1 -0
- package/dist/entitlements/check.js +276 -0
- package/dist/entitlements/check.js.map +1 -0
- package/dist/entitlements/clock-skew.d.ts +27 -0
- package/dist/entitlements/clock-skew.d.ts.map +1 -0
- package/dist/entitlements/clock-skew.js +58 -0
- package/dist/entitlements/clock-skew.js.map +1 -0
- package/dist/entitlements/gate.d.ts +113 -0
- package/dist/entitlements/gate.d.ts.map +1 -0
- package/dist/entitlements/gate.js +430 -0
- package/dist/entitlements/gate.js.map +1 -0
- package/dist/entitlements/hub-machine-token.d.ts +83 -0
- package/dist/entitlements/hub-machine-token.d.ts.map +1 -0
- package/dist/entitlements/hub-machine-token.js +175 -0
- package/dist/entitlements/hub-machine-token.js.map +1 -0
- package/dist/entitlements/hub-subject.d.ts +32 -0
- package/dist/entitlements/hub-subject.d.ts.map +1 -0
- package/dist/entitlements/hub-subject.js +68 -0
- package/dist/entitlements/hub-subject.js.map +1 -0
- package/dist/entitlements/index.d.ts +9 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +9 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/poster.d.ts +81 -0
- package/dist/entitlements/poster.d.ts.map +1 -0
- package/dist/entitlements/poster.js +169 -0
- package/dist/entitlements/poster.js.map +1 -0
- package/dist/entitlements/profiles.d.ts +121 -0
- package/dist/entitlements/profiles.d.ts.map +1 -0
- package/dist/entitlements/profiles.js +212 -0
- package/dist/entitlements/profiles.js.map +1 -0
- package/dist/entitlements/slot.d.ts +33 -0
- package/dist/entitlements/slot.d.ts.map +1 -0
- package/dist/entitlements/slot.js +75 -0
- package/dist/entitlements/slot.js.map +1 -0
- package/dist/env.d.ts +293 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +383 -0
- package/dist/env.js.map +1 -0
- package/dist/files/http.d.ts +23 -0
- package/dist/files/http.d.ts.map +1 -0
- package/dist/files/http.js +66 -0
- package/dist/files/http.js.map +1 -0
- package/dist/files/index.d.ts +5 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +5 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/serve.d.ts +43 -0
- package/dist/files/serve.d.ts.map +1 -0
- package/dist/files/serve.js +71 -0
- package/dist/files/serve.js.map +1 -0
- package/dist/files/service.d.ts +69 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +188 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/spool.d.ts +21 -0
- package/dist/files/spool.d.ts.map +1 -0
- package/dist/files/spool.js +47 -0
- package/dist/files/spool.js.map +1 -0
- package/dist/identity/better-auth.d.ts +3927 -0
- package/dist/identity/better-auth.d.ts.map +1 -0
- package/dist/identity/better-auth.js +896 -0
- package/dist/identity/better-auth.js.map +1 -0
- package/dist/identity/demo-pass-plugin.d.ts +29 -0
- package/dist/identity/demo-pass-plugin.d.ts.map +1 -0
- package/dist/identity/demo-pass-plugin.js +88 -0
- package/dist/identity/demo-pass-plugin.js.map +1 -0
- package/dist/identity/demo-pass-rules.d.ts +84 -0
- package/dist/identity/demo-pass-rules.d.ts.map +1 -0
- package/dist/identity/demo-pass-rules.js +223 -0
- package/dist/identity/demo-pass-rules.js.map +1 -0
- package/dist/identity/demo-pass.d.ts +159 -0
- package/dist/identity/demo-pass.d.ts.map +1 -0
- package/dist/identity/demo-pass.js +319 -0
- package/dist/identity/demo-pass.js.map +1 -0
- package/dist/identity/hub-grant.d.ts +182 -0
- package/dist/identity/hub-grant.d.ts.map +1 -0
- package/dist/identity/hub-grant.js +594 -0
- package/dist/identity/hub-grant.js.map +1 -0
- package/dist/identity/hub-jwt.d.ts +22 -0
- package/dist/identity/hub-jwt.d.ts.map +1 -0
- package/dist/identity/hub-jwt.js +77 -0
- package/dist/identity/hub-jwt.js.map +1 -0
- package/dist/identity/hub-live-gate.d.ts +17 -0
- package/dist/identity/hub-live-gate.d.ts.map +1 -0
- package/dist/identity/hub-live-gate.js +146 -0
- package/dist/identity/hub-live-gate.js.map +1 -0
- package/dist/identity/hub-logout.d.ts +88 -0
- package/dist/identity/hub-logout.d.ts.map +1 -0
- package/dist/identity/hub-logout.js +158 -0
- package/dist/identity/hub-logout.js.map +1 -0
- package/dist/identity/hub-projection.d.ts +103 -0
- package/dist/identity/hub-projection.d.ts.map +1 -0
- package/dist/identity/hub-projection.js +130 -0
- package/dist/identity/hub-projection.js.map +1 -0
- package/dist/identity/hub-reconcile.d.ts +151 -0
- package/dist/identity/hub-reconcile.d.ts.map +1 -0
- package/dist/identity/hub-reconcile.js +400 -0
- package/dist/identity/hub-reconcile.js.map +1 -0
- package/dist/identity/hub-sso.d.ts +363 -0
- package/dist/identity/hub-sso.d.ts.map +1 -0
- package/dist/identity/hub-sso.js +536 -0
- package/dist/identity/hub-sso.js.map +1 -0
- package/dist/identity/hub-user-client.d.ts +222 -0
- package/dist/identity/hub-user-client.d.ts.map +1 -0
- package/dist/identity/hub-user-client.js +406 -0
- package/dist/identity/hub-user-client.js.map +1 -0
- package/dist/identity/index.d.ts +13 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +13 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/oauth-jwt.d.ts +32 -0
- package/dist/identity/oauth-jwt.d.ts.map +1 -0
- package/dist/identity/oauth-jwt.js +122 -0
- package/dist/identity/oauth-jwt.js.map +1 -0
- package/dist/identity/resolve-membership.d.ts +28 -0
- package/dist/identity/resolve-membership.d.ts.map +1 -0
- package/dist/identity/resolve-membership.js +69 -0
- package/dist/identity/resolve-membership.js.map +1 -0
- package/dist/identity/signing-key.d.ts +49 -0
- package/dist/identity/signing-key.d.ts.map +1 -0
- package/dist/identity/signing-key.js +144 -0
- package/dist/identity/signing-key.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/invitations/index.d.ts +2 -0
- package/dist/invitations/index.d.ts.map +1 -0
- package/dist/invitations/index.js +2 -0
- package/dist/invitations/index.js.map +1 -0
- package/dist/invitations/service.d.ts +36 -0
- package/dist/invitations/service.d.ts.map +1 -0
- package/dist/invitations/service.js +122 -0
- package/dist/invitations/service.js.map +1 -0
- package/dist/jobs/index.d.ts +3 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +3 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/pgboss.d.ts +119 -0
- package/dist/jobs/pgboss.d.ts.map +1 -0
- package/dist/jobs/pgboss.js +521 -0
- package/dist/jobs/pgboss.js.map +1 -0
- package/dist/jobs/timers.d.ts +127 -0
- package/dist/jobs/timers.d.ts.map +1 -0
- package/dist/jobs/timers.js +228 -0
- package/dist/jobs/timers.js.map +1 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +64 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/errors.d.ts +37 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +77 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/http.d.ts +35 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +80 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inputs.d.ts +19 -0
- package/dist/mcp/inputs.d.ts.map +1 -0
- package/dist/mcp/inputs.js +28 -0
- package/dist/mcp/inputs.js.map +1 -0
- package/dist/mcp/projects.d.ts +31 -0
- package/dist/mcp/projects.d.ts.map +1 -0
- package/dist/mcp/projects.js +315 -0
- package/dist/mcp/projects.js.map +1 -0
- package/dist/mcp/server.d.ts +52 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +93 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/teams.d.ts +7 -0
- package/dist/mcp/teams.d.ts.map +1 -0
- package/dist/mcp/teams.js +62 -0
- package/dist/mcp/teams.js.map +1 -0
- package/dist/mcp/tool-kit.d.ts +67 -0
- package/dist/mcp/tool-kit.d.ts.map +1 -0
- package/dist/mcp/tool-kit.js +74 -0
- package/dist/mcp/tool-kit.js.map +1 -0
- package/dist/members/removal.d.ts +95 -0
- package/dist/members/removal.d.ts.map +1 -0
- package/dist/members/removal.js +143 -0
- package/dist/members/removal.js.map +1 -0
- package/dist/middleware/auth-body.d.ts +25 -0
- package/dist/middleware/auth-body.d.ts.map +1 -0
- package/dist/middleware/auth-body.js +55 -0
- package/dist/middleware/auth-body.js.map +1 -0
- package/dist/middleware/auth-context.d.ts +170 -0
- package/dist/middleware/auth-context.d.ts.map +1 -0
- package/dist/middleware/auth-context.js +251 -0
- package/dist/middleware/auth-context.js.map +1 -0
- package/dist/middleware/body-cap.d.ts +17 -0
- package/dist/middleware/body-cap.d.ts.map +1 -0
- package/dist/middleware/body-cap.js +31 -0
- package/dist/middleware/body-cap.js.map +1 -0
- package/dist/middleware/body-refusal.d.ts +20 -0
- package/dist/middleware/body-refusal.d.ts.map +1 -0
- package/dist/middleware/body-refusal.js +5 -0
- package/dist/middleware/body-refusal.js.map +1 -0
- package/dist/middleware/cross-site.d.ts +61 -0
- package/dist/middleware/cross-site.d.ts.map +1 -0
- package/dist/middleware/cross-site.js +59 -0
- package/dist/middleware/cross-site.js.map +1 -0
- package/dist/middleware/hub-managed.d.ts +45 -0
- package/dist/middleware/hub-managed.d.ts.map +1 -0
- package/dist/middleware/hub-managed.js +56 -0
- package/dist/middleware/hub-managed.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +14 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +222 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +12 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +12 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/json-depth.d.ts +32 -0
- package/dist/middleware/json-depth.d.ts.map +1 -0
- package/dist/middleware/json-depth.js +80 -0
- package/dist/middleware/json-depth.js.map +1 -0
- package/dist/middleware/no-store.d.ts +21 -0
- package/dist/middleware/no-store.d.ts.map +1 -0
- package/dist/middleware/no-store.js +32 -0
- package/dist/middleware/no-store.js.map +1 -0
- package/dist/middleware/oauth-public.d.ts +16 -0
- package/dist/middleware/oauth-public.d.ts.map +1 -0
- package/dist/middleware/oauth-public.js +50 -0
- package/dist/middleware/oauth-public.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +182 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +277 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-id.d.ts +10 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +42 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/middleware/scopes.d.ts +33 -0
- package/dist/middleware/scopes.d.ts.map +1 -0
- package/dist/middleware/scopes.js +139 -0
- package/dist/middleware/scopes.js.map +1 -0
- package/dist/middleware/security-headers.d.ts +53 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +96 -0
- package/dist/middleware/security-headers.js.map +1 -0
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +3 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/metrics.d.ts +17 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +82 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/otel.d.ts +20 -0
- package/dist/observability/otel.d.ts.map +1 -0
- package/dist/observability/otel.js +54 -0
- package/dist/observability/otel.js.map +1 -0
- package/dist/pagination.d.ts +57 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +90 -0
- package/dist/pagination.js.map +1 -0
- package/dist/platform/edition.d.ts +57 -0
- package/dist/platform/edition.d.ts.map +1 -0
- package/dist/platform/edition.js +90 -0
- package/dist/platform/edition.js.map +1 -0
- package/dist/platform/entitlements.d.ts +26 -0
- package/dist/platform/entitlements.d.ts.map +1 -0
- package/dist/platform/entitlements.js +28 -0
- package/dist/platform/entitlements.js.map +1 -0
- package/dist/platform/events.d.ts +87 -0
- package/dist/platform/events.d.ts.map +1 -0
- package/dist/platform/events.js +32 -0
- package/dist/platform/events.js.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/local-identity.d.ts +39 -0
- package/dist/platform/local-identity.d.ts.map +1 -0
- package/dist/platform/local-identity.js +87 -0
- package/dist/platform/local-identity.js.map +1 -0
- package/dist/platform/registry.d.ts +23 -0
- package/dist/platform/registry.d.ts.map +1 -0
- package/dist/platform/registry.js +5 -0
- package/dist/platform/registry.js.map +1 -0
- package/dist/platform/usage.d.ts +11 -0
- package/dist/platform/usage.d.ts.map +1 -0
- package/dist/platform/usage.js +12 -0
- package/dist/platform/usage.js.map +1 -0
- package/dist/platform/workspaces.d.ts +25 -0
- package/dist/platform/workspaces.d.ts.map +1 -0
- package/dist/platform/workspaces.js +44 -0
- package/dist/platform/workspaces.js.map +1 -0
- package/dist/projects/access.d.ts +40 -0
- package/dist/projects/access.d.ts.map +1 -0
- package/dist/projects/access.js +133 -0
- package/dist/projects/access.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +2 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/route-label.d.ts +23 -0
- package/dist/route-label.d.ts.map +1 -0
- package/dist/route-label.js +29 -0
- package/dist/route-label.js.map +1 -0
- package/dist/routes/health.d.ts +30 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +54 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/wellknown.d.ts +11 -0
- package/dist/routes/wellknown.d.ts.map +1 -0
- package/dist/routes/wellknown.js +45 -0
- package/dist/routes/wellknown.js.map +1 -0
- package/dist/secret.d.ts +30 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +97 -0
- package/dist/secret.js.map +1 -0
- package/dist/setup-transport.d.ts +23 -0
- package/dist/setup-transport.d.ts.map +1 -0
- package/dist/setup-transport.js +52 -0
- package/dist/setup-transport.js.map +1 -0
- package/dist/state.d.ts +21 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +4 -0
- package/dist/state.js.map +1 -0
- package/dist/storage/driver.d.ts +38 -0
- package/dist/storage/driver.d.ts.map +1 -0
- package/dist/storage/driver.js +14 -0
- package/dist/storage/driver.js.map +1 -0
- package/dist/storage/factory.d.ts +4 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +20 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local.d.ts +21 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +88 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/s3.d.ts +29 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +82 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/testing/fake-hub.d.ts +425 -0
- package/dist/testing/fake-hub.d.ts.map +1 -0
- package/dist/testing/fake-hub.js +1074 -0
- package/dist/testing/fake-hub.js.map +1 -0
- package/dist/testing/helpers.d.ts +124 -0
- package/dist/testing/helpers.d.ts.map +1 -0
- package/dist/testing/helpers.js +174 -0
- package/dist/testing/helpers.js.map +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/setup-origins.d.ts +21 -0
- package/dist/testing/setup-origins.d.ts.map +1 -0
- package/dist/testing/setup-origins.js +57 -0
- package/dist/testing/setup-origins.js.map +1 -0
- package/dist/testing/sso-helpers.d.ts +43 -0
- package/dist/testing/sso-helpers.d.ts.map +1 -0
- package/dist/testing/sso-helpers.js +159 -0
- package/dist/testing/sso-helpers.js.map +1 -0
- package/dist/tool-definition.d.ts +382 -0
- package/dist/tool-definition.d.ts.map +1 -0
- package/dist/tool-definition.js +46 -0
- package/dist/tool-definition.js.map +1 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +8 -0
- package/dist/util/index.js.map +1 -0
- package/package.json +161 -0
- package/src/accounts/deletion.ts +389 -0
- package/src/accounts/erasure-log.ts +140 -0
- package/src/accounts/index.ts +5 -0
- package/src/accounts/mint-refusal.ts +67 -0
- package/src/accounts/signup-duplicate.ts +21 -0
- package/src/accounts/superadmin.ts +46 -0
- package/src/api/apikeys.ts +147 -0
- package/src/api/audit.ts +83 -0
- package/src/api/break-glass.ts +294 -0
- package/src/api/cli-auth.ts +308 -0
- package/src/api/create-api.ts +1403 -0
- package/src/api/demo-passes.ts +207 -0
- package/src/api/errors.ts +13 -0
- package/src/api/export.ts +482 -0
- package/src/api/files.ts +200 -0
- package/src/api/index.ts +29 -0
- package/src/api/invitations.ts +396 -0
- package/src/api/members.ts +617 -0
- package/src/api/onboarding.ts +50 -0
- package/src/api/openapi-doc.ts +34 -0
- package/src/api/projects.ts +788 -0
- package/src/api/scope-routes.ts +12 -0
- package/src/api/sso-connect.ts +246 -0
- package/src/api/sso-logout.ts +76 -0
- package/src/api/teams.ts +575 -0
- package/src/api/workspaces.ts +649 -0
- package/src/apikeys/index.ts +2 -0
- package/src/apikeys/peppers.ts +118 -0
- package/src/apikeys/service.ts +223 -0
- package/src/app.ts +228 -0
- package/src/audit/filters.ts +101 -0
- package/src/audit/index.ts +9 -0
- package/src/audit/service.ts +154 -0
- package/src/boot.ts +953 -0
- package/src/constant-time.ts +9 -0
- package/src/email/driver.ts +139 -0
- package/src/email/index.ts +26 -0
- package/src/email/shell.ts +201 -0
- package/src/email/templates.ts +184 -0
- package/src/entitlements/check.ts +435 -0
- package/src/entitlements/clock-skew.ts +58 -0
- package/src/entitlements/gate.ts +636 -0
- package/src/entitlements/hub-machine-token.ts +205 -0
- package/src/entitlements/hub-subject.ts +77 -0
- package/src/entitlements/index.ts +37 -0
- package/src/entitlements/poster.ts +265 -0
- package/src/entitlements/profiles.ts +303 -0
- package/src/entitlements/slot.ts +105 -0
- package/src/env.ts +456 -0
- package/src/files/http.ts +77 -0
- package/src/files/index.ts +4 -0
- package/src/files/serve.ts +115 -0
- package/src/files/service.ts +209 -0
- package/src/files/spool.ts +58 -0
- package/src/identity/better-auth.ts +1103 -0
- package/src/identity/demo-pass-plugin.ts +121 -0
- package/src/identity/demo-pass-rules.ts +229 -0
- package/src/identity/demo-pass.ts +390 -0
- package/src/identity/hub-grant.ts +704 -0
- package/src/identity/hub-jwt.ts +78 -0
- package/src/identity/hub-live-gate.ts +167 -0
- package/src/identity/hub-logout.ts +174 -0
- package/src/identity/hub-projection.ts +251 -0
- package/src/identity/hub-reconcile.ts +572 -0
- package/src/identity/hub-sso.ts +696 -0
- package/src/identity/hub-user-client.ts +554 -0
- package/src/identity/index.ts +56 -0
- package/src/identity/oauth-jwt.ts +128 -0
- package/src/identity/resolve-membership.ts +104 -0
- package/src/identity/signing-key.ts +164 -0
- package/src/index.ts +65 -0
- package/src/invitations/index.ts +1 -0
- package/src/invitations/service.ts +171 -0
- package/src/jobs/index.ts +25 -0
- package/src/jobs/pgboss.ts +594 -0
- package/src/jobs/timers.ts +287 -0
- package/src/logger.ts +67 -0
- package/src/mcp/errors.ts +111 -0
- package/src/mcp/http.ts +129 -0
- package/src/mcp/index.ts +28 -0
- package/src/mcp/inputs.ts +33 -0
- package/src/mcp/projects.ts +480 -0
- package/src/mcp/server.ts +174 -0
- package/src/mcp/teams.ts +97 -0
- package/src/mcp/tool-kit.ts +148 -0
- package/src/members/removal.ts +216 -0
- package/src/middleware/auth-body.ts +58 -0
- package/src/middleware/auth-context.ts +384 -0
- package/src/middleware/body-cap.ts +33 -0
- package/src/middleware/body-refusal.ts +23 -0
- package/src/middleware/cross-site.ts +122 -0
- package/src/middleware/hub-managed.ts +61 -0
- package/src/middleware/idempotency.ts +264 -0
- package/src/middleware/index.ts +33 -0
- package/src/middleware/json-depth.ts +74 -0
- package/src/middleware/no-store.ts +30 -0
- package/src/middleware/oauth-public.ts +54 -0
- package/src/middleware/rate-limit.ts +432 -0
- package/src/middleware/request-id.ts +54 -0
- package/src/middleware/scopes.ts +150 -0
- package/src/middleware/security-headers.ts +129 -0
- package/src/observability/index.ts +2 -0
- package/src/observability/metrics.ts +102 -0
- package/src/observability/otel.ts +79 -0
- package/src/pagination.ts +118 -0
- package/src/platform/edition.ts +140 -0
- package/src/platform/entitlements.ts +47 -0
- package/src/platform/events.ts +71 -0
- package/src/platform/index.ts +7 -0
- package/src/platform/local-identity.ts +90 -0
- package/src/platform/registry.ts +28 -0
- package/src/platform/usage.ts +13 -0
- package/src/platform/workspaces.ts +41 -0
- package/src/projects/access.ts +160 -0
- package/src/projects/index.ts +7 -0
- package/src/route-label.ts +29 -0
- package/src/routes/health.ts +79 -0
- package/src/routes/index.ts +2 -0
- package/src/routes/wellknown.ts +67 -0
- package/src/secret.ts +115 -0
- package/src/setup-transport.ts +46 -0
- package/src/state.ts +23 -0
- package/src/storage/driver.ts +48 -0
- package/src/storage/factory.ts +34 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/local.ts +93 -0
- package/src/storage/s3.ts +109 -0
- package/src/testing/fake-hub.ts +1316 -0
- package/src/testing/helpers.ts +254 -0
- package/src/testing/index.ts +11 -0
- package/src/testing/setup-origins.ts +58 -0
- package/src/testing/sso-helpers.ts +178 -0
- package/src/tool-definition.ts +463 -0
- package/src/util/index.ts +17 -0
- package/test/fixtures/onboarding-backfill.sql +13 -0
- package/test/fixtures/operator-user-id-backfill.sql +18 -0
- package/test/host/dr-lib.sh +253 -0
- package/test/host/drizzle/0000_chassis_baseline.sql +438 -0
- package/test/host/drizzle/0001_last_owner_guard.sql +42 -0
- package/test/host/drizzle/meta/0000_snapshot.json +3566 -0
- package/test/host/drizzle/meta/_journal.json +20 -0
- package/test/host/drizzle.config.ts +17 -0
- package/test/host/identity.ts +41 -0
- package/test/host/minimal-host.ts +35 -0
- package/test/host/minimal-tool.ts +123 -0
- package/test/integration/account-oracle-uniformity.test.ts +284 -0
- package/test/integration/account-recovery.test.ts +236 -0
- package/test/integration/account.test.ts +120 -0
- package/test/integration/api-quota.test.ts +208 -0
- package/test/integration/apikey-expiry.test.ts +178 -0
- package/test/integration/apikey-pepper-rotation.test.ts +190 -0
- package/test/integration/boot-posture.test.ts +169 -0
- package/test/integration/break-glass.test.ts +484 -0
- package/test/integration/cloud-lifecycle.test.ts +233 -0
- package/test/integration/credential-boundaries.test.ts +195 -0
- package/test/integration/default-workspace.test.ts +414 -0
- package/test/integration/demo-pass-rules.test.ts +332 -0
- package/test/integration/demo-pass.test.ts +1222 -0
- package/test/integration/dr-verify-real-dump.test.ts +154 -0
- package/test/integration/edition.test.ts +868 -0
- package/test/integration/email-change.test.ts +394 -0
- package/test/integration/empty-tool.test.ts +267 -0
- package/test/integration/entitlements-null-hook.test.ts +187 -0
- package/test/integration/entitlements.test.ts +693 -0
- package/test/integration/export-entries.test.ts +321 -0
- package/test/integration/files.test.ts +320 -0
- package/test/integration/first-boot-claim.test.ts +131 -0
- package/test/integration/gdpr.test.ts +532 -0
- package/test/integration/helpers.ts +29 -0
- package/test/integration/hub-grant.test.ts +505 -0
- package/test/integration/hub-live-gate.test.ts +612 -0
- package/test/integration/hub-managed-membership.test.ts +559 -0
- package/test/integration/hub-org-selector.test.ts +281 -0
- package/test/integration/hub-reconcile.test.ts +646 -0
- package/test/integration/hub-sso.test.ts +704 -0
- package/test/integration/hub-teams.test.ts +771 -0
- package/test/integration/idempotency.test.ts +261 -0
- package/test/integration/invite-verification.test.ts +210 -0
- package/test/integration/last-owner-race.test.ts +263 -0
- package/test/integration/m9-fixes.test.ts +290 -0
- package/test/integration/mcp-projects.test.ts +463 -0
- package/test/integration/member-removal-race.test.ts +466 -0
- package/test/integration/member-removal.test.ts +794 -0
- package/test/integration/oauth-mcp.test.ts +477 -0
- package/test/integration/oauth-workspace.test.ts +371 -0
- package/test/integration/observability.test.ts +183 -0
- package/test/integration/onboarding.test.ts +268 -0
- package/test/integration/orphan-purge.test.ts +451 -0
- package/test/integration/pagination.test.ts +344 -0
- package/test/integration/platform-core.test.ts +305 -0
- package/test/integration/platform.test.ts +246 -0
- package/test/integration/pool-survives-restart.test.ts +80 -0
- package/test/integration/projects-cloud.test.ts +193 -0
- package/test/integration/projects.test.ts +1310 -0
- package/test/integration/security-fixes.test.ts +190 -0
- package/test/integration/setup-transport.test.ts +118 -0
- package/test/integration/signin-wall.test.ts +103 -0
- package/test/integration/signing-key-rotation.test.ts +349 -0
- package/test/integration/sso-helpers.ts +15 -0
- package/test/integration/sso-logout.test.ts +283 -0
- package/test/integration/teams-cloud.test.ts +194 -0
- package/test/integration/teams.test.ts +551 -0
- package/test/integration/timers.test.ts +377 -0
- package/test/integration/two-factor.test.ts +301 -0
- package/test/integration/usage-window.test.ts +161 -0
- package/test/integration/workspace-create-cloud.test.ts +523 -0
- package/test/integration/workspace-create.test.ts +510 -0
- package/test/integration/workspace-scoping.test.ts +530 -0
- package/test/integration/zz-adv-replay-owner.test.ts +205 -0
- package/test/unit/apikey-peppers.test.ts +110 -0
- package/test/unit/apikey-prefix.test.ts +34 -0
- package/test/unit/audit-filters.test.ts +135 -0
- package/test/unit/auth-origins.test.ts +92 -0
- package/test/unit/body-cap.test.ts +84 -0
- package/test/unit/clock-skew.test.ts +68 -0
- package/test/unit/cross-site.test.ts +166 -0
- package/test/unit/declared-credits.test.ts +13 -0
- package/test/unit/demo-pass-rules.test.ts +192 -0
- package/test/unit/email-driver.test.ts +127 -0
- package/test/unit/end-pool.test.ts +181 -0
- package/test/unit/entitlement-check.test.ts +492 -0
- package/test/unit/entitlement-gate.test.ts +1333 -0
- package/test/unit/entitlement-profiles.test.ts +289 -0
- package/test/unit/erasure-log.test.ts +72 -0
- package/test/unit/error-mapping.test.ts +89 -0
- package/test/unit/export-entries.test.ts +85 -0
- package/test/unit/export-sanitize.test.ts +27 -0
- package/test/unit/files-head-route.test.ts +155 -0
- package/test/unit/files-http.test.ts +97 -0
- package/test/unit/head-rewrite.test.ts +72 -0
- package/test/unit/hub-create-org.test.ts +181 -0
- package/test/unit/hub-jwt.test.ts +93 -0
- package/test/unit/hub-sso-params.test.ts +39 -0
- package/test/unit/hub-subject.test.ts +83 -0
- package/test/unit/idempotency-crypto.test.ts +45 -0
- package/test/unit/json-depth.test.ts +88 -0
- package/test/unit/logger-redaction.test.ts +66 -0
- package/test/unit/mcp-errors.test.ts +110 -0
- package/test/unit/mcp-inputs.test.ts +106 -0
- package/test/unit/mcp-project-hints.test.ts +26 -0
- package/test/unit/no-store.test.ts +49 -0
- package/test/unit/no-tool-migration-filename.test.ts +70 -0
- package/test/unit/observability.test.ts +64 -0
- package/test/unit/openapi-doc.test.ts +93 -0
- package/test/unit/rate-limit-consume-on.test.ts +76 -0
- package/test/unit/rate-limit.test.ts +39 -0
- package/test/unit/readyz-storage.test.ts +157 -0
- package/test/unit/request-quota.test.ts +146 -0
- package/test/unit/scopes.test.ts +66 -0
- package/test/unit/security-headers.test.ts +131 -0
- package/test/unit/serve-content-type.test.ts +74 -0
- package/test/unit/setup-transport.test.ts +29 -0
- package/test/unit/storage-local.test.ts +53 -0
- package/test/unit/superadmin.test.ts +32 -0
- package/test/unit/timers.test.ts +287 -0
- package/test/unit/tool-definition-guards.test.ts +166 -0
- package/test/unit/tool-entitlements-guard.test.ts +168 -0
- package/test/unit/usage-poster.test.ts +542 -0
- package/test/unit/usage-window.test.ts +48 -0
- package/test/unit/workspace-create-audit.test.ts +48 -0
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth';
|
|
2
|
+
import { APIError } from 'better-auth/api';
|
|
3
|
+
import { createAuthMiddleware } from 'better-auth/api';
|
|
4
|
+
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
5
|
+
import { emailOTP, genericOAuth, jwt, twoFactor } from 'better-auth/plugins';
|
|
6
|
+
import { deleteSessionCookie } from 'better-auth/cookies';
|
|
7
|
+
import { generateRandomString } from 'better-auth/crypto';
|
|
8
|
+
import { oauthProvider } from '@better-auth/oauth-provider';
|
|
9
|
+
import { and, eq } from 'drizzle-orm';
|
|
10
|
+
import {
|
|
11
|
+
account,
|
|
12
|
+
instanceSettings,
|
|
13
|
+
jwks,
|
|
14
|
+
oauthAccessToken,
|
|
15
|
+
oauthClient,
|
|
16
|
+
oauthConsent,
|
|
17
|
+
oauthRefreshToken,
|
|
18
|
+
session,
|
|
19
|
+
twoFactor as twoFactorTable,
|
|
20
|
+
user,
|
|
21
|
+
verification,
|
|
22
|
+
workspaceMembers,
|
|
23
|
+
type Db
|
|
24
|
+
} from '@antasphere/chassis-db';
|
|
25
|
+
import type { Env } from '../env.js';
|
|
26
|
+
import { HUB_SSO_PROVIDER_ID, HubSsoLoginError, type HubSsoService } from './hub-sso.js';
|
|
27
|
+
import { parseSuperadminEmails } from '../accounts/superadmin.js';
|
|
28
|
+
import { demoPassPlugin, type DemoPassPluginDeps } from './demo-pass-plugin.js';
|
|
29
|
+
// Types only, erased at emit: this package EMITS DECLARATIONS (the app bundled
|
|
30
|
+
// the file and never did), and `Auth = ReturnType<typeof createAuth>` is an
|
|
31
|
+
// inferred type that names these two modules. TypeScript can only spell a
|
|
32
|
+
// module it has resolved in this program (TS2742 otherwise), so they are
|
|
33
|
+
// resolved here. Nothing is imported at run time.
|
|
34
|
+
import type {} from '@better-auth/core/db/adapter';
|
|
35
|
+
import type {} from 'better-call';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The only file that touches better-auth's constructor. Everything else goes
|
|
39
|
+
* through the identity module's exports or the IdentityProvider seam, so a
|
|
40
|
+
* Better Auth upgrade (or replacement) stays contained.
|
|
41
|
+
*
|
|
42
|
+
* Password login is the self-host default (no SMTP dependency). Email OTP
|
|
43
|
+
* auto-enables when an email driver is configured; Google when client
|
|
44
|
+
* credentials are set. The twoFactor plugin offers OPT-IN per-user TOTP +
|
|
45
|
+
* backup codes (ADR 009) — password and email-OTP sign-ins by an enrolled
|
|
46
|
+
* user require the second factor; machine credentials never see one. The
|
|
47
|
+
* jwt + oauthProvider plugins make every instance an OAuth 2.1 authorization
|
|
48
|
+
* server for its own bundled /mcp endpoint (M6): RS256 access JWTs (15 min),
|
|
49
|
+
* rotating refresh tokens, PKCE + dynamic client registration, issuance
|
|
50
|
+
* gated on a LIVE active workspace membership.
|
|
51
|
+
*/
|
|
52
|
+
/** Credential events audited on Better-Auth-native routes. */
|
|
53
|
+
export type AccountEvent =
|
|
54
|
+
| 'password_reset'
|
|
55
|
+
| 'password_change'
|
|
56
|
+
| 'email_change_request'
|
|
57
|
+
| 'email_change'
|
|
58
|
+
| 'two_factor_enroll'
|
|
59
|
+
| 'two_factor_disable';
|
|
60
|
+
|
|
61
|
+
export interface CreateAuthOptions {
|
|
62
|
+
db: Db;
|
|
63
|
+
env: Pick<
|
|
64
|
+
Env,
|
|
65
|
+
| 'PUBLIC_BASE_URL'
|
|
66
|
+
| 'GOOGLE_CLIENT_ID'
|
|
67
|
+
| 'GOOGLE_CLIENT_SECRET'
|
|
68
|
+
| 'OAUTH_DYNAMIC_CLIENT_REGISTRATION'
|
|
69
|
+
| 'SUPERADMIN_EMAILS'
|
|
70
|
+
>;
|
|
71
|
+
authSecret: string;
|
|
72
|
+
/**
|
|
73
|
+
* The tool's OAuth scope list, in the ORDER it is published (discovery
|
|
74
|
+
* `scopes_supported`, the consent screen, client registration defaults).
|
|
75
|
+
* The chassis spells no scope name: the tool defines the list and hands it
|
|
76
|
+
* in; the well-known documents read the same array from the tool.
|
|
77
|
+
*/
|
|
78
|
+
oauthScopes: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* Origins that are NEVER trusted, serving origin or not (PRDCT-1352): each
|
|
81
|
+
* is filtered out of `trustedOrigins` and refused outright by the sign-in
|
|
82
|
+
* Origin lock. Empty = nothing installed. A tool with a viewer passes its
|
|
83
|
+
* viewer origin (author-controlled script runs there).
|
|
84
|
+
*/
|
|
85
|
+
untrustedOrigins: readonly string[];
|
|
86
|
+
/** When provided (an email driver delivers), the email-OTP login auto-enables. */
|
|
87
|
+
sendOtp?: (params: { email: string; otp: string; type: string }) => Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* When provided (an email driver delivers), self-serve password reset
|
|
90
|
+
* auto-enables — POST /request-password-reset mails a link. Without it that
|
|
91
|
+
* endpoint stays closed (400) and only the admin-generated reset link works.
|
|
92
|
+
* On EDITION=cloud it is IGNORED: the whole reset surface refuses there
|
|
93
|
+
* (D1 hub-only posture — see isPasswordResetPath and the before-hook).
|
|
94
|
+
*/
|
|
95
|
+
sendResetPassword?: (params: { email: string; url: string; token: string }) => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Confirmation mail to the OLD address when a VERIFIED user requests an
|
|
98
|
+
* email change (the two-leg flow). Unverified users — the template's common
|
|
99
|
+
* case — skip straight to the verification mail below.
|
|
100
|
+
*/
|
|
101
|
+
sendChangeEmailConfirmation?: (params: {
|
|
102
|
+
email: string;
|
|
103
|
+
newEmail: string;
|
|
104
|
+
url: string;
|
|
105
|
+
token: string;
|
|
106
|
+
}) => Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Verification mail to the NEW address — the final leg of every self-serve
|
|
109
|
+
* email change. When provided (an email driver delivers), POST /change-email
|
|
110
|
+
* opens; without it the endpoint answers 400 BEFORE any email lookup (no
|
|
111
|
+
* enumeration oracle) and only the admin-generated change link works.
|
|
112
|
+
*/
|
|
113
|
+
sendVerificationEmail?: (params: { email: string; url: string; token: string }) => Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Audit hook for credential events on Better-Auth-native routes (which
|
|
116
|
+
* bypass the /api/v1 audit middleware). Fired best-effort by the caller.
|
|
117
|
+
*/
|
|
118
|
+
onAccountEvent?: (event: AccountEvent, userId: string) => Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Fired from Better Auth's databaseHooks.user.create.after — the ONE seam
|
|
121
|
+
* every account entrance passes through (setup, invitation accept, the
|
|
122
|
+
* collaborator claim endpoint, any future SSO JIT), so a `user.created`
|
|
123
|
+
* event is trustworthy by construction instead of depending on each call
|
|
124
|
+
* site remembering to emit. Boot wires this to the platform event bus.
|
|
125
|
+
*/
|
|
126
|
+
onUserCreated?: (user: { id: string; email: string }) => void;
|
|
127
|
+
/**
|
|
128
|
+
* Self-service account deletion guard, run by POST /delete-user BEFORE the
|
|
129
|
+
* row cascade. Boot wires the AccountDeletionService here and translates
|
|
130
|
+
* LastOwnerError into an APIError('BAD_REQUEST') so the route answers 400.
|
|
131
|
+
*/
|
|
132
|
+
beforeUserDelete?: (userId: string) => Promise<void>;
|
|
133
|
+
/** Completion hook, run AFTER the cascade (the audit row's system actor). */
|
|
134
|
+
afterUserDelete?: (user: { id: string; email: string }) => Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* The cloud edition's hub SSO binding (internal/federation.md, ADR 015).
|
|
137
|
+
* Present ONLY when the instance boots EDITION=cloud: registers the
|
|
138
|
+
* `antasphere` genericOAuth relying party, the D9 trusted-link config,
|
|
139
|
+
* and the per-login callback after-hook (JIT projection + re-sync). An
|
|
140
|
+
* oss boot passes nothing and carries zero SSO surface at runtime.
|
|
141
|
+
*/
|
|
142
|
+
hubSso?: HubSsoService | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Demo sign-in (identity/demo-pass-plugin.ts): handed in by boot only while
|
|
145
|
+
* DEMO_SIGN_IN is on. The redeem endpoint is registered only then, and
|
|
146
|
+
* only on the self-hosted edition.
|
|
147
|
+
*/
|
|
148
|
+
demoPass?: DemoPassPluginDeps | undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const AUTH_BASE_PATH = '/api/v1/auth';
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The canonical OAuth resource identifier (JWT `aud`, RFC 8707) — the bundled
|
|
155
|
+
* MCP endpoint. Derived from PUBLIC_BASE_URL at boot, never from its own env
|
|
156
|
+
* var: AS and resource live on one origin, so the byte-identical-URL footgun
|
|
157
|
+
* of the split dashboard+MCP deployment cannot exist here.
|
|
158
|
+
*/
|
|
159
|
+
export function mcpResourceUrl(publicBaseUrl: string): string {
|
|
160
|
+
return publicBaseUrl.replace(/\/+$/, '') + '/mcp';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type Auth = ReturnType<typeof createAuth>;
|
|
164
|
+
|
|
165
|
+
/** Metadata URI fields a registering client may present to users (RFC 7591). */
|
|
166
|
+
const CLIENT_METADATA_URI_FIELDS = ['client_uri', 'logo_uri', 'tos_uri', 'policy_uri'] as const;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Every route that can CHANGE the account's email address from inside the
|
|
170
|
+
* tool, enumerated against the pinned 1.6.22 surface (re-verify on ANY Better
|
|
171
|
+
* Auth bump):
|
|
172
|
+
*
|
|
173
|
+
* - POST /change-email: the core self-serve change (`user.changeEmail`);
|
|
174
|
+
* - POST /email-otp/request-email-change + POST /email-otp/change-email: the
|
|
175
|
+
* emailOTP plugin's pair. Both answer 400 today (the plugin's own
|
|
176
|
+
* `changeEmail.enabled` is never set); closed as config insurance, the
|
|
177
|
+
* stance /set-password has in isPasswordResetPath.
|
|
178
|
+
*
|
|
179
|
+
* On EDITION=cloud the address is the hub's: the sign-in re-syncs it at every
|
|
180
|
+
* login (identity/hub-sso.ts), so a change made here is overwritten at the
|
|
181
|
+
* next one, and until then the person's address here differs from the one
|
|
182
|
+
* their hub identity carries. The dashboard hides the form
|
|
183
|
+
* (`emailChange: false`); the route itself refuses too (PRDCT-2818), like the
|
|
184
|
+
* owner's change-email link (api/members.ts, `email_change_disabled`).
|
|
185
|
+
* Ruled OUT on purpose: the tokened GET /verify-email stays open, since it
|
|
186
|
+
* also lands the address verification the instance sends, and with both
|
|
187
|
+
* mints closed no change token can be issued on cloud.
|
|
188
|
+
*/
|
|
189
|
+
function isEmailChangePath(path: string): boolean {
|
|
190
|
+
return (
|
|
191
|
+
path.startsWith('/change-email') ||
|
|
192
|
+
path.startsWith('/email-otp/request-email-change') ||
|
|
193
|
+
path.startsWith('/email-otp/change-email')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Every Better Auth route that can SET or RESET a local password with no
|
|
199
|
+
* current-password proof, enumerated against the pinned 1.6.22 surface
|
|
200
|
+
* (re-enumerated on the 1.6.15 → 1.6.22 bump, 2026-08-29, ADR 001; the
|
|
201
|
+
* dist-wide createAuthEndpoint path+method diff was one added route in the
|
|
202
|
+
* unregistered oauth-popup plugin, nothing removed)
|
|
203
|
+
* (re-verify on ANY Better Auth bump):
|
|
204
|
+
*
|
|
205
|
+
* - core emailAndPassword: POST /request-password-reset,
|
|
206
|
+
* POST /reset-password, GET /reset-password/:token (the mailed callback);
|
|
207
|
+
* - emailOTP plugin (active whenever a mailer delivers):
|
|
208
|
+
* POST /email-otp/request-password-reset, POST /email-otp/reset-password,
|
|
209
|
+
* and the deprecated POST /forget-password/email-otp alias;
|
|
210
|
+
* - core /set-password (the passwordless-ADD route): included as bump
|
|
211
|
+
* insurance. In 1.6.22 its endpoint is `createAuthEndpoint.serverOnly`;
|
|
212
|
+
* in 1.6.15 it was registered PATHLESS
|
|
213
|
+
* (update-user.mjs — `createAuthEndpoint({ method, body, use })` with no
|
|
214
|
+
* path arg), so better-call's router SKIPS it (`!endpoint.path` →
|
|
215
|
+
* `continue`, router.mjs) and it is unreachable over HTTP today — the one
|
|
216
|
+
* route that could otherwise let a passwordless hub-JIT user ADD a
|
|
217
|
+
* credential and then /sign-in/email. A future bump could give it a path;
|
|
218
|
+
* a security allowlist must not depend on that accident, so guard it now
|
|
219
|
+
* (denying an unreachable path is a no-op until it isn't).
|
|
220
|
+
*
|
|
221
|
+
* Ruled OUT of this predicate on purpose, against the same 1.6.22 surface:
|
|
222
|
+
* - /change-password proves the CURRENT password AND requires an existing
|
|
223
|
+
* credential account (CREDENTIAL_ACCOUNT_NOT_FOUND otherwise), so a
|
|
224
|
+
* hub-JIT user — who has neither — cannot set a password through it;
|
|
225
|
+
* - admin /admin/set-user-password and the phoneNumber reset routes live in
|
|
226
|
+
* the admin / phoneNumber plugins, neither of which this instance
|
|
227
|
+
* registers (only genericOAuth, emailOTP, twoFactor, jwt, oauthProvider),
|
|
228
|
+
* so they are never mounted;
|
|
229
|
+
* - /sign-up/email is closed by the sign-up switches + the before-hook.
|
|
230
|
+
*
|
|
231
|
+
* On EDITION=cloud these are the SSO-bypass entrance the D1 hub-only posture
|
|
232
|
+
* closes (internal/federation.md, ADR 017): a hub-JIT user (no credential
|
|
233
|
+
* account) could otherwise mail themselves a reset, SET a local password,
|
|
234
|
+
* and mint sessions via /sign-in/email that skip the per-login hub re-sync.
|
|
235
|
+
* P4's re-assertion still gates every such session, so this is posture, not
|
|
236
|
+
* an attacker hole — but the entrance must not exist. /sign-in/email itself
|
|
237
|
+
* stays wired (the break-glass operator door).
|
|
238
|
+
* `ctx.path` here is the ROUTE PATTERN (e.g. '/reset-password/:token'), so
|
|
239
|
+
* prefix matching covers the tokened callback.
|
|
240
|
+
*/
|
|
241
|
+
function isPasswordResetPath(path: string): boolean {
|
|
242
|
+
return (
|
|
243
|
+
path.startsWith('/request-password-reset') ||
|
|
244
|
+
path.startsWith('/reset-password') ||
|
|
245
|
+
path.startsWith('/forget-password') ||
|
|
246
|
+
path.startsWith('/email-otp/request-password-reset') ||
|
|
247
|
+
path.startsWith('/email-otp/reset-password') ||
|
|
248
|
+
path.startsWith('/set-password')
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Every emailOTP route that can MINT A SESSION from an emailed code — or
|
|
254
|
+
* mail such a code — enumerated against the pinned 1.6.22 surface
|
|
255
|
+
* (re-enumerated on the 1.6.15 → 1.6.22 bump, 2026-08-29: the plugin's nine
|
|
256
|
+
* routes are byte-identical in path and method)
|
|
257
|
+
* (plugins/email-otp/routes.mjs; re-verify on ANY Better Auth bump):
|
|
258
|
+
*
|
|
259
|
+
* - POST /sign-in/email-otp: the sign-in mint — verifies the code, then
|
|
260
|
+
* UNCONDITIONALLY createSession + setSessionCookie;
|
|
261
|
+
* - POST /email-otp/verify-email: flips emailVerified, and mints a session
|
|
262
|
+
* ONLY under emailVerification.autoSignInAfterVerification — UNSET on
|
|
263
|
+
* this instance, so it mints nothing today. Guarded as config insurance
|
|
264
|
+
* (the same stance as /set-password in isPasswordResetPath): a future
|
|
265
|
+
* config change must not silently open an SSO-bypassing session mint,
|
|
266
|
+
* and no client of this instance calls the route (the email-change
|
|
267
|
+
* landing is the core tokened GET /verify-email, untouched);
|
|
268
|
+
* - POST /email-otp/send-verification-otp: the mail leg. With this close,
|
|
269
|
+
* EVERY redemption route for every OTP type refuses on cloud (sign-in +
|
|
270
|
+
* verify-email here, the reset trio in isPasswordResetPath), so a sent
|
|
271
|
+
* code could only ever be a dead letter — refuse the send rather than
|
|
272
|
+
* mail codes that cannot work.
|
|
273
|
+
*
|
|
274
|
+
* Ruled OUT on purpose, against the same 1.6.22 surface:
|
|
275
|
+
* - POST /email-otp/check-verification-otp verifies WITHOUT consuming and
|
|
276
|
+
* mints nothing (no createSession in its handler); with the send leg
|
|
277
|
+
* closed there is nothing to check anyway;
|
|
278
|
+
* - POST /email-otp/request-email-change + POST /email-otp/change-email
|
|
279
|
+
* (the plugin's two remaining routes, unchanged at 1.6.22): both run behind
|
|
280
|
+
* sensitiveSessionMiddleware — an EXISTING fresh session is their entry
|
|
281
|
+
* condition, so neither is a session ENTRANCE (change-email's
|
|
282
|
+
* setSessionCookie only re-sets the cookie of the session it already
|
|
283
|
+
* required) — and both answer 400 unless the plugin option
|
|
284
|
+
* `changeEmail.enabled` is set, which this instance never sets (the
|
|
285
|
+
* core tokened /change-email flow is the email-change surface; on cloud
|
|
286
|
+
* it is hidden and D10 re-syncs email from the hub anyway). If
|
|
287
|
+
* changeEmail is ever enabled, re-rule them here;
|
|
288
|
+
* - createVerificationOTP / getVerificationOTP are registered PATHLESS in
|
|
289
|
+
* 1.6.15 and `createAuthEndpoint.serverOnly` in 1.6.22 (routes.mjs), so
|
|
290
|
+
* better-call's router skips them: server-side only, unreachable over
|
|
291
|
+
* HTTP (same accident /set-password documents — do not rely on it);
|
|
292
|
+
* - /sign-in/email-otp is the plugin's ONLY /sign-in/* route; the twoFactor
|
|
293
|
+
* plugin mints only from a pending first factor, magic-link/phone are not
|
|
294
|
+
* registered here.
|
|
295
|
+
*
|
|
296
|
+
* On EDITION=cloud this pair is the last non-SSO HUMAN session entrance
|
|
297
|
+
* under the D1 hub-only posture (the reset surface closed at P8; ADR 017 §7
|
|
298
|
+
* recorded the OTP sign-in KNOWN-OPEN pending a charter call — now taken):
|
|
299
|
+
* every cloud credential must trace through "Sign in with Antasphere" so
|
|
300
|
+
* the hub's audit log is the complete access record. P4's re-assertion
|
|
301
|
+
* already gated every OTP session, so this is posture, not an attacker
|
|
302
|
+
* hole. The CLI counterpart (/cli/auth/*) closes in api/cli-auth.ts;
|
|
303
|
+
* /sign-in/email stays wired (the break-glass operator door, which the
|
|
304
|
+
* string '/sign-in/email-otp' does not prefix-match); oss keeps the full
|
|
305
|
+
* OTP login unchanged.
|
|
306
|
+
*/
|
|
307
|
+
function isOtpSignInPath(path: string): boolean {
|
|
308
|
+
return (
|
|
309
|
+
path.startsWith('/sign-in/email-otp') ||
|
|
310
|
+
path.startsWith('/email-otp/verify-email') ||
|
|
311
|
+
path.startsWith('/email-otp/send-verification-otp')
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The two Better Auth core routes that hand a caller the PROVIDER GRANT
|
|
317
|
+
* stored on their own `account` row, in PLAINTEXT (PRDCT-1354, findings
|
|
318
|
+
* AUTH-3 + AUTH-7; better-auth 1.6.22 `api/routes/account.mjs` (both routes
|
|
319
|
+
* present unchanged at the 1.6.15 → 1.6.22 bump, 2026-08-29) — re-verify
|
|
320
|
+
* the enumeration on ANY bump):
|
|
321
|
+
*
|
|
322
|
+
* - POST /get-access-token → `{ accessToken, idToken, accessTokenExpiresAt,
|
|
323
|
+
* scopes }`, refreshing first when the stored token is within 5 s of
|
|
324
|
+
* expiry;
|
|
325
|
+
* - POST /refresh-token → the same PLUS `refreshToken`, and it ALWAYS
|
|
326
|
+
* refreshes.
|
|
327
|
+
*
|
|
328
|
+
* Both are closed here, on BOTH editions, because both are pure subtraction:
|
|
329
|
+
* nothing in this repo, the SDK, the CLI, the dashboard, or the MCP server
|
|
330
|
+
* calls either one (grep `get-access-token` / `refresh-token`), and no
|
|
331
|
+
* server-side caller exists either — the hub grant's own refresh is
|
|
332
|
+
* `HubGrantService.postRefresh`, which posts to the hub's token endpoint
|
|
333
|
+
* directly and never goes through this route. The plugin-owned OAuth
|
|
334
|
+
* endpoints this instance DOES serve (`/oauth2/token`, `/oauth2/authorize`,
|
|
335
|
+
* …) are separate paths and untouched.
|
|
336
|
+
*
|
|
337
|
+
* Why they must not exist, even though `resolveUserId` requires a session
|
|
338
|
+
* for any HTTP caller (so they are not a cross-USER surface):
|
|
339
|
+
*
|
|
340
|
+
* 1. The `/auth/*` mount is registered BEFORE `authContext`
|
|
341
|
+
* (`api/index.ts`), so neither route sees the fail-closed scope
|
|
342
|
+
* allowlist, the per-principal quota, the idempotency claim, or the
|
|
343
|
+
* audit middleware. Any session-bearing caller — including a browser
|
|
344
|
+
* tricked into a same-site POST, or anything that reaches the cookie —
|
|
345
|
+
* reads the grant with no record left behind, which defeats the whole
|
|
346
|
+
* point of `encryptOAuthTokens: true` (encryption at rest is worthless
|
|
347
|
+
* next to an unlogged plaintext read endpoint).
|
|
348
|
+
* 2. On cloud the grant in question is the HUB grant, i.e. the credential
|
|
349
|
+
* the user-scoped federation is built on (ADR 019). Handing it out in
|
|
350
|
+
* plaintext hands out the ability to read that user's hub orgs directly.
|
|
351
|
+
* 3. `/refresh-token` additionally ROTATES that grant OUTSIDE the
|
|
352
|
+
* `pg_advisory_lock(7432004, hashtext(userId))` single-flight ADR 019
|
|
353
|
+
* requires. The hub's RFC 9700 reuse detection makes an unserialized
|
|
354
|
+
* double-refresh a grant-family-KILLING event, so this route is a
|
|
355
|
+
* denial-of-service any logged-in user can trigger from a browser tab.
|
|
356
|
+
* (The hub-side half of that hardening is PRDCT-1370.)
|
|
357
|
+
*
|
|
358
|
+
* `ctx.path` is the route pattern, and neither route has path params, so an
|
|
359
|
+
* exact match is enough; startsWith would also swallow future siblings, but
|
|
360
|
+
* an allowlist-shaped closure should deny exactly what it has verified.
|
|
361
|
+
*/
|
|
362
|
+
function isProviderGrantPath(path: string): boolean {
|
|
363
|
+
return path === '/get-access-token' || path === '/refresh-token';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isHttpUrl(value: unknown): boolean {
|
|
367
|
+
if (typeof value !== 'string') return false;
|
|
368
|
+
try {
|
|
369
|
+
const url = new URL(value);
|
|
370
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Better Auth `trustedOrigins` (PRDCT-1352): the configured public origin plus
|
|
378
|
+
* the origin the request was served on (localhost, previews, any domain behind
|
|
379
|
+
* a TLS-terminating proxy) — MINUS the viewer origin, which is author-
|
|
380
|
+
* controlled script's origin and is never trusted, serving origin or
|
|
381
|
+
* not. The host gate keeps the auth surface off that hostname; this is the
|
|
382
|
+
* second lock behind it. Exported pure so a unit test can pin the exclusion.
|
|
383
|
+
*/
|
|
384
|
+
export function trustedOriginsFor(
|
|
385
|
+
publicBaseUrl: string,
|
|
386
|
+
untrustedOrigins: readonly string[]
|
|
387
|
+
): (request?: Request) => string[] {
|
|
388
|
+
return (request) => {
|
|
389
|
+
const origins = [publicBaseUrl];
|
|
390
|
+
try {
|
|
391
|
+
if (request) origins.push(new URL(request.url).origin);
|
|
392
|
+
} catch {
|
|
393
|
+
// unparseable request URL — explicit origin only
|
|
394
|
+
}
|
|
395
|
+
return untrustedOrigins.length > 0
|
|
396
|
+
? origins.filter((origin) => !untrustedOrigins.includes(origin))
|
|
397
|
+
: origins;
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The sign-in Origin lock (M9 login-CSRF hardening + PRDCT-1352): a sign-in
|
|
403
|
+
* that presents an Origin must present a trusted one — the serving origin or
|
|
404
|
+
* Better Auth's own trusted list — and the viewer origin is refused OUTRIGHT,
|
|
405
|
+
* even as the serving origin: author script must never mint a session on the
|
|
406
|
+
* hostname it runs on. Exported pure so a unit test can pin both arms.
|
|
407
|
+
*/
|
|
408
|
+
export function signInOriginRefused(input: {
|
|
409
|
+
origin: string;
|
|
410
|
+
servingOrigin: string | null;
|
|
411
|
+
untrustedOrigins: readonly string[];
|
|
412
|
+
isTrustedOrigin: (origin: string) => boolean;
|
|
413
|
+
}): boolean {
|
|
414
|
+
const { origin, servingOrigin, untrustedOrigins, isTrustedOrigin } = input;
|
|
415
|
+
if (untrustedOrigins.includes(origin)) return true;
|
|
416
|
+
return origin !== servingOrigin && !isTrustedOrigin(origin);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function createAuth({
|
|
420
|
+
db,
|
|
421
|
+
env,
|
|
422
|
+
authSecret,
|
|
423
|
+
oauthScopes,
|
|
424
|
+
untrustedOrigins,
|
|
425
|
+
sendOtp,
|
|
426
|
+
sendResetPassword,
|
|
427
|
+
sendChangeEmailConfirmation,
|
|
428
|
+
sendVerificationEmail,
|
|
429
|
+
onAccountEvent,
|
|
430
|
+
onUserCreated,
|
|
431
|
+
beforeUserDelete,
|
|
432
|
+
afterUserDelete,
|
|
433
|
+
hubSso,
|
|
434
|
+
demoPass
|
|
435
|
+
}: CreateAuthOptions) {
|
|
436
|
+
const isHttps = env.PUBLIC_BASE_URL.startsWith('https://');
|
|
437
|
+
const resource = mcpResourceUrl(env.PUBLIC_BASE_URL);
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Cloud only: may this email take the local password door? The setup
|
|
441
|
+
* operator (instance_settings.operator_user_id — the durable record,
|
|
442
|
+
* CLOUD-3) and the SUPERADMIN_EMAILS allowlist; nobody else.
|
|
443
|
+
*/
|
|
444
|
+
const isLocalSignInAllowed = async (email: string): Promise<boolean> => {
|
|
445
|
+
if (parseSuperadminEmails(env.SUPERADMIN_EMAILS).has(email)) return true;
|
|
446
|
+
const [row] = await db
|
|
447
|
+
.select({ email: user.email })
|
|
448
|
+
.from(instanceSettings)
|
|
449
|
+
.innerJoin(user, eq(user.id, instanceSettings.operatorUserId))
|
|
450
|
+
.limit(1);
|
|
451
|
+
return row?.email?.toLowerCase() === email;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The JWT issuance gate (user-scoped credential model): a grant is the
|
|
456
|
+
* USER — "consent to act as you" — so issuance requires only that the
|
|
457
|
+
* user holds AT LEAST ONE live active membership on this instance, and
|
|
458
|
+
* the minted claims carry NO workspace. The request-time workspace comes
|
|
459
|
+
* from the X-Workspace-Id selector against the user's live memberships
|
|
460
|
+
* (identity/oauth-jwt.ts); a claim could only ever go stale against that.
|
|
461
|
+
*
|
|
462
|
+
* Runs on the authorization_code AND refresh_token grants; throwing
|
|
463
|
+
* aborts issuance with an RFC 6749 error body. It is NOT what revokes a
|
|
464
|
+
* deactivated member's access, though: a refresh without the RFC 8707
|
|
465
|
+
* `resource` param still mints an OPAQUE token that never passes through
|
|
466
|
+
* here. The real enforcement is resource-side — every request re-checks
|
|
467
|
+
* the live membership (and an opaque token fails the bearer gate's
|
|
468
|
+
* looksLikeJwt outright), so whatever a refresh mints is rejected at
|
|
469
|
+
* every resource. Verified live (M9).
|
|
470
|
+
*/
|
|
471
|
+
async function userAccessClaims(user: { id: string; email: string } | null | undefined) {
|
|
472
|
+
if (!user?.id) {
|
|
473
|
+
throw new APIError('FORBIDDEN', {
|
|
474
|
+
error: 'access_denied',
|
|
475
|
+
error_description: 'Token issuance requires a user-bound grant'
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const [row] = await db
|
|
479
|
+
.select({ id: workspaceMembers.id })
|
|
480
|
+
.from(workspaceMembers)
|
|
481
|
+
.where(and(eq(workspaceMembers.userId, user.id), eq(workspaceMembers.isActive, true)))
|
|
482
|
+
.limit(1);
|
|
483
|
+
if (!row) {
|
|
484
|
+
throw new APIError('FORBIDDEN', {
|
|
485
|
+
error: 'access_denied',
|
|
486
|
+
error_description: 'This account has no active membership on this instance'
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return { email: user.email };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return betterAuth({
|
|
493
|
+
plugins: [
|
|
494
|
+
// Cloud edition only (hubSso is constructed iff EDITION=cloud): the
|
|
495
|
+
// "Sign in with Antasphere" relying party. `disableSignUp` is
|
|
496
|
+
// DELIBERATELY unset on this provider — hub SSO is the sanctioned
|
|
497
|
+
// account entrance on cloud, the conscious FOURTH switch next to the
|
|
498
|
+
// three closed-signup switches (internal/federation.md). Discovery, token
|
|
499
|
+
// exchange (with RFC 8707 `resource`), and token verification all
|
|
500
|
+
// live in the provider config hub-sso.ts builds.
|
|
501
|
+
...(hubSso ? [genericOAuth({ config: [hubSso.providerConfig()] })] : []),
|
|
502
|
+
// Demo sign-in, self-hosted only: POST /demo/redeem signs a demo pass's
|
|
503
|
+
// person in (identity/demo-pass-plugin.ts). Never on cloud, where every
|
|
504
|
+
// session entrance goes through the hub.
|
|
505
|
+
...(demoPass && !hubSso ? [demoPassPlugin(demoPass)] : []),
|
|
506
|
+
...(sendOtp
|
|
507
|
+
? [
|
|
508
|
+
emailOTP({
|
|
509
|
+
// Closed instance: OTP signs in EXISTING accounts only — without
|
|
510
|
+
// this, an OTP send to any unknown email would mint a user.
|
|
511
|
+
disableSignUp: true,
|
|
512
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
513
|
+
await sendOtp({ email, otp, type });
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
]
|
|
517
|
+
: []),
|
|
518
|
+
// Optional per-user 2FA: TOTP + one-time backup codes (ADR 009). NO
|
|
519
|
+
// otpOptions.sendOTP — an emailed code as "second factor" would collapse
|
|
520
|
+
// into the first factor for anyone who controls the mailbox. Enrollment
|
|
521
|
+
// is password-gated and pending until the user proves a working
|
|
522
|
+
// authenticator by verifying one TOTP code; disable is password-gated.
|
|
523
|
+
// Sign-up stays closed: the plugin adds no account-creating route.
|
|
524
|
+
twoFactor({
|
|
525
|
+
// Label shown in authenticator apps — the instance's own host.
|
|
526
|
+
issuer: new URL(env.PUBLIC_BASE_URL).host
|
|
527
|
+
}),
|
|
528
|
+
jwt({
|
|
529
|
+
// JWTs exist ONLY as OAuth access tokens — never attach a signed JWT
|
|
530
|
+
// to plain session responses (recommended with an oauth provider).
|
|
531
|
+
disableSettingJwtHeader: true,
|
|
532
|
+
// RS256 over the default EdDSA: broadest verifier compatibility.
|
|
533
|
+
jwks: { keyPairConfig: { alg: 'RS256' } },
|
|
534
|
+
// Clean origin (no /api/v1/auth path) as the OAuth issuer — the exact
|
|
535
|
+
// `iss` oauth-jwt.ts pins, and the root of the RFC 8414 discovery
|
|
536
|
+
// documents re-served by routes/wellknown.ts.
|
|
537
|
+
jwt: { issuer: env.PUBLIC_BASE_URL }
|
|
538
|
+
}),
|
|
539
|
+
oauthProvider({
|
|
540
|
+
// SPA routes; the API-side validation is Better Auth's signed query.
|
|
541
|
+
loginPage: '/login',
|
|
542
|
+
consentPage: '/oauth/consent',
|
|
543
|
+
scopes: [...oauthScopes],
|
|
544
|
+
// `resource` (RFC 8707) must be the bundled MCP endpoint — its exact `aud`.
|
|
545
|
+
validAudiences: [resource],
|
|
546
|
+
// No M2M clients: every token is bound to a consenting user.
|
|
547
|
+
grantTypes: ['authorization_code', 'refresh_token'],
|
|
548
|
+
// MCP clients (claude.ai, Claude Desktop) register themselves via
|
|
549
|
+
// RFC 7591 without a session. Registration alone grants nothing —
|
|
550
|
+
// tokens still require login + consent + an active membership — and
|
|
551
|
+
// the register endpoint is rate-limited in api/index.ts.
|
|
552
|
+
//
|
|
553
|
+
// PLT-29: it is still an anonymous write to the authorization server's
|
|
554
|
+
// client table, so an operator gets a switch rather than a hardcoded
|
|
555
|
+
// posture. OAUTH_DYNAMIC_CLIENT_REGISTRATION=false turns BOTH flags
|
|
556
|
+
// off and /oauth2/register refuses everyone.
|
|
557
|
+
//
|
|
558
|
+
// THE DEFAULT IS DELIBERATELY UNCHANGED (true): the MCP connector
|
|
559
|
+
// story depends on open registration, and the fleet-wide decision on
|
|
560
|
+
// whether that posture should change belongs to the OIDC audit, not to
|
|
561
|
+
// this hardening pass. This adds the control and nothing else.
|
|
562
|
+
allowDynamicClientRegistration: env.OAUTH_DYNAMIC_CLIENT_REGISTRATION,
|
|
563
|
+
allowUnauthenticatedClientRegistration: env.OAUTH_DYNAMIC_CLIENT_REGISTRATION,
|
|
564
|
+
clientRegistrationDefaultScopes: [...oauthScopes],
|
|
565
|
+
clientRegistrationAllowedScopes: [...oauthScopes],
|
|
566
|
+
// Short access tokens: stateless JWTs can't be revoked, so revocation
|
|
567
|
+
// latency == remaining lifetime. Refresh tokens rotate (reuse detected).
|
|
568
|
+
accessTokenExpiresIn: 900, // 15 min
|
|
569
|
+
refreshTokenExpiresIn: 60 * 60 * 24 * 365, // 365 days (sliding; see the session note)
|
|
570
|
+
customAccessTokenClaims: async ({ user }) => userAccessClaims(user),
|
|
571
|
+
// Grants are user-scoped ("act as you"): consents bind NOTHING —
|
|
572
|
+
// the explicit `undefined` referenceId is the no-binding wiring
|
|
573
|
+
// (the plugin type requires the key whenever postLogin exists).
|
|
574
|
+
// `page` + `shouldRedirect` are required by the type too; the
|
|
575
|
+
// constant false keeps the flow on the consent page itself.
|
|
576
|
+
postLogin: {
|
|
577
|
+
page: '/oauth/consent',
|
|
578
|
+
shouldRedirect: () => false,
|
|
579
|
+
consentReferenceId: async () => undefined
|
|
580
|
+
},
|
|
581
|
+
// Root discovery documents are re-served by routes/wellknown.ts.
|
|
582
|
+
silenceWarnings: { oauthAuthServerConfig: true, openidConfig: true }
|
|
583
|
+
})
|
|
584
|
+
],
|
|
585
|
+
// Google social sign-in is a SELF-HOST option only. On EDITION=cloud
|
|
586
|
+
// (hubSso present) the provider is NOT registered at all — even with
|
|
587
|
+
// GOOGLE_CLIENT_ID/SECRET set — so /sign-in/social and
|
|
588
|
+
// /oauth2/callback/google mint nothing there. Under the D1 hub-only
|
|
589
|
+
// posture the sole sanctioned human entrance is "Sign in with
|
|
590
|
+
// Antasphere" (the `antasphere` genericOAuth provider), with
|
|
591
|
+
// /sign-in/email kept as the deliberate break-glass door; a live Google
|
|
592
|
+
// provider would be a NON-hub session entrance that defeats the
|
|
593
|
+
// audit-completeness guarantee (every cloud login must trace through the
|
|
594
|
+
// hub). Gating on !hubSso — not on "operator didn't set the env var" —
|
|
595
|
+
// makes the guarantee hold by construction (defense in depth against
|
|
596
|
+
// misconfiguration), mirroring the OTP closures. oss is unchanged: with
|
|
597
|
+
// the credentials set, Google signs in accounts that already exist
|
|
598
|
+
// (disableSignUp — the callback never creates), one of the three
|
|
599
|
+
// closed-signup switches.
|
|
600
|
+
socialProviders:
|
|
601
|
+
!hubSso && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
|
|
602
|
+
? {
|
|
603
|
+
google: {
|
|
604
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
605
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
606
|
+
disableSignUp: true
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
: {},
|
|
610
|
+
account: {
|
|
611
|
+
// OAuth provider tokens on the `account` row are encrypted at rest —
|
|
612
|
+
// on cloud that row IS the user's hub grant (offline_access refresh
|
|
613
|
+
// token, identity/hub-grant.ts), which must never sit plaintext in a
|
|
614
|
+
// dump. Enabled on BOTH editions (oss: Google tokens, when
|
|
615
|
+
// configured). CONFIG-ONLY: no schema change (drift:check proves),
|
|
616
|
+
// and retroactively safe — the decrypt passes legacy PLAINTEXT values
|
|
617
|
+
// through untouched (dist/oauth2/utils.mjs isLikelyEncrypted, verified
|
|
618
|
+
// on 1.6.15 and again on 1.6.22;
|
|
619
|
+
// hub-grant.ts mirrors the same rule). The key is the auth context's
|
|
620
|
+
// secretConfig (= this instance's AUTH_SECRET under our string-secret
|
|
621
|
+
// config). Re-verify both facts on any Better Auth bump.
|
|
622
|
+
encryptOAuthTokens: true,
|
|
623
|
+
// D9 (cloud only): the hub is a TRUSTED provider — its verified email
|
|
624
|
+
// assertion may link onto an existing local account of the same
|
|
625
|
+
// address (the setup operator's entrance under hub-only login).
|
|
626
|
+
// Keeping requireLocalEmailVerified true (stated, not defaulted)
|
|
627
|
+
// means a parked UNVERIFIED local account can never be taken over via
|
|
628
|
+
// SSO; the cloud setup flow mints the operator emailVerified=true for
|
|
629
|
+
// exactly this reason (internal/federation.md, pinned by edition tests).
|
|
630
|
+
...(hubSso
|
|
631
|
+
? {
|
|
632
|
+
accountLinking: {
|
|
633
|
+
trustedProviders: [HUB_SSO_PROVIDER_ID],
|
|
634
|
+
requireLocalEmailVerified: true
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
: {})
|
|
638
|
+
},
|
|
639
|
+
baseURL: env.PUBLIC_BASE_URL,
|
|
640
|
+
basePath: AUTH_BASE_PATH,
|
|
641
|
+
secret: authSecret,
|
|
642
|
+
// Cloud only: OAuth-callback error redirects land on the LOGIN PAGE.
|
|
643
|
+
// Verified on 1.6.15 and 1.6.22 (re-verify on ANY bump): the genericOAuth callback
|
|
644
|
+
// resolves `onAPIError.errorURL || <authBase>/error` and redirects an
|
|
645
|
+
// AS error (`?error=login_required` from a prompt=none authorize,
|
|
646
|
+
// `?error=access_denied`, …) there BEFORE parseState runs
|
|
647
|
+
// (generic-oauth/routes.mjs) — errorCallbackURL never sees these, so
|
|
648
|
+
// without this the silent-connect bounce would dead-end on better-auth's
|
|
649
|
+
// own /error page. The dashboard maps the ?error=… code to copy (and the
|
|
650
|
+
// silent-connect lattice treats the login_required family as a quiet
|
|
651
|
+
// hint-clear, Stage E). oss stays byte-identical: key absent, the
|
|
652
|
+
// pre-change default error landing untouched.
|
|
653
|
+
...(hubSso ? { onAPIError: { errorURL: env.PUBLIC_BASE_URL.replace(/\/+$/, '') + '/login' } } : {}),
|
|
654
|
+
database: drizzleAdapter(db, {
|
|
655
|
+
provider: 'pg',
|
|
656
|
+
schema: {
|
|
657
|
+
user,
|
|
658
|
+
session,
|
|
659
|
+
account,
|
|
660
|
+
verification,
|
|
661
|
+
jwks,
|
|
662
|
+
oauthClient,
|
|
663
|
+
oauthAccessToken,
|
|
664
|
+
oauthRefreshToken,
|
|
665
|
+
oauthConsent,
|
|
666
|
+
twoFactor: twoFactorTable
|
|
667
|
+
}
|
|
668
|
+
}),
|
|
669
|
+
user: {
|
|
670
|
+
// Self-serve email change. Enabled unconditionally: without a
|
|
671
|
+
// delivering email driver POST /change-email answers 400 BEFORE the
|
|
672
|
+
// email lookup (no enumeration oracle), so the switch is safe. Tokens
|
|
673
|
+
// are stateless HS256 JWTs signed with the auth secret (1h expiry,
|
|
674
|
+
// not revocable). Template users are typically emailVerified=false, so
|
|
675
|
+
// the COMMON flow is single-leg: one verification mail to the NEW
|
|
676
|
+
// address; the confirmation-to-old-address leg only runs for verified
|
|
677
|
+
// users.
|
|
678
|
+
changeEmail: {
|
|
679
|
+
enabled: true,
|
|
680
|
+
...(sendChangeEmailConfirmation
|
|
681
|
+
? {
|
|
682
|
+
sendChangeEmailConfirmation: async ({
|
|
683
|
+
user,
|
|
684
|
+
newEmail,
|
|
685
|
+
url,
|
|
686
|
+
token
|
|
687
|
+
}: {
|
|
688
|
+
user: { email: string };
|
|
689
|
+
newEmail: string;
|
|
690
|
+
url: string;
|
|
691
|
+
token: string;
|
|
692
|
+
}) => {
|
|
693
|
+
await sendChangeEmailConfirmation({ email: user.email, newEmail, url, token });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
: {})
|
|
697
|
+
},
|
|
698
|
+
// Self-service account deletion (GDPR erasure). NO
|
|
699
|
+
// sendDeleteAccountVerification: the password path works with zero
|
|
700
|
+
// SMTP. Known Better Auth default, relied on rather than fought: a
|
|
701
|
+
// session younger than freshAge (24h) may delete WITHOUT a password
|
|
702
|
+
// server-side. The dashboard collects the password where the instance
|
|
703
|
+
// offers one (`/instance` auth.methods has `password`); a hub-JIT user
|
|
704
|
+
// on cloud has NO credential account, so there the dashboard sends the
|
|
705
|
+
// person through a fresh hub sign-in first and the new session is the
|
|
706
|
+
// proof (PRDCT-2640). Deletion is local either way: it erases the
|
|
707
|
+
// instance's user row, never the hub identity (accounts/deletion.ts).
|
|
708
|
+
// See ADR 006.
|
|
709
|
+
deleteUser: {
|
|
710
|
+
enabled: true,
|
|
711
|
+
beforeDelete: async (user: { id: string }) => {
|
|
712
|
+
await beforeUserDelete?.(user.id);
|
|
713
|
+
},
|
|
714
|
+
afterDelete: async (user: { id: string; email: string }) => {
|
|
715
|
+
await afterUserDelete?.({ id: user.id, email: user.email });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
emailVerification: {
|
|
720
|
+
expiresIn: 3600,
|
|
721
|
+
...(sendVerificationEmail
|
|
722
|
+
? {
|
|
723
|
+
sendVerificationEmail: async ({
|
|
724
|
+
user,
|
|
725
|
+
url,
|
|
726
|
+
token
|
|
727
|
+
}: {
|
|
728
|
+
user: { email: string };
|
|
729
|
+
url: string;
|
|
730
|
+
token: string;
|
|
731
|
+
}) => {
|
|
732
|
+
// `user.email` is already the NEW address on the change-email leg.
|
|
733
|
+
await sendVerificationEmail({ email: user.email, url, token });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
: {}),
|
|
737
|
+
// Fires when GET /verify-email flips a user to verified — the email
|
|
738
|
+
// change landing. Also fires on a hypothetical plain verify-email
|
|
739
|
+
// (acceptable: the audit row records a verification event either way).
|
|
740
|
+
afterEmailVerification: async (user: { id: string }) => {
|
|
741
|
+
if (user?.id) await onAccountEvent?.('email_change', user.id);
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
emailAndPassword: {
|
|
745
|
+
enabled: true,
|
|
746
|
+
minPasswordLength: 12,
|
|
747
|
+
// Recovery/change implies a possibly-compromised account: drop the
|
|
748
|
+
// other sessions on a reset. Applies to self-serve AND admin-minted
|
|
749
|
+
// reset links (both land on POST /reset-password).
|
|
750
|
+
revokeSessionsOnPasswordReset: true,
|
|
751
|
+
onPasswordReset: async ({ user }: { user: { id: string } }) => {
|
|
752
|
+
await onAccountEvent?.('password_reset', user.id);
|
|
753
|
+
},
|
|
754
|
+
// Self-serve reset exists only with a delivering email driver; without
|
|
755
|
+
// it, /request-password-reset returns 400 (no user-enumeration oracle).
|
|
756
|
+
// Cloud never wires it (defense in depth under the before-hook refusal
|
|
757
|
+
// above): no reset mail can even be BUILT on that edition.
|
|
758
|
+
...(sendResetPassword && !hubSso
|
|
759
|
+
? {
|
|
760
|
+
resetPasswordTokenExpiresIn: 3600,
|
|
761
|
+
sendResetPassword: async ({
|
|
762
|
+
user,
|
|
763
|
+
url,
|
|
764
|
+
token
|
|
765
|
+
}: {
|
|
766
|
+
user: { email: string };
|
|
767
|
+
url: string;
|
|
768
|
+
token: string;
|
|
769
|
+
}) => {
|
|
770
|
+
await sendResetPassword({ email: user.email, url, token });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
: {})
|
|
774
|
+
},
|
|
775
|
+
hooks: {
|
|
776
|
+
// Accounts enter through setup or invitations only. HTTP sign-up is
|
|
777
|
+
// closed: ctx.request exists exactly when the call arrived over HTTP;
|
|
778
|
+
// server-side auth.api.signUpEmail calls (setup, invitation accept)
|
|
779
|
+
// carry no request and pass.
|
|
780
|
+
before: createAuthMiddleware(async (ctx) => {
|
|
781
|
+
if (ctx.path.startsWith('/sign-up') && ctx.request) {
|
|
782
|
+
throw new APIError('FORBIDDEN', {
|
|
783
|
+
message: 'Sign-up is closed on this instance — ask an admin for an invitation'
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
// Cloud edition (D1, hub-only login): the ENTIRE local password-reset
|
|
787
|
+
// surface refuses — see isPasswordResetPath for the enumerated routes
|
|
788
|
+
// and why. Unconditional (no ctx.request escape hatch like sign-up's):
|
|
789
|
+
// nothing server-side consumes these paths, so a server-side caller
|
|
790
|
+
// appearing would itself be a posture regression. oss is untouched
|
|
791
|
+
// (hubSso exists only on EDITION=cloud boots).
|
|
792
|
+
if (hubSso && isPasswordResetPath(ctx.path)) {
|
|
793
|
+
throw new APIError('FORBIDDEN', {
|
|
794
|
+
message:
|
|
795
|
+
'Password reset is disabled on this edition — credentials are managed at the Antasphere hub'
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
// Cloud edition (PRDCT-2818): the email address is the hub's — see
|
|
799
|
+
// isEmailChangePath. Unconditional like the reset closure: nothing
|
|
800
|
+
// server-side calls these paths.
|
|
801
|
+
if (hubSso && isEmailChangePath(ctx.path)) {
|
|
802
|
+
throw new APIError('FORBIDDEN', {
|
|
803
|
+
code: 'email_change_disabled',
|
|
804
|
+
message: `Email changes are disabled on this edition — change your address on your Antasphere account (${hubSso.accountUrl()})`
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
// Cloud edition (D1, hub-only login): the emailOTP SIGN-IN surface
|
|
808
|
+
// refuses too — see isOtpSignInPath for the enumerated routes and
|
|
809
|
+
// why. Same unconditional stance as the reset closure (no
|
|
810
|
+
// ctx.request escape hatch): the one server-side consumer of these
|
|
811
|
+
// paths, /cli/auth/request's sendVerificationOTP delegate, refuses
|
|
812
|
+
// at its own route first on cloud (cli_otp_disabled), so a
|
|
813
|
+
// server-side caller reaching this would itself be a posture
|
|
814
|
+
// regression. oss is untouched (hubSso exists only on cloud boots).
|
|
815
|
+
if (hubSso && isOtpSignInPath(ctx.path)) {
|
|
816
|
+
throw new APIError('FORBIDDEN', {
|
|
817
|
+
code: 'otp_signin_disabled',
|
|
818
|
+
message: 'Email-code sign-in is disabled on this edition — use "Sign in with Antasphere"'
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
// BOTH editions: the provider-grant read/rotate routes are closed —
|
|
822
|
+
// see isProviderGrantPath for the enumeration and the three reasons.
|
|
823
|
+
// Unconditional (no `ctx.request` escape hatch): nothing server-side
|
|
824
|
+
// calls them either, so a server-side caller appearing would itself
|
|
825
|
+
// be the regression this guard exists to catch.
|
|
826
|
+
// Cloud edition (CLOUD-1 / AUTH-13, PRDCT-1356): unlinking the
|
|
827
|
+
// `antasphere` provider is refused. The /auth/* mount sits BEFORE
|
|
828
|
+
// authContext (isPublicApiPath), so the principal gate never saw
|
|
829
|
+
// this route; a hub user who unlinked kept their origin='hub'
|
|
830
|
+
// memberships while every later reconcile took the `no_link` branch
|
|
831
|
+
// — the one fail-open outcome — cached like a success. Enforcement
|
|
832
|
+
// must not be switchable by its subject. (The reconciler now also
|
|
833
|
+
// fails CLOSED on no_link for a principal holding hub-origin rows.)
|
|
834
|
+
if (hubSso && ctx.path === '/unlink-account') {
|
|
835
|
+
const providerId = (ctx.body as { providerId?: unknown } | undefined)?.providerId;
|
|
836
|
+
if (providerId === 'antasphere') {
|
|
837
|
+
throw new APIError('FORBIDDEN', {
|
|
838
|
+
code: 'hub_unlink_forbidden',
|
|
839
|
+
message: 'The Antasphere link cannot be removed on this edition — it is the identity itself'
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Cloud edition (EDIT-2, PRDCT-1356): /sign-in/email stays WIRED as
|
|
844
|
+
// the break-glass door, but it is a door for the OPERATOR and the
|
|
845
|
+
// SUPERADMIN_EMAILS allowlist only. Every other local credential on
|
|
846
|
+
// a cloud instance (pre-flip users, a leftover invitation account)
|
|
847
|
+
// would otherwise be a permanent non-SSO entrance that never sees
|
|
848
|
+
// the hub's audit log. Decided on the presented email; unknown
|
|
849
|
+
// addresses answer the same 403 (no account oracle).
|
|
850
|
+
if (hubSso && ctx.path === '/sign-in/email' && ctx.request) {
|
|
851
|
+
const email = String((ctx.body as { email?: unknown } | undefined)?.email ?? '')
|
|
852
|
+
.trim()
|
|
853
|
+
.toLowerCase();
|
|
854
|
+
if (!email || !(await isLocalSignInAllowed(email))) {
|
|
855
|
+
throw new APIError('FORBIDDEN', {
|
|
856
|
+
code: 'local_signin_disabled',
|
|
857
|
+
message:
|
|
858
|
+
'Password sign-in is reserved for the instance operator on this edition — use "Sign in with Antasphere"'
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (isProviderGrantPath(ctx.path)) {
|
|
863
|
+
throw new APIError('FORBIDDEN', {
|
|
864
|
+
code: 'provider_grant_forbidden',
|
|
865
|
+
message: 'This endpoint is disabled — provider tokens are never handed to callers'
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
// Login-CSRF (session fixation) hardening: Better Auth's own origin
|
|
869
|
+
// check deliberately SKIPS requests carrying neither cookies nor
|
|
870
|
+
// Sec-Fetch metadata (progressive enhancement for non-browser
|
|
871
|
+
// clients), which leaves legacy browsers open to a cross-site login
|
|
872
|
+
// POST. Close the gap on the credential-issuing sign-in surface: a
|
|
873
|
+
// sign-in that DOES present an Origin must present a trusted one —
|
|
874
|
+
// trust evaluation is Better Auth's own isTrustedOrigin plus the
|
|
875
|
+
// serving origin (mirroring the trustedOrigins function below).
|
|
876
|
+
// CLI/SDK/MCP flows send no Origin header and are untouched; the
|
|
877
|
+
// OAuth token/register endpoints keep their deliberate public
|
|
878
|
+
// wildcard-CORS posture (middleware/oauth-public.ts).
|
|
879
|
+
if (ctx.path.startsWith('/sign-in') && ctx.request) {
|
|
880
|
+
const origin = ctx.request.headers.get('origin');
|
|
881
|
+
if (origin) {
|
|
882
|
+
let servingOrigin: string | null = null;
|
|
883
|
+
try {
|
|
884
|
+
servingOrigin = new URL(ctx.request.url).origin;
|
|
885
|
+
} catch {
|
|
886
|
+
// unparseable request URL — judge by the trusted list only
|
|
887
|
+
}
|
|
888
|
+
if (
|
|
889
|
+
signInOriginRefused({
|
|
890
|
+
origin,
|
|
891
|
+
servingOrigin,
|
|
892
|
+
untrustedOrigins,
|
|
893
|
+
isTrustedOrigin: (o) => ctx.context.isTrustedOrigin(o)
|
|
894
|
+
})
|
|
895
|
+
) {
|
|
896
|
+
throw new APIError('FORBIDDEN', { message: 'Invalid origin' });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
// PRDCT-1437 door 1 (the change-email consume existence oracle) is
|
|
901
|
+
// closed at the mount wrapper in api/index.ts, not here: it needs to
|
|
902
|
+
// rewrite the REQUEST before Better Auth reads the token, which a
|
|
903
|
+
// before-hook's `ctx.query` mutation does not reach (the endpoint reads
|
|
904
|
+
// its own parsed query). See `rewriteChangeEmailCollision` there.
|
|
905
|
+
// The plugin scheme-checks only redirect_uris — reject javascript:/data:
|
|
906
|
+
// metadata URIs on every client write path (unauthenticated DCR AND the
|
|
907
|
+
// session-gated create/update-client endpoints) so a stored javascript:
|
|
908
|
+
// URI can never reach a render site. The consent page also validates at
|
|
909
|
+
// render (defense in depth).
|
|
910
|
+
if (
|
|
911
|
+
ctx.path === '/oauth2/register' ||
|
|
912
|
+
ctx.path === '/oauth2/create-client' ||
|
|
913
|
+
ctx.path === '/oauth2/update-client'
|
|
914
|
+
) {
|
|
915
|
+
const raw = ctx.body as Record<string, unknown> | undefined;
|
|
916
|
+
// register/create-client carry the fields at the top level;
|
|
917
|
+
// update-client nests them under `update`.
|
|
918
|
+
const meta = (raw?.update ?? raw) as Record<string, unknown> | undefined;
|
|
919
|
+
for (const field of CLIENT_METADATA_URI_FIELDS) {
|
|
920
|
+
const value = meta?.[field];
|
|
921
|
+
if (value != null && value !== '' && !isHttpUrl(value)) {
|
|
922
|
+
throw new APIError('BAD_REQUEST', {
|
|
923
|
+
error: 'invalid_client_metadata',
|
|
924
|
+
error_description: `${field} must be an http(s) URL`
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}),
|
|
930
|
+
// A successful /change-password or /change-email reaches the after-hook
|
|
931
|
+
// (an error throws before it) — audit them here since Better Auth
|
|
932
|
+
// routes bypass the /api/v1 audit middleware. Reset is audited via
|
|
933
|
+
// onPasswordReset above; the email-change LANDING (the verify-email
|
|
934
|
+
// consumption) via afterEmailVerification.
|
|
935
|
+
after: createAuthMiddleware(async (ctx) => {
|
|
936
|
+
// Hub SSO per-login work (cloud only) — the callback after-hook.
|
|
937
|
+
// Fires for JIT AND returning users; `ctx.context.newSession` is the
|
|
938
|
+
// success discriminator (set by setSessionCookie before the redirect
|
|
939
|
+
// throw, never on the plugin's error exits — S1(b)). The verified
|
|
940
|
+
// assertion crosses from getUserInfo via the request-scoped login
|
|
941
|
+
// scope (ADR 015); everything here FAILS CLOSED: no assertion or a
|
|
942
|
+
// failed projection revokes the just-minted session — a cloud login
|
|
943
|
+
// without its hub-asserted workspace projection must not exist.
|
|
944
|
+
if (hubSso && ctx.path === '/oauth2/callback/:providerId') {
|
|
945
|
+
const providerId = (ctx as { params?: Record<string, string> }).params?.providerId;
|
|
946
|
+
if (providerId === HUB_SSO_PROVIDER_ID) {
|
|
947
|
+
const data = ctx.context.newSession;
|
|
948
|
+
if (!data?.user) return; // error exits mint no session — nothing to assert
|
|
949
|
+
const assertion = hubSso.takeAssertion();
|
|
950
|
+
try {
|
|
951
|
+
if (!assertion) {
|
|
952
|
+
throw new HubSsoLoginError(
|
|
953
|
+
'sso_assertion_missing',
|
|
954
|
+
'callback succeeded but no verified hub assertion reached the after-hook'
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
await hubSso.assertLogin(data.user.id, assertion);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
hubSso.logLoginFailure(err, data.user.id);
|
|
960
|
+
// Same undo dance as the 2FA interstitial below: drop the
|
|
961
|
+
// session row, expire the cookie, clear newSession, then
|
|
962
|
+
// replace the success redirect with the error redirect.
|
|
963
|
+
await ctx.context.internalAdapter.deleteSession(data.session.token);
|
|
964
|
+
deleteSessionCookie(ctx, true);
|
|
965
|
+
ctx.context.setNewSession(null);
|
|
966
|
+
const code = err instanceof HubSsoLoginError ? err.code : 'sso_login_failed';
|
|
967
|
+
throw ctx.redirect(hubSso.loginErrorUrl(code));
|
|
968
|
+
}
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (ctx.path === '/change-password') {
|
|
973
|
+
const userId = ctx.context.session?.user?.id;
|
|
974
|
+
if (userId) await onAccountEvent?.('password_change', userId);
|
|
975
|
+
}
|
|
976
|
+
if (ctx.path === '/change-email') {
|
|
977
|
+
const userId = ctx.context.session?.user?.id;
|
|
978
|
+
if (userId) await onAccountEvent?.('email_change_request', userId);
|
|
979
|
+
}
|
|
980
|
+
// 2FA credential-material events (both routes are session-gated).
|
|
981
|
+
// Enroll = secret + backup codes issued (activation completes on the
|
|
982
|
+
// first verified TOTP); disable = secret + backup codes destroyed.
|
|
983
|
+
if (ctx.path === '/two-factor/enable') {
|
|
984
|
+
const userId = ctx.context.session?.user?.id;
|
|
985
|
+
if (userId) await onAccountEvent?.('two_factor_enroll', userId);
|
|
986
|
+
}
|
|
987
|
+
if (ctx.path === '/two-factor/disable') {
|
|
988
|
+
const userId = ctx.context.session?.user?.id;
|
|
989
|
+
if (userId) await onAccountEvent?.('two_factor_disable', userId);
|
|
990
|
+
}
|
|
991
|
+
// Close the email-OTP second-factor bypass: the twoFactor plugin's
|
|
992
|
+
// own sign-in hook covers /sign-in/email (password) but NOT
|
|
993
|
+
// /sign-in/email-otp, so an emailed one-time code would sign a
|
|
994
|
+
// 2FA-enabled user straight in — collapsing 2FA to mailbox control.
|
|
995
|
+
// Mirror the plugin's dance (verified against 1.6.22
|
|
996
|
+
// plugins/two-factor/index.mjs; re-verify on ANY Better Auth bump):
|
|
997
|
+
// drop the just-minted session, park the pending sign-in in the
|
|
998
|
+
// verification table behind the signed `two_factor` cookie, park the
|
|
999
|
+
// per-challenge attempt counter next to it, and answer
|
|
1000
|
+
// { twoFactorRedirect } so /two-factor/verify-totp (or a backup code)
|
|
1001
|
+
// completes it. A 2FA-enabled user always has a verified TOTP secret
|
|
1002
|
+
// — activation is the only path that sets user.twoFactorEnabled.
|
|
1003
|
+
// The `2fa-attempts-<identifier>` row is NOT optional since 1.6.22:
|
|
1004
|
+
// verifyTwoFactor's beginAttempt CONSUMES it on every verify and
|
|
1005
|
+
// throws INVALID_TWO_FACTOR_COOKIE (401) when it is missing, so a
|
|
1006
|
+
// mirror that parks only the `2fa-` row hands the user a pending
|
|
1007
|
+
// state no factor can ever complete (PRDCT-1344; the hub hit the
|
|
1008
|
+
// same trap on its own bump).
|
|
1009
|
+
if (ctx.path === '/sign-in/email-otp') {
|
|
1010
|
+
const data = ctx.context.newSession;
|
|
1011
|
+
if (!data?.user || !(data.user as { twoFactorEnabled?: boolean }).twoFactorEnabled) return;
|
|
1012
|
+
deleteSessionCookie(ctx, true);
|
|
1013
|
+
await ctx.context.internalAdapter.deleteSession(data.session.token);
|
|
1014
|
+
ctx.context.setNewSession(null);
|
|
1015
|
+
const maxAge = 600; // the plugin's twoFactorCookieMaxAge default
|
|
1016
|
+
const twoFactorCookie = ctx.context.createAuthCookie('two_factor', { maxAge });
|
|
1017
|
+
const identifier = `2fa-${generateRandomString(20)}`;
|
|
1018
|
+
const expiresAt = new Date(Date.now() + maxAge * 1000);
|
|
1019
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1020
|
+
value: data.user.id,
|
|
1021
|
+
identifier,
|
|
1022
|
+
expiresAt
|
|
1023
|
+
});
|
|
1024
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1025
|
+
value: '0',
|
|
1026
|
+
identifier: `2fa-attempts-${identifier}`,
|
|
1027
|
+
expiresAt
|
|
1028
|
+
});
|
|
1029
|
+
await ctx.setSignedCookie(
|
|
1030
|
+
twoFactorCookie.name,
|
|
1031
|
+
identifier,
|
|
1032
|
+
ctx.context.secret,
|
|
1033
|
+
twoFactorCookie.attributes
|
|
1034
|
+
);
|
|
1035
|
+
return ctx.json({ twoFactorRedirect: true, twoFactorMethods: ['totp'] });
|
|
1036
|
+
}
|
|
1037
|
+
})
|
|
1038
|
+
},
|
|
1039
|
+
// Adapter-level hooks: config only, NOT schema — the drift guard's
|
|
1040
|
+
// generated auth-schema is unaffected (verified via drift:check).
|
|
1041
|
+
databaseHooks: {
|
|
1042
|
+
user: {
|
|
1043
|
+
create: {
|
|
1044
|
+
// Every entrance (setup's signUpEmail, invitation accept, the
|
|
1045
|
+
// collaborator claim endpoint, a future SSO JIT) creates the row
|
|
1046
|
+
// through the internal adapter, so this is the single trustworthy
|
|
1047
|
+
// `user.created` seam. Synchronous dispatch onto the event bus;
|
|
1048
|
+
// subscriber failures are isolated there and never fail account
|
|
1049
|
+
// creation.
|
|
1050
|
+
after: async (user: { id: string; email: string }) => {
|
|
1051
|
+
onUserCreated?.({ id: user.id, email: user.email });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
// Sessions, split by edition (SL-5, "one concept of being logged in"):
|
|
1057
|
+
//
|
|
1058
|
+
// - oss keeps the template's long-lived SLIDING sessions, byte-identical
|
|
1059
|
+
// (safe because the auth-context middleware re-checks workspace
|
|
1060
|
+
// membership on every request — instant revocation).
|
|
1061
|
+
// - cloud runs 30-day FIXED (non-sliding) sessions: the tool session is
|
|
1062
|
+
// a PROJECTION of the hub anchor session, and a sliding projection
|
|
1063
|
+
// would never re-dance — it would let the anchor die under an active
|
|
1064
|
+
// user. A fixed expiry forces a silent prompt=none re-derivation from
|
|
1065
|
+
// the anchor at most every 30 days, and each re-derivation's authorize
|
|
1066
|
+
// touch slides the HUB session forward (the anchor stays alive off
|
|
1067
|
+
// activity on ANY tool). The knob is `disableSessionRefresh` — pinned
|
|
1068
|
+
// on 1.6.15 and 1.6.22 (dist/api/routes/session.mjs is its ONLY reader:
|
|
1069
|
+
// `needsRefresh` goes false, so expiresAt never advances on use;
|
|
1070
|
+
// re-verify on ANY bump). `updateAge` is left at its default there —
|
|
1071
|
+
// it is dead config once refresh is disabled. CONFIG-ONLY: no schema
|
|
1072
|
+
// change (drift:check proves).
|
|
1073
|
+
session: hubSso
|
|
1074
|
+
? {
|
|
1075
|
+
expiresIn: 60 * 60 * 24 * 30,
|
|
1076
|
+
disableSessionRefresh: true
|
|
1077
|
+
}
|
|
1078
|
+
: {
|
|
1079
|
+
expiresIn: 60 * 60 * 24 * 365,
|
|
1080
|
+
updateAge: 60 * 60 * 24
|
|
1081
|
+
},
|
|
1082
|
+
advanced: {
|
|
1083
|
+
useSecureCookies: isHttps
|
|
1084
|
+
},
|
|
1085
|
+
// The library's own limiter, on in production, keyed per client ADDRESS. Its defaults
|
|
1086
|
+
// (100 requests per 10 s per path, and 3 per 10 s on every /sign-in/*) let a room behind one
|
|
1087
|
+
// venue address sign in at about 18 people a minute and share one session-check budget
|
|
1088
|
+
// (proven on the cloud instances, 2026-10-01: the fourth sign-in start in 10 s answered 429).
|
|
1089
|
+
// Sized for an event: the general budget at ten times the default, the SSO start at 500.
|
|
1090
|
+
// The per-person walls (login, otp, the API quota) stay what they are.
|
|
1091
|
+
rateLimit: {
|
|
1092
|
+
window: 10,
|
|
1093
|
+
max: 1000,
|
|
1094
|
+
customRules: {
|
|
1095
|
+
'/sign-in/oauth2': { window: 10, max: 500 }
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
// Trust the serving origin (works on localhost, previews, any domain)
|
|
1099
|
+
// plus the configured public origin behind a TLS-terminating proxy.
|
|
1100
|
+
// Serving origin + public origin, never the viewer origin (see trustedOriginsFor).
|
|
1101
|
+
trustedOrigins: trustedOriginsFor(env.PUBLIC_BASE_URL, untrustedOrigins)
|
|
1102
|
+
});
|
|
1103
|
+
}
|