@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,696 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { JWTPayload } from 'jose';
|
|
3
|
+
import { and, eq, ne } from 'drizzle-orm';
|
|
4
|
+
import type { GenericOAuthConfig } from 'better-auth/plugins';
|
|
5
|
+
import { account, user as userTable, userOnboarding, type Db } from '@antasphere/chassis-db';
|
|
6
|
+
import type { Logger } from '../logger.js';
|
|
7
|
+
import { HubJwtVerifier } from './hub-jwt.js';
|
|
8
|
+
import type { ReconcilePassOutcome } from './hub-reconcile.js';
|
|
9
|
+
import type { LoginAccessToken } from './hub-user-client.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* "Sign in with Antasphere" — the cloud edition's SSO entrance
|
|
13
|
+
* (internal/federation.md, user-scoped federation). This module owns everything
|
|
14
|
+
* hub-SSO:
|
|
15
|
+
*
|
|
16
|
+
* - the relying-party provider config for better-auth's `genericOAuth`
|
|
17
|
+
* plugin (registered by identity/better-auth.ts ONLY when the instance
|
|
18
|
+
* boots EDITION=cloud — an oss boot never constructs this class);
|
|
19
|
+
* - `HubJwtVerifier`: remote-JWKS verification of hub-minted JWTs (hard
|
|
20
|
+
* iss/aud pinning, RS256 allowlist, cached keys + one forced refresh);
|
|
21
|
+
* - the per-login ASSERTION handoff (an AsyncLocalStorage scope spanning
|
|
22
|
+
* the callback request — see `runWithLoginScope`);
|
|
23
|
+
* - the per-login work better-auth does not know about: identity guard,
|
|
24
|
+
* D10 email re-sync, and the FAIL-CLOSED login reconcile (the org
|
|
25
|
+
* projection) — a cloud login whose reconcile pass fails must not exist.
|
|
26
|
+
*
|
|
27
|
+
* Identity comes from the ID TOKEN ONLY (aud = HUB_CLIENT_ID). The access
|
|
28
|
+
* token is never claim-bearing for the tool: the SSO code exchange sends
|
|
29
|
+
* RFC 8707 `resource = <hub>/mcp` (the `tokenResource` seam constant shared
|
|
30
|
+
* with hub-grant.ts), so the callback access token is HUB-audienced — a
|
|
31
|
+
* bearer for the hub's own `/api/v1`, which the login-time reconcile uses
|
|
32
|
+
* to read the user's org list AS THE USER. Org claims are gone from the
|
|
33
|
+
* login path entirely; the hub's caller-scoped `GET /orgs` is the ONE
|
|
34
|
+
* source of org/membership truth. Refresh + between-logins reads live in
|
|
35
|
+
* hub-grant.ts / hub-user-client.ts; SSO scopes carry `offline_access
|
|
36
|
+
* account:read` so the grant persists on the account row, plus
|
|
37
|
+
* `orgs:create` (HUB_SSO_SCOPES below) for the one write the tool makes at
|
|
38
|
+
* the hub as the user.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* providerId of the hub relying party — the discovery method name in
|
|
43
|
+
* `/api/v1/instance`, the better-auth callback path segment
|
|
44
|
+
* (`/api/v1/auth/oauth2/callback/antasphere` — the redirect URI in the
|
|
45
|
+
* hub's TOOL_REGISTRY entry), and the D9 trusted-provider key.
|
|
46
|
+
*/
|
|
47
|
+
export const HUB_SSO_PROVIDER_ID = 'antasphere';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What one hub login asserts about the USER, extracted from the VERIFIED
|
|
51
|
+
* id_token (aud = our client id). No org half: org truth is read live from
|
|
52
|
+
* the hub, never from token claims.
|
|
53
|
+
*/
|
|
54
|
+
export interface HubSsoAssertion {
|
|
55
|
+
/** Hub user id (`sub`) — matched against `account.accountId`, never a local user id. */
|
|
56
|
+
sub: string;
|
|
57
|
+
email: string;
|
|
58
|
+
emailVerified: boolean;
|
|
59
|
+
name: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What one VERIFIED hub exchange JWT asserts (`POST /sso/cli-connect`,
|
|
64
|
+
* internal/federation.md P5): the USER only. The H3 token still carries ONE
|
|
65
|
+
* org's transitional claims through the hub's compat window, but the tool
|
|
66
|
+
* reads none of them — org truth comes from the connect-time reconcile
|
|
67
|
+
* (as-the-user `GET /orgs` with the grant the SAME H3 response delivered),
|
|
68
|
+
* exactly like the browser login path.
|
|
69
|
+
*/
|
|
70
|
+
export interface HubConnectAssertion extends HubSsoAssertion {
|
|
71
|
+
/** Unique per-token id — consumed one-time-use by the replay ledger. */
|
|
72
|
+
jti: string;
|
|
73
|
+
/** The token's own `exp` — how long the consumed jti must stay claimed. */
|
|
74
|
+
expiresAt: Date;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The minimal slice of better-auth's server API `provisionConnect` needs
|
|
79
|
+
* (structural — `Auth` from identity/better-auth.ts satisfies it; typing it
|
|
80
|
+
* here keeps hub-sso.ts free of an import cycle with that module). Going
|
|
81
|
+
* through the INTERNAL ADAPTER is the point: `createOAuthUser` is the exact
|
|
82
|
+
* call the genericOAuth callback makes for a browser-SSO JIT, so the
|
|
83
|
+
* databaseHooks.user.create.after seam (`user.created` → the collaborator
|
|
84
|
+
* grant sweep) fires identically, and the rows are byte-identical.
|
|
85
|
+
*/
|
|
86
|
+
export interface ConnectAuthSeam {
|
|
87
|
+
$context: Promise<{
|
|
88
|
+
internalAdapter: {
|
|
89
|
+
createOAuthUser: (
|
|
90
|
+
user: { email: string; name: string; emailVerified: boolean },
|
|
91
|
+
account: { providerId: string; accountId: string }
|
|
92
|
+
) => Promise<{ user: { id: string } }>;
|
|
93
|
+
linkAccount: (account: { providerId: string; accountId: string; userId: string }) => Promise<unknown>;
|
|
94
|
+
};
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The login-time face of the org reconciler (hub-reconcile.ts) — bound by
|
|
100
|
+
* boot AFTER construction (the reconciler needs the auth context's secret,
|
|
101
|
+
* which needs createAuth, which needs this service). Structural to avoid a
|
|
102
|
+
* hard cycle.
|
|
103
|
+
*/
|
|
104
|
+
export interface LoginReconciler {
|
|
105
|
+
forceReconcile(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A login failure the after-hook converts into a clean, session-less
|
|
110
|
+
* redirect back to /login?error=<code>. Codes are stable (the dashboard
|
|
111
|
+
* maps them to copy); anything unexpected becomes `sso_login_failed`.
|
|
112
|
+
* `/sso/cli-connect` reuses the same errors as 403 bodies —
|
|
113
|
+
* `sso_link_refused` is its trusted-link refusal (never thrown on the
|
|
114
|
+
* browser login path, where better-auth's own linking gate answers first).
|
|
115
|
+
*/
|
|
116
|
+
export class HubSsoLoginError extends Error {
|
|
117
|
+
constructor(
|
|
118
|
+
readonly code:
|
|
119
|
+
| 'sso_assertion_missing'
|
|
120
|
+
| 'sso_identity_conflict'
|
|
121
|
+
| 'sso_email_conflict'
|
|
122
|
+
| 'sso_link_refused'
|
|
123
|
+
| 'sso_projection_failed'
|
|
124
|
+
| 'sso_login_failed',
|
|
125
|
+
message: string
|
|
126
|
+
) {
|
|
127
|
+
super(message);
|
|
128
|
+
this.name = 'HubSsoLoginError';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The assertion handoff (ADR 015, reshaped): the verified USER identity —
|
|
134
|
+
* plus the raw hub-audienced access token the login reconcile presents —
|
|
135
|
+
* exists in `getUserInfo` (PRE user-create); the per-login work runs in the
|
|
136
|
+
* callback's after-hook (POST create). An AsyncLocalStorage scope
|
|
137
|
+
* established around the whole auth request (api/index.ts wraps
|
|
138
|
+
* `auth.handler`) carries it across: getUserInfo writes, the after-hook
|
|
139
|
+
* takes. Being request-scoped, two concurrent logins can NEVER read each
|
|
140
|
+
* other's assertion or token.
|
|
141
|
+
*/
|
|
142
|
+
export interface LoginScopeAssertion extends HubSsoAssertion {
|
|
143
|
+
/** The callback access token (hub-audienced) — the login reconcile's bearer. */
|
|
144
|
+
accessToken: string;
|
|
145
|
+
accessTokenExpiresAt: Date | null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface LoginScope {
|
|
149
|
+
assertion?: LoginScopeAssertion;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const loginScope = new AsyncLocalStorage<LoginScope>();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The scopes the tool requests at hub sign-in — the ONE statement of the
|
|
156
|
+
* list (the provider entry below spreads it; tests import it).
|
|
157
|
+
*
|
|
158
|
+
* - `openid profile email`: the id_token identity.
|
|
159
|
+
* - `offline_access account:read`: the stored grant and its as-the-user
|
|
160
|
+
* `GET /orgs` reads (hub-grant.ts, hub-user-client.ts).
|
|
161
|
+
* - `orgs:create` (PRDCT-2443): the hub's DEDICATED scope for `POST /orgs`
|
|
162
|
+
* — it opens that one route, to a tool-registry client only.
|
|
163
|
+
* `account:write` does NOT open it and is deliberately not requested: a
|
|
164
|
+
* tool holds the narrowest write it needs, nothing account-wide.
|
|
165
|
+
*
|
|
166
|
+
* DEPLOY ORDER — THE HUB FIRST. The hub's authorize endpoint
|
|
167
|
+
* (@better-auth/oauth-provider) validates every requested scope against the
|
|
168
|
+
* CLIENT's registered scopes (its tool registry seeds them) and answers
|
|
169
|
+
* `error=invalid_scope` for an unknown one — the WHOLE sign-in fails, for
|
|
170
|
+
* every user, not just workspace creation. A hub that does not yet list
|
|
171
|
+
* `orgs:create` for this client must never meet a tool that requests
|
|
172
|
+
* it. A grant minted BEFORE this shipped simply lacks the scope: the hub
|
|
173
|
+
* answers 403 `insufficient_scope` on create, which the route maps to 401
|
|
174
|
+
* `hub_reauth_required` — one sign-in heals it.
|
|
175
|
+
*/
|
|
176
|
+
export const HUB_SSO_SCOPES = [
|
|
177
|
+
'openid',
|
|
178
|
+
'profile',
|
|
179
|
+
'email',
|
|
180
|
+
'offline_access',
|
|
181
|
+
'account:read',
|
|
182
|
+
'orgs:create'
|
|
183
|
+
] as const;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Strict whitelist for per-call authorize params (the SL-1 silent-connect
|
|
187
|
+
* seam): the dashboard's silent auto-connect passes
|
|
188
|
+
* `additionalData: { prompt: 'none' }` on `POST /sign-in/oauth2`, and the
|
|
189
|
+
* ONLY thing that may ever reach the hub's authorize URL from that
|
|
190
|
+
* caller-controlled blob is the literal `prompt=none` pair — anything else
|
|
191
|
+
* (other prompt values, extra keys) is dropped. Verified on better-auth
|
|
192
|
+
* 1.6.15 (re-verify on ANY bump): `authorizationUrlParams` may be a
|
|
193
|
+
* FUNCTION of the endpoint ctx (generic-oauth/types.d.mts), the sign-in
|
|
194
|
+
* body carries `additionalData` (generic-oauth/routes.mjs — also threaded
|
|
195
|
+
* into the state blob), and the function's return is applied to the
|
|
196
|
+
* authorization URL via `createAuthorizationURL`'s `additionalParams`.
|
|
197
|
+
*/
|
|
198
|
+
export function ssoAuthorizationUrlParams(body: unknown): Record<string, string> {
|
|
199
|
+
const additionalData = (body as { additionalData?: unknown } | null | undefined)?.additionalData;
|
|
200
|
+
const prompt = (additionalData as { prompt?: unknown } | null | undefined)?.prompt;
|
|
201
|
+
return prompt === 'none' ? { prompt: 'none' } : {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface HubSsoOptions {
|
|
205
|
+
db: Db;
|
|
206
|
+
logger: Logger;
|
|
207
|
+
/** The hub OIDC issuer (HUB_ISSUER_URL) — iss pin + discovery root. */
|
|
208
|
+
issuerUrl: string;
|
|
209
|
+
clientId: string;
|
|
210
|
+
clientSecret: string;
|
|
211
|
+
/**
|
|
212
|
+
* This instance's own OAuth resource identifier (`<PUBLIC_BASE_URL>/mcp`,
|
|
213
|
+
* always derived, never configured) — the aud pin for H3 CONNECT tokens
|
|
214
|
+
* (which the hub mints FOR this tool). Not used on the login path.
|
|
215
|
+
*/
|
|
216
|
+
resourceUrl: string;
|
|
217
|
+
/**
|
|
218
|
+
* RFC 8707 `resource` for the SSO CODE EXCHANGE — `<hub>/mcp`
|
|
219
|
+
* (hubApiResource), THE seam constant shared with hub-grant.ts: the
|
|
220
|
+
* callback access token and every refreshed token are HUB-audienced and
|
|
221
|
+
* callable at hub /api/v1. Null = omit the param (flips the whole flow
|
|
222
|
+
* to opaque tokens in one line if the hub ever accepts them).
|
|
223
|
+
*/
|
|
224
|
+
tokenResource: string | null;
|
|
225
|
+
/** Dashboard origin for the failure redirects (PUBLIC_BASE_URL). */
|
|
226
|
+
publicBaseUrl: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class HubSsoService {
|
|
230
|
+
private readonly verifier: HubJwtVerifier;
|
|
231
|
+
private reconciler: LoginReconciler | null = null;
|
|
232
|
+
|
|
233
|
+
constructor(private readonly opts: HubSsoOptions) {
|
|
234
|
+
this.verifier = new HubJwtVerifier(opts.issuerUrl);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Boot binds the reconciler once it exists (see LoginReconciler). */
|
|
238
|
+
bindReconciler(reconciler: LoginReconciler): void {
|
|
239
|
+
this.reconciler = reconciler;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Establish the per-request login scope. api/index.ts wraps EVERY
|
|
244
|
+
* /api/v1/auth/* request on cloud — an empty object store is free, and
|
|
245
|
+
* wrapping unconditionally keeps the seam independent of better-auth's
|
|
246
|
+
* internal routing.
|
|
247
|
+
*/
|
|
248
|
+
runWithLoginScope<T>(fn: () => T): T {
|
|
249
|
+
return loginScope.run({}, fn);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One-shot read of the assertion the current request's getUserInfo verified. */
|
|
253
|
+
takeAssertion(): LoginScopeAssertion | undefined {
|
|
254
|
+
const scope = loginScope.getStore();
|
|
255
|
+
const assertion = scope?.assertion;
|
|
256
|
+
if (scope) delete scope.assertion;
|
|
257
|
+
return assertion;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The account site's address (the hub issuer, no trailing slash): where a refusal sends the person. */
|
|
261
|
+
accountUrl(): string {
|
|
262
|
+
return this.opts.issuerUrl.replace(/\/+$/, '');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Where a failed SSO login lands — the login page with a stable error code. */
|
|
266
|
+
loginErrorUrl(code: string): string {
|
|
267
|
+
return `${this.opts.publicBaseUrl.replace(/\/+$/, '')}/login?error=${encodeURIComponent(code)}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logLoginFailure(err: unknown, localUserId: string): void {
|
|
271
|
+
this.opts.logger.warn(
|
|
272
|
+
{ err, localUserId },
|
|
273
|
+
'hub SSO login failed after callback — session revoked, redirecting with error'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The genericOAuth provider entry for the hub. Notes that are POSTURE,
|
|
279
|
+
* not accident:
|
|
280
|
+
*
|
|
281
|
+
* - `scopes` (HUB_SSO_SCOPES) carry `offline_access account:read`: the
|
|
282
|
+
* grant IS the between-logins credential — better-auth persists the
|
|
283
|
+
* refresh token on the account row (encrypted; identity/better-auth.ts),
|
|
284
|
+
* and hub-grant.ts keeps it alive — and `orgs:create`, the hub's
|
|
285
|
+
* DEDICATED scope for `POST /orgs` (PRDCT-2443).
|
|
286
|
+
* - `tokenUrlParams.resource` (RFC 8707) is `tokenResource` — `<hub>/mcp`
|
|
287
|
+
* — so the minted access token is a bearer for the HUB's own API (the
|
|
288
|
+
* login reconcile + every between-logins read), never for ours.
|
|
289
|
+
* - `disableSignUp` stays UNSET: SSO IS the sanctioned signup entrance
|
|
290
|
+
* on cloud — the deliberate FOURTH switch next to the three
|
|
291
|
+
* closed-signup switches (CLAUDE.md invariant, internal/federation.md).
|
|
292
|
+
* - `getUserInfo` replaces the plugin's default entirely, so IT owns the
|
|
293
|
+
* emailVerified mapping — set from the hub's verified `email_verified`
|
|
294
|
+
* claim, never defaulted.
|
|
295
|
+
*/
|
|
296
|
+
providerConfig(): GenericOAuthConfig {
|
|
297
|
+
return {
|
|
298
|
+
providerId: HUB_SSO_PROVIDER_ID,
|
|
299
|
+
discoveryUrl: this.opts.issuerUrl.replace(/\/+$/, '') + '/.well-known/openid-configuration',
|
|
300
|
+
clientId: this.opts.clientId,
|
|
301
|
+
clientSecret: this.opts.clientSecret,
|
|
302
|
+
scopes: [...HUB_SSO_SCOPES],
|
|
303
|
+
pkce: true,
|
|
304
|
+
...(this.opts.tokenResource ? { tokenUrlParams: { resource: this.opts.tokenResource } } : {}),
|
|
305
|
+
// Per-call prompt=none passthrough for the silent auto-connect — the
|
|
306
|
+
// strict whitelist above; nothing else from the caller's
|
|
307
|
+
// additionalData ever reaches the hub authorize URL.
|
|
308
|
+
authorizationUrlParams: (ctx) => ssoAuthorizationUrlParams(ctx.body),
|
|
309
|
+
getUserInfo: (tokens) => this.getUserInfo(tokens)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Verify the callback id_token and extract the USER assertion. Returning
|
|
315
|
+
* null makes the plugin fail the login with a clean error redirect — the
|
|
316
|
+
* fail-closed path for every bad token (wrong iss/aud, expired, bad
|
|
317
|
+
* signature, malformed claims). The access token is NOT verified here —
|
|
318
|
+
* it is hub-audienced, never claim-bearing for us; it rides the login
|
|
319
|
+
* scope as the reconcile's bearer, and the HUB verifies it on use.
|
|
320
|
+
*/
|
|
321
|
+
private async getUserInfo(tokens: {
|
|
322
|
+
accessToken?: string | undefined;
|
|
323
|
+
idToken?: string | undefined;
|
|
324
|
+
accessTokenExpiresAt?: Date | undefined;
|
|
325
|
+
}): Promise<{ id: string; email: string; name: string; emailVerified: boolean } | null> {
|
|
326
|
+
try {
|
|
327
|
+
if (!tokens.idToken) throw new Error('token response carried no id_token (openid is always scoped)');
|
|
328
|
+
if (!tokens.accessToken) throw new Error('token response carried no access token');
|
|
329
|
+
const idClaims = await this.verifier.verify(tokens.idToken, this.opts.clientId);
|
|
330
|
+
const assertion = buildUserAssertion(idClaims);
|
|
331
|
+
const scope = loginScope.getStore();
|
|
332
|
+
if (scope) {
|
|
333
|
+
scope.assertion = {
|
|
334
|
+
...assertion,
|
|
335
|
+
accessToken: tokens.accessToken,
|
|
336
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt ?? null
|
|
337
|
+
};
|
|
338
|
+
} else {
|
|
339
|
+
// No scope means the auth mount was not wrapped — the after-hook
|
|
340
|
+
// will fail this login closed (sso_assertion_missing). Say why.
|
|
341
|
+
this.opts.logger.error(
|
|
342
|
+
'hub SSO: getUserInfo ran outside a login scope — the callback after-hook will refuse this login'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
id: assertion.sub,
|
|
347
|
+
email: assertion.email,
|
|
348
|
+
name: assertion.name,
|
|
349
|
+
emailVerified: assertion.emailVerified
|
|
350
|
+
};
|
|
351
|
+
} catch (err) {
|
|
352
|
+
this.opts.logger.warn({ err }, 'hub SSO: callback token rejected');
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The per-login work (after-hook, newSession-gated, JIT and returning
|
|
359
|
+
* users alike), in fail-closed order:
|
|
360
|
+
*
|
|
361
|
+
* 1. single-hub-identity guard — undo an email-match link that would
|
|
362
|
+
* merge two hub subjects into one local user;
|
|
363
|
+
* 2. email re-sync (D10) with the explicit duplicate-email collision
|
|
364
|
+
* path (fail the login, corrupt nothing);
|
|
365
|
+
* 3. the FAIL-CLOSED login reconcile: one forced pass reading the user's
|
|
366
|
+
* org list from the hub with the callback access token — reconcile IS
|
|
367
|
+
* the projection now. A pass that does not definitively succeed
|
|
368
|
+
* throws `sso_projection_failed` (session revoked, error redirect):
|
|
369
|
+
* a cloud login without its projection must not exist.
|
|
370
|
+
*
|
|
371
|
+
* Throws HubSsoLoginError; the caller revokes the just-minted session and
|
|
372
|
+
* redirects. Any other throw is mapped to the generic failure code there.
|
|
373
|
+
*/
|
|
374
|
+
async assertLogin(localUserId: string, assertion: LoginScopeAssertion): Promise<void> {
|
|
375
|
+
await this.guardSingleHubIdentity(localUserId, assertion.sub);
|
|
376
|
+
await this.syncEmail(localUserId, assertion);
|
|
377
|
+
if (!this.reconciler) {
|
|
378
|
+
// Boot always binds on cloud; a missing binding is a wiring bug and
|
|
379
|
+
// must fail the login closed, never mint an unprojected session.
|
|
380
|
+
throw new HubSsoLoginError('sso_projection_failed', 'no login reconciler bound (boot wiring bug)');
|
|
381
|
+
}
|
|
382
|
+
const outcome = await this.reconciler.forceReconcile(localUserId, {
|
|
383
|
+
accessToken: assertion.accessToken,
|
|
384
|
+
expiresAt: assertion.accessTokenExpiresAt
|
|
385
|
+
});
|
|
386
|
+
if (outcome !== 'ok') {
|
|
387
|
+
throw new HubSsoLoginError('sso_projection_failed', `login reconcile pass failed (${outcome})`);
|
|
388
|
+
}
|
|
389
|
+
// SL-6: lazily mark the first login for the tool-local onboarding seam.
|
|
390
|
+
// AFTER the reconcile 'ok' (a revoked login must not leave a row) and
|
|
391
|
+
// strictly BEST-EFFORT: this write is NEVER a login failure mode — a
|
|
392
|
+
// failed insert just means the row appears on the NEXT login, and the
|
|
393
|
+
// banner semantics (`NOT EXISTS dismissed row` = welcome owed) already
|
|
394
|
+
// show the welcome without any row at all. ON CONFLICT DO NOTHING keeps
|
|
395
|
+
// returning logins from touching an existing (possibly dismissed) row.
|
|
396
|
+
try {
|
|
397
|
+
await this.opts.db.insert(userOnboarding).values({ userId: localUserId }).onConflictDoNothing();
|
|
398
|
+
} catch (err) {
|
|
399
|
+
this.opts.logger.warn(
|
|
400
|
+
{ err, localUserId },
|
|
401
|
+
'onboarding first-login insert failed — retry-safe (the welcome stays owed), login unaffected'
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Verify a hub-minted exchange JWT (`POST /sso/cli-connect`, the H3
|
|
408
|
+
* counterpart — internal/federation.md P5) and extract its assertion. The
|
|
409
|
+
* chain, every link fail-closed:
|
|
410
|
+
*
|
|
411
|
+
* 1. `HubJwtVerifier.verify` — hub JWKS signature, `iss` pinned to the
|
|
412
|
+
* hub, `aud` pinned to OUR resource URL (a token minted for another
|
|
413
|
+
* tool dies here; a tool-minted MCP token dies on `iss`),
|
|
414
|
+
* RS256 allowlist, expiry (5 s tolerance).
|
|
415
|
+
* 2. `exp` REQUIRED — jose only validates expiry when the claim exists,
|
|
416
|
+
* and the replay ledger needs a bound; a token without one is a
|
|
417
|
+
* contract break, refused.
|
|
418
|
+
* 3. `purpose === 'sso-connect'` REQUIRED — a hub ACCESS token carries
|
|
419
|
+
* no purpose claim and dies here even if iss/aud ever matched.
|
|
420
|
+
* 4. `jti` REQUIRED (bounded) — the caller consumes it one-time-use.
|
|
421
|
+
*
|
|
422
|
+
* The token's TRANSITIONAL org claims (workspace_id/role/workspace_name,
|
|
423
|
+
* kept by the hub through the compat window) are deliberately NOT read —
|
|
424
|
+
* org truth comes from the connect-time reconcile, never a claim.
|
|
425
|
+
*
|
|
426
|
+
* Throws on any failure; the route maps every throw to one uniform 401
|
|
427
|
+
* (no oracle distinguishing replay from expiry from a foreign audience).
|
|
428
|
+
*/
|
|
429
|
+
async verifyConnectToken(token: string): Promise<HubConnectAssertion> {
|
|
430
|
+
const claims = await this.verifier.verify(token, this.opts.resourceUrl);
|
|
431
|
+
if (typeof claims.exp !== 'number') throw new Error('exchange token carries no exp');
|
|
432
|
+
if (claims.purpose !== 'sso-connect') {
|
|
433
|
+
throw new Error('token is not an sso-connect exchange token (purpose claim)');
|
|
434
|
+
}
|
|
435
|
+
const jti = claims.jti;
|
|
436
|
+
if (typeof jti !== 'string' || !jti || jti.length > 256) {
|
|
437
|
+
throw new Error('exchange token carries no usable jti');
|
|
438
|
+
}
|
|
439
|
+
const sub = claims.sub;
|
|
440
|
+
if (typeof sub !== 'string' || !sub) throw new Error('exchange token carries no sub');
|
|
441
|
+
const email = claims.email;
|
|
442
|
+
if (typeof email !== 'string' || !email.includes('@')) {
|
|
443
|
+
throw new Error('exchange token carries no usable email claim');
|
|
444
|
+
}
|
|
445
|
+
const normalizedEmail = email.toLowerCase();
|
|
446
|
+
return {
|
|
447
|
+
sub,
|
|
448
|
+
email: normalizedEmail,
|
|
449
|
+
// The hub mints exchange tokens only for a live, authenticated hub
|
|
450
|
+
// user — an identity whose email the hub itself verified (H3 re-reads
|
|
451
|
+
// emailVerified LIVE at mint). The H3 contract carries no
|
|
452
|
+
// email_verified or name claim, so the honest mirror of the SSO
|
|
453
|
+
// id_token's `email_verified: true` is asserted here, and the display
|
|
454
|
+
// name falls back exactly like an SSO login with a blank profile name.
|
|
455
|
+
emailVerified: true,
|
|
456
|
+
name: normalizedEmail.split('@')[0]!,
|
|
457
|
+
jti,
|
|
458
|
+
expiresAt: new Date(claims.exp * 1000)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* JIT-provision for `/sso/cli-connect`: resolve-or-create the local user
|
|
464
|
+
* for a VERIFIED connect assertion, then run the USER half of the
|
|
465
|
+
* per-login work — identity guard, D10 email sync. NO org projection
|
|
466
|
+
* happens here: the route stores the H3 grant (`acquireFromConnect`) and
|
|
467
|
+
* runs the same fail-closed reconcile as a browser login — the reconcile
|
|
468
|
+
* IS the projection, on the connect path too. User resolution mirrors
|
|
469
|
+
* better-auth's own OAuth linking exactly (oauth2/link-account.mjs on the
|
|
470
|
+
* pinned 1.6.15):
|
|
471
|
+
*
|
|
472
|
+
* 1. account row (providerId='antasphere', accountId=sub) → that user;
|
|
473
|
+
* 2. else a local user holding the asserted email → TRUSTED LINK (D9),
|
|
474
|
+
* but only onto a VERIFIED local email (requireLocalEmailVerified —
|
|
475
|
+
* an attacker-parked unverified account can never be taken over);
|
|
476
|
+
* 3. else create user + account through the internal adapter's
|
|
477
|
+
* `createOAuthUser` — the genericOAuth JIT call, so the
|
|
478
|
+
* databaseHooks.user.create.after seam (`user.created` → collaborator
|
|
479
|
+
* grant sweep) fires exactly as it would for a browser SSO signup.
|
|
480
|
+
*
|
|
481
|
+
* Two concurrent first-connects race the create; the loser's unique-email
|
|
482
|
+
* violation is caught and resolved by re-running the lookup (the Phase 5
|
|
483
|
+
* duplicate-account lesson).
|
|
484
|
+
*/
|
|
485
|
+
async provisionConnect(
|
|
486
|
+
auth: ConnectAuthSeam,
|
|
487
|
+
assertion: HubConnectAssertion
|
|
488
|
+
): Promise<{ user: { id: string; email: string; name: string } }> {
|
|
489
|
+
let userId = await this.resolveConnectUser(auth, assertion, true);
|
|
490
|
+
if (userId === null) {
|
|
491
|
+
// Lost the create race: the winner's rows are committed now (the
|
|
492
|
+
// adapter's transaction passthrough) — one retry must resolve.
|
|
493
|
+
userId = await this.resolveConnectUser(auth, assertion, false);
|
|
494
|
+
if (userId === null) {
|
|
495
|
+
throw new HubSsoLoginError('sso_login_failed', 'user provisioning raced and re-lookup failed');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
await this.guardSingleHubIdentity(userId, assertion.sub);
|
|
499
|
+
await this.syncEmail(userId, assertion);
|
|
500
|
+
// Re-read AFTER the D10 sync may have just rewritten the email; the
|
|
501
|
+
// response must carry what the database now holds.
|
|
502
|
+
const [row] = await this.opts.db
|
|
503
|
+
.select({ id: userTable.id, email: userTable.email, name: userTable.name })
|
|
504
|
+
.from(userTable)
|
|
505
|
+
.where(eq(userTable.id, userId))
|
|
506
|
+
.limit(1);
|
|
507
|
+
if (!row) throw new HubSsoLoginError('sso_login_failed', 'user row missing after provisioning');
|
|
508
|
+
return { user: row };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The connect path's fail-closed projection pass: one forced reconcile
|
|
513
|
+
* reading the user's org list AS THE USER with their freshly stored (or
|
|
514
|
+
* pre-existing) grant. Returns the pass's own outcome — the route mints a
|
|
515
|
+
* key ONLY on 'ok' (never a born-dead key).
|
|
516
|
+
*/
|
|
517
|
+
async reconcileConnect(localUserId: string): Promise<ReconcilePassOutcome> {
|
|
518
|
+
if (!this.reconciler) {
|
|
519
|
+
// Boot always binds on cloud; a missing binding is a wiring bug and
|
|
520
|
+
// must refuse the connect, never mint an unprojected key.
|
|
521
|
+
throw new HubSsoLoginError('sso_projection_failed', 'no login reconciler bound (boot wiring bug)');
|
|
522
|
+
}
|
|
523
|
+
return this.reconciler.forceReconcile(localUserId);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* One resolution pass (see provisionConnect). Returns the local user id,
|
|
528
|
+
* or null when `mayCreate` and the create lost a uniqueness race —
|
|
529
|
+
* signalling the caller to re-run with the winner's rows visible.
|
|
530
|
+
*/
|
|
531
|
+
private async resolveConnectUser(
|
|
532
|
+
auth: ConnectAuthSeam,
|
|
533
|
+
assertion: HubConnectAssertion,
|
|
534
|
+
mayCreate: boolean
|
|
535
|
+
): Promise<string | null> {
|
|
536
|
+
const db = this.opts.db;
|
|
537
|
+
const [linked] = await db
|
|
538
|
+
.select({ userId: account.userId })
|
|
539
|
+
.from(account)
|
|
540
|
+
.where(and(eq(account.providerId, HUB_SSO_PROVIDER_ID), eq(account.accountId, assertion.sub)))
|
|
541
|
+
.limit(1);
|
|
542
|
+
if (linked) return linked.userId;
|
|
543
|
+
|
|
544
|
+
const [existing] = await db
|
|
545
|
+
.select({ id: userTable.id, emailVerified: userTable.emailVerified })
|
|
546
|
+
.from(userTable)
|
|
547
|
+
.where(eq(userTable.email, assertion.email))
|
|
548
|
+
.limit(1);
|
|
549
|
+
const authCtx = await auth.$context;
|
|
550
|
+
if (existing) {
|
|
551
|
+
if (!existing.emailVerified) {
|
|
552
|
+
// requireLocalEmailVerified, mirrored: linking a trusted provider
|
|
553
|
+
// onto an UNVERIFIED local account is the classic pre-registration
|
|
554
|
+
// takeover — refuse, exactly like the browser SSO path does.
|
|
555
|
+
throw new HubSsoLoginError(
|
|
556
|
+
'sso_link_refused',
|
|
557
|
+
'a local account holds this email but its address is unverified — refusing the trusted link'
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
await authCtx.internalAdapter.linkAccount({
|
|
561
|
+
providerId: HUB_SSO_PROVIDER_ID,
|
|
562
|
+
accountId: assertion.sub,
|
|
563
|
+
userId: existing.id
|
|
564
|
+
});
|
|
565
|
+
return existing.id;
|
|
566
|
+
}
|
|
567
|
+
if (!mayCreate) return null;
|
|
568
|
+
try {
|
|
569
|
+
const created = await authCtx.internalAdapter.createOAuthUser(
|
|
570
|
+
{ email: assertion.email, name: assertion.name, emailVerified: assertion.emailVerified },
|
|
571
|
+
{ providerId: HUB_SSO_PROVIDER_ID, accountId: assertion.sub }
|
|
572
|
+
);
|
|
573
|
+
return created.user.id;
|
|
574
|
+
} catch (err) {
|
|
575
|
+
// The only expected failure is the unique-email (or duplicate-link)
|
|
576
|
+
// violation from a concurrent first-connect — resolvable by retry.
|
|
577
|
+
this.opts.logger.warn({ err }, 'sso cli-connect: JIT create raced — retrying resolution');
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A local user may hold AT MOST ONE hub identity. The dangerous path:
|
|
584
|
+
* hub user B's email was changed (hub-side, verified there) to an address
|
|
585
|
+
* some local user A still carries because A's projection is stale — B's
|
|
586
|
+
* first login then finds no account row, matches A by email, and the D9
|
|
587
|
+
* trusted link merges B onto A's local user (and A's resources). Detect the
|
|
588
|
+
* second `antasphere` accountId, DELETE the NEWEST link row, and fail the
|
|
589
|
+
* login. The conflict clears when A next logs in (their email re-syncs
|
|
590
|
+
* away) or an operator intervenes.
|
|
591
|
+
*
|
|
592
|
+
* The undo keys on RECENCY, not on the current login's `sub`. On the
|
|
593
|
+
* normal conflict they are the same row (the trusted link just inserted
|
|
594
|
+
* it). They differ only in residue states where TWO rows pre-exist — a
|
|
595
|
+
* crash between better-auth's link and this hook, or a second identity
|
|
596
|
+
* linked via the explicit /oauth2/link flow (which mints no session, so
|
|
597
|
+
* this guard never saw it). Deleting the current-sub row there would let
|
|
598
|
+
* the ESTABLISHED identity's own login destroy itself (and two
|
|
599
|
+
* concurrent conflicting logins destroy BOTH rows, stranding the user);
|
|
600
|
+
* the newest row is always the intruding link, never the long-standing
|
|
601
|
+
* identity, so the user self-heals on retry.
|
|
602
|
+
*/
|
|
603
|
+
private async guardSingleHubIdentity(localUserId: string, sub: string): Promise<void> {
|
|
604
|
+
const rows = await this.opts.db
|
|
605
|
+
.select({ accountId: account.accountId, createdAt: account.createdAt })
|
|
606
|
+
.from(account)
|
|
607
|
+
.where(and(eq(account.userId, localUserId), eq(account.providerId, HUB_SSO_PROVIDER_ID)));
|
|
608
|
+
if (!rows.some((r) => r.accountId !== sub)) return;
|
|
609
|
+
const newest = [...rows].sort(
|
|
610
|
+
(a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.accountId.localeCompare(a.accountId)
|
|
611
|
+
)[0]!;
|
|
612
|
+
await this.opts.db
|
|
613
|
+
.delete(account)
|
|
614
|
+
.where(
|
|
615
|
+
and(
|
|
616
|
+
eq(account.userId, localUserId),
|
|
617
|
+
eq(account.providerId, HUB_SSO_PROVIDER_ID),
|
|
618
|
+
eq(account.accountId, newest.accountId)
|
|
619
|
+
)
|
|
620
|
+
);
|
|
621
|
+
throw new HubSsoLoginError(
|
|
622
|
+
'sso_identity_conflict',
|
|
623
|
+
`hub subject ${sub} maps to a local user already linked to a different hub subject — newest link undone`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* D10: the hub is the identity source on cloud, so the local email FOLLOWS
|
|
629
|
+
* the (verified) hub email at every login — email-keyed features (the
|
|
630
|
+
* collaborator grant sweep) must see the current address. A collision with
|
|
631
|
+
* another local user's email fails the login cleanly; the unique
|
|
632
|
+
* constraint on user.email backstops the read-then-write race.
|
|
633
|
+
*/
|
|
634
|
+
private async syncEmail(localUserId: string, assertion: HubSsoAssertion): Promise<void> {
|
|
635
|
+
const email = assertion.email.toLowerCase();
|
|
636
|
+
const [row] = await this.opts.db
|
|
637
|
+
.select({ email: userTable.email, emailVerified: userTable.emailVerified })
|
|
638
|
+
.from(userTable)
|
|
639
|
+
.where(eq(userTable.id, localUserId))
|
|
640
|
+
.limit(1);
|
|
641
|
+
if (!row) throw new HubSsoLoginError('sso_login_failed', 'user row missing after callback');
|
|
642
|
+
const emailChanged = row.email !== email;
|
|
643
|
+
// For an UNCHANGED address, emailVerified is a LATCH: once this instance
|
|
644
|
+
// holds the address as verified, a weaker hub assertion never flips it
|
|
645
|
+
// back — break-glass (api/break-glass.ts) refuses unverified users, so a
|
|
646
|
+
// downward sync could close the operator door if the hub's posture ever
|
|
647
|
+
// allowed an unverified login. A CHANGED address takes the hub's
|
|
648
|
+
// asserted state honestly (the new mailbox is unproven). Mirrors
|
|
649
|
+
// better-auth's own overrideUserInfo semantics.
|
|
650
|
+
const emailVerified = emailChanged
|
|
651
|
+
? assertion.emailVerified
|
|
652
|
+
: row.emailVerified || assertion.emailVerified;
|
|
653
|
+
if (!emailChanged && row.emailVerified === emailVerified) return;
|
|
654
|
+
if (emailChanged) {
|
|
655
|
+
const [holder] = await this.opts.db
|
|
656
|
+
.select({ id: userTable.id })
|
|
657
|
+
.from(userTable)
|
|
658
|
+
.where(and(eq(userTable.email, email), ne(userTable.id, localUserId)))
|
|
659
|
+
.limit(1);
|
|
660
|
+
if (holder) {
|
|
661
|
+
throw new HubSsoLoginError(
|
|
662
|
+
'sso_email_conflict',
|
|
663
|
+
`hub-asserted email is already held by another local user — refusing to sync`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
await this.opts.db
|
|
669
|
+
.update(userTable)
|
|
670
|
+
.set({ email, emailVerified, updatedAt: new Date() })
|
|
671
|
+
.where(eq(userTable.id, localUserId));
|
|
672
|
+
} catch {
|
|
673
|
+
// Concurrent claim of the same address between check and write.
|
|
674
|
+
throw new HubSsoLoginError('sso_email_conflict', 'email sync lost a uniqueness race');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** The SSO-login user assertion, from the VERIFIED id_token ONLY. */
|
|
680
|
+
function buildUserAssertion(idClaims: JWTPayload): HubSsoAssertion {
|
|
681
|
+
const sub = idClaims.sub;
|
|
682
|
+
if (typeof sub !== 'string' || !sub) throw new Error('id_token carries no sub');
|
|
683
|
+
const email = idClaims.email;
|
|
684
|
+
if (typeof email !== 'string' || !email.includes('@')) {
|
|
685
|
+
throw new Error('id_token carries no usable email claim');
|
|
686
|
+
}
|
|
687
|
+
const normalizedEmail = email.toLowerCase();
|
|
688
|
+
const rawName = idClaims.name;
|
|
689
|
+
const name = typeof rawName === 'string' && rawName.trim() ? rawName : normalizedEmail.split('@')[0]!;
|
|
690
|
+
return {
|
|
691
|
+
sub,
|
|
692
|
+
email: normalizedEmail,
|
|
693
|
+
emailVerified: idClaims.email_verified === true,
|
|
694
|
+
name
|
|
695
|
+
};
|
|
696
|
+
}
|