@lastshotlabs/bunshot 0.0.27 → 0.0.28
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/.oclif.manifest.json +39 -0
- package/README.md +8282 -2147
- package/dist/cli/commands/init.js +690 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli.js +4 -4
- package/dist/packages/bunshot-admin/src/index.d.ts +15 -0
- package/dist/packages/bunshot-admin/src/index.js +11 -0
- package/dist/packages/bunshot-admin/src/lib/resourceTypes.d.ts +8 -0
- package/dist/packages/bunshot-admin/src/lib/resourceTypes.js +33 -0
- package/dist/packages/bunshot-admin/src/lib/typedRoute.d.ts +14 -0
- package/dist/packages/bunshot-admin/src/lib/typedRoute.js +17 -0
- package/dist/packages/bunshot-admin/src/plugin.d.ts +4 -0
- package/dist/packages/bunshot-admin/src/plugin.js +46 -0
- package/dist/packages/bunshot-admin/src/providers/auth0Access.d.ts +6 -0
- package/dist/packages/bunshot-admin/src/providers/auth0Access.js +32 -0
- package/dist/packages/bunshot-admin/src/routes/admin.d.ts +10 -0
- package/dist/packages/bunshot-admin/src/routes/admin.js +923 -0
- package/dist/packages/bunshot-admin/src/routes/mail.d.ts +6 -0
- package/dist/packages/bunshot-admin/src/routes/mail.js +114 -0
- package/dist/packages/bunshot-admin/src/routes/permissions.d.ts +8 -0
- package/dist/packages/bunshot-admin/src/routes/permissions.js +315 -0
- package/dist/packages/bunshot-admin/src/types/config.d.ts +16 -0
- package/dist/packages/bunshot-admin/src/types/config.js +37 -0
- package/dist/packages/bunshot-admin/src/types/env.d.ts +14 -0
- package/dist/packages/bunshot-admin/src/types/provider.d.ts +1 -0
- package/dist/packages/bunshot-admin/src/types/provider.js +4 -0
- package/dist/packages/bunshot-auth/src/adapters/memoryAuth.d.ts +66 -0
- package/dist/packages/bunshot-auth/src/adapters/memoryAuth.js +1063 -0
- package/dist/packages/bunshot-auth/src/adapters/mongoAuth.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/adapters/mongoAuth.js +536 -0
- package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.d.ts +88 -0
- package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.js +1366 -0
- package/dist/packages/bunshot-auth/src/admin/bunshotAccess.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/admin/bunshotAccess.js +23 -0
- package/dist/packages/bunshot-auth/src/admin/bunshotUsers.d.ts +5 -0
- package/dist/packages/bunshot-auth/src/admin/bunshotUsers.js +131 -0
- package/dist/packages/bunshot-auth/src/bootstrap.d.ts +38 -0
- package/dist/packages/bunshot-auth/src/bootstrap.js +384 -0
- package/dist/packages/bunshot-auth/src/config/appConfig.d.ts +3 -0
- package/dist/packages/bunshot-auth/src/config/appConfig.js +4 -0
- package/dist/packages/bunshot-auth/src/config/authConfig.d.ts +478 -0
- package/dist/packages/bunshot-auth/src/config/authConfig.js +46 -0
- package/dist/packages/bunshot-auth/src/config/configLock.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/config/configLock.js +10 -0
- package/dist/packages/bunshot-auth/src/index.d.ts +25 -0
- package/dist/packages/bunshot-auth/src/index.js +23 -0
- package/dist/packages/bunshot-auth/src/infra/mongo.d.ts +15 -0
- package/dist/packages/bunshot-auth/src/infra/mongo.js +44 -0
- package/dist/packages/bunshot-auth/src/infra/queue.d.ts +14 -0
- package/dist/packages/bunshot-auth/src/infra/queue.js +27 -0
- package/dist/packages/bunshot-auth/src/infra/redis.d.ts +5 -0
- package/dist/packages/bunshot-auth/src/infra/redis.js +15 -0
- package/dist/packages/bunshot-auth/src/infra/signing.d.ts +7 -0
- package/dist/packages/bunshot-auth/src/infra/signing.js +8 -0
- package/dist/packages/bunshot-auth/src/lib/accountLockout.d.ts +34 -0
- package/dist/packages/bunshot-auth/src/lib/accountLockout.js +244 -0
- package/dist/packages/bunshot-auth/src/lib/adapterTiers.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/lib/adapterTiers.js +1 -0
- package/dist/packages/bunshot-auth/src/lib/authAdapter.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/lib/authAdapter.js +1 -0
- package/dist/packages/bunshot-auth/src/lib/authContext.d.ts +15 -0
- package/dist/packages/bunshot-auth/src/lib/authContext.js +1 -0
- package/dist/packages/bunshot-auth/src/lib/authEventBus.d.ts +4 -0
- package/dist/packages/bunshot-auth/src/lib/authEventBus.js +15 -0
- package/dist/packages/bunshot-auth/src/lib/authRateLimit.d.ts +28 -0
- package/dist/packages/bunshot-auth/src/lib/authRateLimit.js +205 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.d.ts +8 -2
- package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.js +22 -9
- package/dist/packages/bunshot-auth/src/lib/cache.d.ts +12 -0
- package/dist/packages/bunshot-auth/src/lib/cache.js +120 -0
- package/dist/packages/bunshot-auth/src/lib/clientIp.d.ts +4 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/clientIp.js +14 -7
- package/dist/packages/bunshot-auth/src/lib/cookieOptions.d.ts +27 -0
- package/dist/packages/bunshot-auth/src/lib/cookieOptions.js +33 -0
- package/dist/packages/bunshot-auth/src/lib/credentialStuffing.d.ts +40 -0
- package/dist/packages/bunshot-auth/src/lib/credentialStuffing.js +221 -0
- package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.d.ts +19 -0
- package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.js +148 -0
- package/dist/packages/bunshot-auth/src/lib/emailTemplates.d.ts +23 -0
- package/dist/packages/bunshot-auth/src/lib/emailTemplates.js +265 -0
- package/dist/packages/bunshot-auth/src/lib/emailVerification.d.ts +30 -0
- package/dist/packages/bunshot-auth/src/lib/emailVerification.js +200 -0
- package/dist/packages/bunshot-auth/src/lib/env.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/lib/env.js +3 -0
- package/dist/packages/bunshot-auth/src/lib/fingerprint.js +36 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/groups.d.ts +15 -16
- package/dist/{lib → packages/bunshot-auth/src/lib}/groups.js +22 -34
- package/dist/packages/bunshot-auth/src/lib/jwks.d.ts +28 -0
- package/dist/packages/bunshot-auth/src/lib/jwks.js +79 -0
- package/dist/packages/bunshot-auth/src/lib/jwt.d.ts +12 -0
- package/dist/packages/bunshot-auth/src/lib/jwt.js +86 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/logger.js +3 -3
- package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.d.ts +5 -4
- package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.js +6 -10
- package/dist/packages/bunshot-auth/src/lib/magicLink.d.ts +13 -0
- package/dist/packages/bunshot-auth/src/lib/magicLink.js +145 -0
- package/dist/packages/bunshot-auth/src/lib/mfaChallenge.d.ts +60 -0
- package/dist/packages/bunshot-auth/src/lib/mfaChallenge.js +419 -0
- package/dist/packages/bunshot-auth/src/lib/oauth.d.ts +82 -0
- package/dist/packages/bunshot-auth/src/lib/oauth.js +177 -0
- package/dist/packages/bunshot-auth/src/lib/oauthCode.d.ts +19 -0
- package/dist/packages/bunshot-auth/src/lib/oauthCode.js +182 -0
- package/dist/packages/bunshot-auth/src/lib/oauthReauth.d.ts +19 -0
- package/dist/packages/bunshot-auth/src/lib/oauthReauth.js +255 -0
- package/dist/packages/bunshot-auth/src/lib/organization.d.ts +66 -0
- package/dist/packages/bunshot-auth/src/lib/organization.js +225 -0
- package/dist/packages/bunshot-auth/src/lib/passwordHistory.d.ts +12 -0
- package/dist/packages/bunshot-auth/src/lib/passwordHistory.js +31 -0
- package/dist/packages/bunshot-auth/src/lib/resetPassword.d.ts +20 -0
- package/dist/packages/bunshot-auth/src/lib/resetPassword.js +148 -0
- package/dist/packages/bunshot-auth/src/lib/roles.d.ts +9 -0
- package/dist/packages/bunshot-auth/src/lib/roles.js +93 -0
- package/dist/packages/bunshot-auth/src/lib/saml.d.ts +29 -0
- package/dist/packages/bunshot-auth/src/lib/saml.js +73 -0
- package/dist/packages/bunshot-auth/src/lib/samlRequestId.d.ts +13 -0
- package/dist/packages/bunshot-auth/src/lib/samlRequestId.js +129 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/scim.d.ts +7 -7
- package/dist/{lib → packages/bunshot-auth/src/lib}/scim.js +15 -13
- package/dist/packages/bunshot-auth/src/lib/securityEventWiring.d.ts +22 -0
- package/dist/packages/bunshot-auth/src/lib/securityEventWiring.js +65 -0
- package/dist/packages/bunshot-auth/src/lib/session.d.ts +45 -0
- package/dist/packages/bunshot-auth/src/lib/session.js +1211 -0
- package/dist/packages/bunshot-auth/src/lib/storeInfra.d.ts +26 -0
- package/dist/packages/bunshot-auth/src/lib/storeInfra.js +18 -0
- package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.d.ts +3 -2
- package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.js +2 -5
- package/dist/packages/bunshot-auth/src/lib/validateAdapter.d.ts +16 -0
- package/dist/packages/bunshot-auth/src/lib/validateAdapter.js +161 -0
- package/dist/packages/bunshot-auth/src/middleware/bearerAuth.d.ts +13 -0
- package/dist/packages/bunshot-auth/src/middleware/bearerAuth.js +58 -0
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/csrf.d.ts +5 -4
- package/dist/packages/bunshot-auth/src/middleware/csrf.js +138 -0
- package/dist/packages/bunshot-auth/src/middleware/identify.d.ts +4 -0
- package/dist/packages/bunshot-auth/src/middleware/identify.js +124 -0
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.d.ts +2 -2
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.js +10 -8
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.d.ts +2 -2
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.js +20 -16
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.d.ts +2 -2
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.js +6 -6
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.d.ts +2 -2
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.js +8 -7
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.d.ts +2 -2
- package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.js +7 -6
- package/dist/packages/bunshot-auth/src/middleware/scimAuth.d.ts +8 -0
- package/dist/packages/bunshot-auth/src/middleware/scimAuth.js +29 -0
- package/dist/packages/bunshot-auth/src/middleware/userAuth.d.ts +3 -0
- package/dist/packages/bunshot-auth/src/middleware/userAuth.js +6 -0
- package/dist/{models → packages/bunshot-auth/src/models}/AuthUser.d.ts +12 -8
- package/dist/packages/bunshot-auth/src/models/AuthUser.js +53 -0
- package/dist/packages/bunshot-auth/src/models/Group.d.ts +19 -0
- package/dist/packages/bunshot-auth/src/models/Group.js +22 -0
- package/dist/{models → packages/bunshot-auth/src/models}/GroupMembership.d.ts +6 -8
- package/dist/packages/bunshot-auth/src/models/GroupMembership.js +19 -0
- package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.d.ts +1 -1
- package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.js +5 -5
- package/dist/packages/bunshot-auth/src/models/TenantRole.d.ts +13 -0
- package/dist/packages/bunshot-auth/src/models/TenantRole.js +17 -0
- package/dist/packages/bunshot-auth/src/plugin.d.ts +4 -0
- package/dist/packages/bunshot-auth/src/plugin.js +274 -0
- package/dist/packages/bunshot-auth/src/routes/auth.d.ts +15 -0
- package/dist/packages/bunshot-auth/src/routes/auth.js +1624 -0
- package/dist/packages/bunshot-auth/src/routes/groups.d.ts +4 -0
- package/dist/packages/bunshot-auth/src/routes/groups.js +481 -0
- package/dist/packages/bunshot-auth/src/routes/m2m.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/routes/m2m.js +145 -0
- package/dist/packages/bunshot-auth/src/routes/mfa.d.ts +6 -0
- package/dist/packages/bunshot-auth/src/routes/mfa.js +991 -0
- package/dist/packages/bunshot-auth/src/routes/oauth.d.ts +3 -0
- package/dist/packages/bunshot-auth/src/routes/oauth.js +1727 -0
- package/dist/packages/bunshot-auth/src/routes/oidc.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/routes/oidc.js +84 -0
- package/dist/packages/bunshot-auth/src/routes/organizations.d.ts +3 -0
- package/dist/packages/bunshot-auth/src/routes/organizations.js +741 -0
- package/dist/packages/bunshot-auth/src/routes/passkey.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/routes/passkey.js +199 -0
- package/dist/packages/bunshot-auth/src/routes/saml.d.ts +2 -0
- package/dist/packages/bunshot-auth/src/routes/saml.js +226 -0
- package/dist/packages/bunshot-auth/src/routes/scim.d.ts +3 -0
- package/dist/packages/bunshot-auth/src/routes/scim.js +588 -0
- package/dist/packages/bunshot-auth/src/runtime.d.ts +52 -0
- package/dist/packages/bunshot-auth/src/runtime.js +11 -0
- package/dist/{schemas → packages/bunshot-auth/src/schemas}/auth.d.ts +4 -5
- package/dist/packages/bunshot-auth/src/schemas/auth.js +24 -0
- package/dist/packages/bunshot-auth/src/schemas/error.d.ts +10 -0
- package/dist/packages/bunshot-auth/src/schemas/error.js +10 -0
- package/dist/packages/bunshot-auth/src/schemas/success.d.ts +10 -0
- package/dist/packages/bunshot-auth/src/schemas/success.js +10 -0
- package/dist/packages/bunshot-auth/src/services/auth.d.ts +39 -0
- package/dist/packages/bunshot-auth/src/services/auth.js +378 -0
- package/dist/{services → packages/bunshot-auth/src/services}/mfa.d.ts +41 -17
- package/dist/{services → packages/bunshot-auth/src/services}/mfa.js +259 -183
- package/dist/packages/bunshot-auth/src/testing.d.ts +31 -0
- package/dist/packages/bunshot-auth/src/testing.js +23 -0
- package/dist/packages/bunshot-auth/src/types/adapter.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/types/adapter.js +1 -0
- package/dist/packages/bunshot-auth/src/types/config.d.ts +152 -0
- package/dist/packages/bunshot-auth/src/types/config.js +179 -0
- package/dist/{routes → packages/bunshot-auth/src/types}/groups.d.ts +2 -3
- package/dist/packages/bunshot-auth/src/types/groups.js +1 -0
- package/dist/packages/bunshot-auth/src/types/oauthCode.d.ts +6 -0
- package/dist/packages/bunshot-auth/src/types/oauthCode.js +1 -0
- package/dist/packages/bunshot-auth/src/types/oauthReauth.d.ts +13 -0
- package/dist/packages/bunshot-auth/src/types/oauthReauth.js +1 -0
- package/dist/packages/bunshot-auth/src/types/redis.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/types/redis.js +1 -0
- package/dist/packages/bunshot-auth/src/types/saml.d.ts +10 -0
- package/dist/packages/bunshot-auth/src/types/saml.js +1 -0
- package/dist/packages/bunshot-auth/src/types/session.d.ts +18 -0
- package/dist/packages/bunshot-auth/src/types/session.js +1 -0
- package/dist/packages/bunshot-auth/src/types/store.d.ts +1 -0
- package/dist/packages/bunshot-auth/src/types/store.js +1 -0
- package/dist/packages/bunshot-core/src/adminProvider.d.ts +95 -0
- package/dist/packages/bunshot-core/src/adminProvider.js +1 -0
- package/dist/packages/bunshot-core/src/auditLog.d.ts +34 -0
- package/dist/packages/bunshot-core/src/auditLog.js +1 -0
- package/dist/packages/bunshot-core/src/auth-adapter.d.ts +227 -0
- package/dist/packages/bunshot-core/src/auth-adapter.js +4 -0
- package/dist/packages/bunshot-core/src/authVariables.d.ts +14 -0
- package/dist/packages/bunshot-core/src/authVariables.js +4 -0
- package/dist/packages/bunshot-core/src/cache.d.ts +12 -0
- package/dist/packages/bunshot-core/src/cache.js +21 -0
- package/dist/{lib → packages/bunshot-core/src}/captcha.d.ts +1 -10
- package/dist/packages/bunshot-core/src/captcha.js +1 -0
- package/dist/packages/bunshot-core/src/clearRegistry.d.ts +6 -0
- package/dist/packages/bunshot-core/src/clearRegistry.js +17 -0
- package/dist/packages/bunshot-core/src/clientIp.d.ts +3 -0
- package/dist/packages/bunshot-core/src/clientIp.js +45 -0
- package/dist/packages/bunshot-core/src/configLock.d.ts +4 -0
- package/dist/packages/bunshot-core/src/configLock.js +7 -0
- package/dist/packages/bunshot-core/src/configValidation.d.ts +22 -0
- package/dist/packages/bunshot-core/src/configValidation.js +39 -0
- package/dist/packages/bunshot-core/src/constants.js +10 -0
- package/dist/packages/bunshot-core/src/context/bunshotContext.d.ts +232 -0
- package/dist/packages/bunshot-core/src/context/bunshotContext.js +1 -0
- package/dist/packages/bunshot-core/src/context/contextAccess.d.ts +3 -0
- package/dist/packages/bunshot-core/src/context/contextAccess.js +16 -0
- package/dist/packages/bunshot-core/src/context/contextStore.d.ts +16 -0
- package/dist/packages/bunshot-core/src/context/contextStore.js +31 -0
- package/dist/packages/bunshot-core/src/context/frameworkConfig.d.ts +38 -0
- package/dist/packages/bunshot-core/src/context/frameworkConfig.js +1 -0
- package/dist/packages/bunshot-core/src/context/index.d.ts +4 -0
- package/dist/packages/bunshot-core/src/context/index.js +2 -0
- package/dist/packages/bunshot-core/src/context.d.ts +40 -0
- package/dist/packages/bunshot-core/src/context.js +35 -0
- package/dist/packages/bunshot-core/src/coreContracts.d.ts +47 -0
- package/dist/packages/bunshot-core/src/coreContracts.js +1 -0
- package/dist/packages/bunshot-core/src/coreRegistrar.d.ts +6 -0
- package/dist/packages/bunshot-core/src/coreRegistrar.js +42 -0
- package/dist/{lib → packages/bunshot-core/src}/createRoute.d.ts +4 -30
- package/dist/{lib → packages/bunshot-core/src}/createRoute.js +39 -88
- package/dist/packages/bunshot-core/src/cronRegistry.d.ts +11 -0
- package/dist/packages/bunshot-core/src/cronRegistry.js +1 -0
- package/dist/packages/bunshot-core/src/crypto.d.ts +43 -0
- package/dist/packages/bunshot-core/src/crypto.js +74 -0
- package/dist/packages/bunshot-core/src/csrf.d.ts +8 -0
- package/dist/packages/bunshot-core/src/csrf.js +1 -0
- package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.d.ts +7 -0
- package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.js +19 -0
- package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.d.ts +6 -0
- package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.js +40 -0
- package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.d.ts +6 -0
- package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.js +24 -0
- package/dist/packages/bunshot-core/src/emailTemplates.d.ts +5 -0
- package/dist/packages/bunshot-core/src/emailTemplates.js +10 -0
- package/dist/{lib/HttpError.d.ts → packages/bunshot-core/src/errors.d.ts} +4 -1
- package/dist/{lib/HttpError.js → packages/bunshot-core/src/errors.js} +7 -1
- package/dist/packages/bunshot-core/src/eventBus.d.ts +270 -0
- package/dist/packages/bunshot-core/src/eventBus.js +143 -0
- package/dist/packages/bunshot-core/src/idempotency.d.ts +18 -0
- package/dist/packages/bunshot-core/src/idempotency.js +1 -0
- package/dist/packages/bunshot-core/src/index.d.ts +60 -0
- package/dist/packages/bunshot-core/src/index.js +34 -0
- package/dist/packages/bunshot-core/src/mail.d.ts +14 -0
- package/dist/packages/bunshot-core/src/mail.js +8 -0
- package/dist/packages/bunshot-core/src/memoryEviction.d.ts +24 -0
- package/dist/packages/bunshot-core/src/memoryEviction.js +52 -0
- package/dist/packages/bunshot-core/src/pagination.d.ts +45 -0
- package/dist/packages/bunshot-core/src/pagination.js +61 -0
- package/dist/packages/bunshot-core/src/permissions.d.ts +64 -0
- package/dist/packages/bunshot-core/src/permissions.js +27 -0
- package/dist/packages/bunshot-core/src/plugin.d.ts +44 -0
- package/dist/packages/bunshot-core/src/plugin.js +1 -0
- package/dist/packages/bunshot-core/src/rateLimit.d.ts +5 -0
- package/dist/packages/bunshot-core/src/rateLimit.js +18 -0
- package/dist/packages/bunshot-core/src/redis.d.ts +21 -0
- package/dist/packages/bunshot-core/src/redis.js +1 -0
- package/dist/packages/bunshot-core/src/routeAuth.d.ts +5 -0
- package/dist/packages/bunshot-core/src/routeAuth.js +11 -0
- package/dist/packages/bunshot-core/src/routeOverrides.d.ts +24 -0
- package/dist/packages/bunshot-core/src/routeOverrides.js +25 -0
- package/dist/packages/bunshot-core/src/routerAdapter.d.ts +6 -0
- package/dist/packages/bunshot-core/src/routerAdapter.js +56 -0
- package/dist/packages/bunshot-core/src/secrets.d.ts +48 -0
- package/dist/packages/bunshot-core/src/secrets.js +8 -0
- package/dist/packages/bunshot-core/src/signing.d.ts +41 -0
- package/dist/packages/bunshot-core/src/signing.js +1 -0
- package/dist/packages/bunshot-core/src/sse.d.ts +36 -0
- package/dist/packages/bunshot-core/src/sse.js +1 -0
- package/dist/packages/bunshot-core/src/storageAdapter.js +1 -0
- package/dist/packages/bunshot-core/src/storeInfra.d.ts +44 -0
- package/dist/packages/bunshot-core/src/storeInfra.js +18 -0
- package/dist/packages/bunshot-core/src/storeType.d.ts +7 -0
- package/dist/packages/bunshot-core/src/storeType.js +1 -0
- package/dist/packages/bunshot-core/src/testing.d.ts +1 -0
- package/dist/packages/bunshot-core/src/testing.js +1 -0
- package/dist/packages/bunshot-core/src/uploadRegistry.d.ts +23 -0
- package/dist/packages/bunshot-core/src/uploadRegistry.js +4 -0
- package/dist/packages/bunshot-core/src/userResolver.d.ts +5 -0
- package/dist/packages/bunshot-core/src/userResolver.js +14 -0
- package/dist/packages/bunshot-core/src/wsMessages.d.ts +42 -0
- package/dist/packages/bunshot-core/src/wsMessages.js +4 -0
- package/dist/packages/bunshot-permissions/src/adapters/memory.d.ts +7 -0
- package/dist/packages/bunshot-permissions/src/adapters/memory.js +73 -0
- package/dist/packages/bunshot-permissions/src/index.d.ts +10 -0
- package/dist/packages/bunshot-permissions/src/index.js +5 -0
- package/dist/packages/bunshot-permissions/src/lib/bootstrap.d.ts +7 -0
- package/dist/packages/bunshot-permissions/src/lib/bootstrap.js +12 -0
- package/dist/packages/bunshot-permissions/src/lib/evaluator.d.ts +10 -0
- package/dist/packages/bunshot-permissions/src/lib/evaluator.js +165 -0
- package/dist/packages/bunshot-permissions/src/lib/registry.d.ts +2 -0
- package/dist/packages/bunshot-permissions/src/lib/registry.js +31 -0
- package/dist/packages/bunshot-permissions/src/lib/validation.d.ts +1 -0
- package/dist/packages/bunshot-permissions/src/lib/validation.js +1 -0
- package/dist/packages/bunshot-permissions/src/types/adapter.d.ts +1 -0
- package/dist/packages/bunshot-permissions/src/types/adapter.js +1 -0
- package/dist/packages/bunshot-permissions/src/types/evaluator.d.ts +1 -0
- package/dist/packages/bunshot-permissions/src/types/evaluator.js +1 -0
- package/dist/packages/bunshot-permissions/src/types/models.d.ts +1 -0
- package/dist/packages/bunshot-permissions/src/types/models.js +1 -0
- package/dist/packages/bunshot-permissions/src/types/registry.d.ts +1 -0
- package/dist/packages/bunshot-permissions/src/types/registry.js +1 -0
- package/dist/packages/bunshot-postgres/src/adapter.d.ts +6 -0
- package/dist/packages/bunshot-postgres/src/adapter.js +794 -0
- package/dist/packages/bunshot-postgres/src/connection.d.ts +15 -0
- package/dist/packages/bunshot-postgres/src/connection.js +16 -0
- package/dist/packages/bunshot-postgres/src/index.d.ts +4 -0
- package/dist/packages/bunshot-postgres/src/index.js +2 -0
- package/dist/packages/bunshot-postgres/src/schema.d.ts +997 -0
- package/dist/packages/bunshot-postgres/src/schema.js +105 -0
- package/dist/src/app.d.ts +230 -0
- package/dist/src/app.js +182 -0
- package/dist/src/cli/commands/init.d.ts +10 -0
- package/dist/src/cli/commands/init.js +709 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +3 -0
- package/dist/src/entrypoints/mongo.d.ts +6 -0
- package/dist/src/entrypoints/mongo.js +4 -0
- package/dist/src/entrypoints/queue.d.ts +2 -0
- package/dist/src/entrypoints/queue.js +1 -0
- package/dist/src/entrypoints/redis.d.ts +1 -0
- package/dist/src/entrypoints/redis.js +1 -0
- package/dist/{adapters → src/framework/adapters}/localStorage.d.ts +1 -1
- package/dist/{adapters → src/framework/adapters}/localStorage.js +10 -10
- package/dist/src/framework/adapters/memoryStorage.d.ts +2 -0
- package/dist/src/framework/adapters/memoryStorage.js +45 -0
- package/dist/{adapters → src/framework/adapters}/s3Storage.d.ts +1 -1
- package/dist/{adapters → src/framework/adapters}/s3Storage.js +12 -12
- package/dist/src/framework/admin/bunshotAccess.d.ts +2 -0
- package/dist/src/framework/admin/bunshotAccess.js +23 -0
- package/dist/src/framework/admin/bunshotUsers.d.ts +2 -0
- package/dist/src/framework/admin/bunshotUsers.js +103 -0
- package/dist/src/framework/admin/index.d.ts +7 -0
- package/dist/src/framework/admin/index.js +21 -0
- package/dist/src/framework/boundaryAdapters/cacheFactories.d.ts +13 -0
- package/dist/src/framework/boundaryAdapters/cacheFactories.js +86 -0
- package/dist/src/framework/boundaryAdapters/index.d.ts +2 -0
- package/dist/src/framework/boundaryAdapters/index.js +1 -0
- package/dist/src/framework/boundaryAdapters.d.ts +17 -0
- package/dist/src/framework/boundaryAdapters.js +62 -0
- package/dist/src/framework/buildContext.d.ts +33 -0
- package/dist/src/framework/buildContext.js +119 -0
- package/dist/src/framework/config/schema.d.ts +447 -0
- package/dist/src/framework/config/schema.js +528 -0
- package/dist/src/framework/createInfrastructure.d.ts +76 -0
- package/dist/src/framework/createInfrastructure.js +221 -0
- package/dist/src/framework/lib/auditLog.d.ts +23 -0
- package/dist/src/framework/lib/auditLog.js +416 -0
- package/dist/src/framework/lib/captcha.d.ts +11 -0
- package/dist/{lib → src/framework/lib}/captcha.js +13 -10
- package/dist/{lib → src/framework/lib}/createDtoMapper.js +4 -4
- package/dist/src/framework/lib/createRoute.d.ts +1 -0
- package/dist/src/framework/lib/createRoute.js +2 -0
- package/dist/{lib → src/framework/lib}/idempotency.d.ts +2 -6
- package/dist/src/framework/lib/idempotency.js +74 -0
- package/dist/src/framework/lib/logger.d.ts +3 -0
- package/dist/src/framework/lib/logger.js +14 -0
- package/dist/src/framework/lib/metrics.d.ts +34 -0
- package/dist/{lib → src/framework/lib}/metrics.js +49 -57
- package/dist/src/framework/lib/pagination.d.ts +42 -0
- package/dist/src/framework/lib/pagination.js +51 -0
- package/dist/src/framework/lib/redisTransport.d.ts +38 -0
- package/dist/src/framework/lib/redisTransport.js +107 -0
- package/dist/src/framework/lib/resolveUserId.d.ts +2 -0
- package/dist/src/framework/lib/resolveUserId.js +5 -0
- package/dist/src/framework/lib/sseCollision.d.ts +6 -0
- package/dist/src/framework/lib/sseCollision.js +26 -0
- package/dist/src/framework/lib/storageAdapter.d.ts +1 -0
- package/dist/src/framework/lib/storageAdapter.js +1 -0
- package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.js +4 -4
- package/dist/src/framework/lib/tenant.d.ts +21 -0
- package/dist/src/framework/lib/tenant.js +70 -0
- package/dist/{lib → src/framework/lib}/upload.d.ts +11 -10
- package/dist/src/framework/lib/upload.js +132 -0
- package/dist/src/framework/lib/uploadRegistry.d.ts +23 -0
- package/dist/src/framework/lib/uploadRegistry.js +34 -0
- package/dist/{lib → src/framework/lib}/validate.d.ts +1 -1
- package/dist/{lib → src/framework/lib}/validate.js +2 -2
- package/dist/src/framework/lib/ws.d.ts +19 -0
- package/dist/src/framework/lib/ws.js +130 -0
- package/dist/src/framework/lib/wsHeartbeat.d.ts +12 -0
- package/dist/src/framework/lib/wsHeartbeat.js +53 -0
- package/dist/src/framework/lib/wsMessages.d.ts +25 -0
- package/dist/src/framework/lib/wsMessages.js +45 -0
- package/dist/src/framework/lib/wsNamespace.d.ts +17 -0
- package/dist/src/framework/lib/wsNamespace.js +19 -0
- package/dist/src/framework/lib/wsPresence.d.ts +17 -0
- package/dist/src/framework/lib/wsPresence.js +84 -0
- package/dist/src/framework/lib/wsTransport.d.ts +38 -0
- package/dist/src/framework/lib/wsTransport.js +9 -0
- package/dist/{lib → src/framework/lib}/zodToMongoose.d.ts +1 -1
- package/dist/{lib → src/framework/lib}/zodToMongoose.js +11 -11
- package/dist/{middleware → src/framework/middleware}/auditLog.d.ts +4 -3
- package/dist/src/framework/middleware/auditLog.js +42 -0
- package/dist/{middleware → src/framework/middleware}/botProtection.d.ts +2 -2
- package/dist/{middleware → src/framework/middleware}/botProtection.js +8 -9
- package/dist/src/framework/middleware/cacheResponse.d.ts +35 -0
- package/dist/src/framework/middleware/cacheResponse.js +126 -0
- package/dist/{middleware → src/framework/middleware}/captcha.d.ts +2 -3
- package/dist/src/framework/middleware/captcha.js +37 -0
- package/dist/{middleware → src/framework/middleware}/errorHandler.d.ts +1 -1
- package/dist/{middleware → src/framework/middleware}/errorHandler.js +2 -2
- package/dist/src/framework/middleware/index.js +1 -0
- package/dist/{middleware → src/framework/middleware}/logger.d.ts +1 -1
- package/dist/src/framework/middleware/metrics.d.ts +12 -0
- package/dist/src/framework/middleware/metrics.js +26 -0
- package/dist/{middleware → src/framework/middleware}/rateLimit.d.ts +2 -2
- package/dist/src/framework/middleware/rateLimit.js +22 -0
- package/dist/src/framework/middleware/requestId.d.ts +3 -0
- package/dist/{middleware → src/framework/middleware}/requestId.js +2 -2
- package/dist/{middleware → src/framework/middleware}/requestLogger.d.ts +3 -3
- package/dist/{middleware → src/framework/middleware}/requestLogger.js +17 -12
- package/dist/{middleware → src/framework/middleware}/requestSigning.d.ts +2 -2
- package/dist/{middleware → src/framework/middleware}/requestSigning.js +18 -20
- package/dist/src/framework/middleware/tenant.d.ts +14 -0
- package/dist/{middleware → src/framework/middleware}/tenant.js +31 -27
- package/dist/src/framework/middleware/upload.d.ts +5 -0
- package/dist/{middleware → src/framework/middleware}/upload.js +4 -4
- package/dist/{middleware → src/framework/middleware}/webhookAuth.d.ts +3 -3
- package/dist/{middleware → src/framework/middleware}/webhookAuth.js +11 -12
- package/dist/src/framework/models/AuditLog.d.ts +21 -0
- package/dist/src/framework/models/AuditLog.js +31 -0
- package/dist/src/framework/mountMiddleware.d.ts +91 -0
- package/dist/src/framework/mountMiddleware.js +128 -0
- package/dist/src/framework/mountOptionalEndpoints.d.ts +103 -0
- package/dist/src/framework/mountOptionalEndpoints.js +47 -0
- package/dist/src/framework/mountRoutes.d.ts +21 -0
- package/dist/src/framework/mountRoutes.js +144 -0
- package/dist/src/framework/persistence/cronRegistry.d.ts +28 -0
- package/dist/src/framework/persistence/cronRegistry.js +139 -0
- package/dist/src/framework/persistence/idempotency.d.ts +26 -0
- package/dist/src/framework/persistence/idempotency.js +178 -0
- package/dist/src/framework/persistence/index.d.ts +6 -0
- package/dist/src/framework/persistence/index.js +8 -0
- package/dist/src/framework/persistence/storeInfra.d.ts +9 -0
- package/dist/src/framework/persistence/storeInfra.js +1 -0
- package/dist/src/framework/persistence/uploadRegistry.d.ts +35 -0
- package/dist/src/framework/persistence/uploadRegistry.js +235 -0
- package/dist/src/framework/persistence/wsMessages.d.ts +22 -0
- package/dist/src/framework/persistence/wsMessages.js +296 -0
- package/dist/src/framework/preloadSchemas.d.ts +24 -0
- package/dist/src/framework/preloadSchemas.js +42 -0
- package/dist/src/framework/registerBoundaryAdapters.d.ts +23 -0
- package/dist/src/framework/registerBoundaryAdapters.js +46 -0
- package/dist/src/framework/routes/admin.d.ts +9 -0
- package/dist/src/framework/routes/admin.js +361 -0
- package/dist/src/framework/routes/health.d.ts +1 -0
- package/dist/src/framework/routes/health.js +21 -0
- package/dist/src/framework/routes/home.d.ts +1 -0
- package/dist/src/framework/routes/home.js +18 -0
- package/dist/src/framework/routes/jobs.d.ts +3 -0
- package/dist/{routes → src/framework/routes}/jobs.js +128 -103
- package/dist/src/framework/routes/metrics.d.ts +10 -0
- package/dist/src/framework/routes/metrics.js +57 -0
- package/dist/{routes → src/framework/routes}/uploads.d.ts +3 -3
- package/dist/src/framework/routes/uploads.js +262 -0
- package/dist/src/framework/runPluginLifecycle.d.ts +27 -0
- package/dist/src/framework/runPluginLifecycle.js +121 -0
- package/dist/src/framework/secrets/frameworkSecretSchema.d.ts +58 -0
- package/dist/src/framework/secrets/frameworkSecretSchema.js +20 -0
- package/dist/src/framework/secrets/index.d.ts +9 -0
- package/dist/src/framework/secrets/index.js +7 -0
- package/dist/src/framework/secrets/providers/envProvider.d.ts +15 -0
- package/dist/src/framework/secrets/providers/envProvider.js +18 -0
- package/dist/src/framework/secrets/providers/fileProvider.d.ts +8 -0
- package/dist/src/framework/secrets/providers/fileProvider.js +82 -0
- package/dist/src/framework/secrets/providers/ssmProvider.d.ts +20 -0
- package/dist/src/framework/secrets/providers/ssmProvider.js +127 -0
- package/dist/src/framework/secrets/resolveSecretBundle.d.ts +53 -0
- package/dist/src/framework/secrets/resolveSecretBundle.js +84 -0
- package/dist/src/framework/secrets/resolveSecrets.d.ts +18 -0
- package/dist/src/framework/secrets/resolveSecrets.js +34 -0
- package/dist/src/framework/sse/index.d.ts +21 -0
- package/dist/src/framework/sse/index.js +109 -0
- package/dist/src/framework/ws/index.d.ts +11 -0
- package/dist/src/framework/ws/index.js +8 -0
- package/dist/src/index.d.ts +87 -0
- package/dist/src/index.js +58 -0
- package/dist/src/lib/appConfig.d.ts +7 -0
- package/dist/src/lib/appConfig.js +27 -0
- package/dist/src/lib/appMeta.d.ts +7 -0
- package/dist/src/lib/appMeta.js +3 -0
- package/dist/src/lib/authConfig.d.ts +532 -0
- package/dist/{lib/appConfig.js → src/lib/authConfig.js} +75 -17
- package/dist/{lib → src/lib}/context.d.ts +6 -12
- package/dist/{lib → src/lib}/context.js +5 -5
- package/dist/src/lib/logger.d.ts +1 -0
- package/dist/src/lib/logger.js +1 -0
- package/dist/src/lib/mongo.d.ts +58 -0
- package/dist/src/lib/mongo.js +96 -0
- package/dist/src/lib/queue.d.ts +72 -0
- package/dist/src/lib/queue.js +152 -0
- package/dist/src/lib/redis.d.ts +28 -0
- package/dist/src/lib/redis.js +72 -0
- package/dist/{lib → src/lib}/signing.d.ts +2 -2
- package/dist/src/lib/signing.js +210 -0
- package/dist/src/lib/signingConfig.d.ts +40 -0
- package/dist/src/lib/signingConfig.js +28 -0
- package/dist/src/server.d.ts +146 -0
- package/dist/src/server.js +469 -0
- package/dist/src/shared/lib/HttpError.d.ts +1 -0
- package/dist/src/shared/lib/HttpError.js +2 -0
- package/dist/src/shared/lib/constants.d.ts +10 -0
- package/dist/src/shared/lib/crypto.d.ts +43 -0
- package/dist/src/shared/lib/crypto.js +74 -0
- package/dist/src/shared/lib/signing.d.ts +52 -0
- package/dist/{lib → src/shared/lib}/signing.js +35 -8
- package/dist/src/testing.d.ts +34 -0
- package/dist/src/testing.js +93 -0
- package/package.json +60 -24
- package/dist/adapters/memoryAuth.d.ts +0 -52
- package/dist/adapters/memoryAuth.js +0 -749
- package/dist/adapters/memoryStorage.d.ts +0 -3
- package/dist/adapters/memoryStorage.js +0 -44
- package/dist/adapters/mongoAuth.d.ts +0 -2
- package/dist/adapters/mongoAuth.js +0 -403
- package/dist/adapters/sqliteAuth.d.ts +0 -72
- package/dist/adapters/sqliteAuth.js +0 -858
- package/dist/app.d.ts +0 -559
- package/dist/app.js +0 -651
- package/dist/entrypoints/mongo.d.ts +0 -5
- package/dist/entrypoints/mongo.js +0 -4
- package/dist/entrypoints/queue.d.ts +0 -2
- package/dist/entrypoints/queue.js +0 -1
- package/dist/entrypoints/redis.d.ts +0 -1
- package/dist/entrypoints/redis.js +0 -1
- package/dist/index.d.ts +0 -117
- package/dist/index.js +0 -88
- package/dist/lib/appConfig.d.ts +0 -275
- package/dist/lib/auditLog.d.ts +0 -58
- package/dist/lib/auditLog.js +0 -218
- package/dist/lib/authAdapter.d.ts +0 -246
- package/dist/lib/authAdapter.js +0 -7
- package/dist/lib/authRateLimit.d.ts +0 -13
- package/dist/lib/authRateLimit.js +0 -117
- package/dist/lib/clientIp.d.ts +0 -14
- package/dist/lib/credentialStuffing.d.ts +0 -31
- package/dist/lib/credentialStuffing.js +0 -77
- package/dist/lib/crypto.d.ts +0 -11
- package/dist/lib/crypto.js +0 -22
- package/dist/lib/deletionCancelToken.d.ts +0 -12
- package/dist/lib/deletionCancelToken.js +0 -88
- package/dist/lib/emailVerification.d.ts +0 -19
- package/dist/lib/emailVerification.js +0 -129
- package/dist/lib/fingerprint.js +0 -36
- package/dist/lib/idempotency.js +0 -182
- package/dist/lib/jwks.d.ts +0 -25
- package/dist/lib/jwks.js +0 -51
- package/dist/lib/jwt.d.ts +0 -15
- package/dist/lib/jwt.js +0 -111
- package/dist/lib/metrics.d.ts +0 -14
- package/dist/lib/mfaChallenge.d.ts +0 -55
- package/dist/lib/mfaChallenge.js +0 -398
- package/dist/lib/mongo.d.ts +0 -39
- package/dist/lib/mongo.js +0 -124
- package/dist/lib/oauth.d.ts +0 -40
- package/dist/lib/oauth.js +0 -101
- package/dist/lib/oauthCode.d.ts +0 -15
- package/dist/lib/oauthCode.js +0 -95
- package/dist/lib/pagination.d.ts +0 -119
- package/dist/lib/pagination.js +0 -166
- package/dist/lib/queue.d.ts +0 -37
- package/dist/lib/queue.js +0 -117
- package/dist/lib/redis.d.ts +0 -9
- package/dist/lib/redis.js +0 -61
- package/dist/lib/resetPassword.d.ts +0 -12
- package/dist/lib/resetPassword.js +0 -93
- package/dist/lib/roles.d.ts +0 -7
- package/dist/lib/roles.js +0 -49
- package/dist/lib/saml.d.ts +0 -25
- package/dist/lib/saml.js +0 -64
- package/dist/lib/securityEvents.d.ts +0 -28
- package/dist/lib/securityEvents.js +0 -26
- package/dist/lib/session.d.ts +0 -49
- package/dist/lib/session.js +0 -597
- package/dist/lib/tenant.d.ts +0 -15
- package/dist/lib/tenant.js +0 -65
- package/dist/lib/upload.js +0 -112
- package/dist/lib/uploadRegistry.d.ts +0 -18
- package/dist/lib/uploadRegistry.js +0 -83
- package/dist/lib/ws.d.ts +0 -22
- package/dist/lib/ws.js +0 -96
- package/dist/lib/wsHeartbeat.d.ts +0 -12
- package/dist/lib/wsHeartbeat.js +0 -57
- package/dist/lib/wsMessages.d.ts +0 -40
- package/dist/lib/wsMessages.js +0 -330
- package/dist/lib/wsPresence.d.ts +0 -25
- package/dist/lib/wsPresence.js +0 -99
- package/dist/middleware/auditLog.js +0 -39
- package/dist/middleware/bearerAuth.d.ts +0 -2
- package/dist/middleware/bearerAuth.js +0 -11
- package/dist/middleware/cacheResponse.d.ts +0 -15
- package/dist/middleware/cacheResponse.js +0 -178
- package/dist/middleware/captcha.js +0 -36
- package/dist/middleware/csrf.js +0 -129
- package/dist/middleware/identify.d.ts +0 -3
- package/dist/middleware/identify.js +0 -122
- package/dist/middleware/index.js +0 -1
- package/dist/middleware/metrics.d.ts +0 -9
- package/dist/middleware/metrics.js +0 -26
- package/dist/middleware/rateLimit.js +0 -22
- package/dist/middleware/requestId.d.ts +0 -3
- package/dist/middleware/scimAuth.d.ts +0 -8
- package/dist/middleware/scimAuth.js +0 -29
- package/dist/middleware/tenant.d.ts +0 -5
- package/dist/middleware/upload.d.ts +0 -5
- package/dist/middleware/userAuth.d.ts +0 -3
- package/dist/middleware/userAuth.js +0 -6
- package/dist/models/AuditLog.d.ts +0 -30
- package/dist/models/AuditLog.js +0 -39
- package/dist/models/AuthUser.js +0 -55
- package/dist/models/Group.d.ts +0 -21
- package/dist/models/Group.js +0 -28
- package/dist/models/GroupMembership.js +0 -25
- package/dist/models/TenantRole.d.ts +0 -15
- package/dist/models/TenantRole.js +0 -23
- package/dist/routes/auth.d.ts +0 -12
- package/dist/routes/auth.js +0 -744
- package/dist/routes/groups.js +0 -346
- package/dist/routes/health.d.ts +0 -1
- package/dist/routes/health.js +0 -22
- package/dist/routes/home.d.ts +0 -1
- package/dist/routes/home.js +0 -16
- package/dist/routes/jobs.d.ts +0 -2
- package/dist/routes/m2m.d.ts +0 -2
- package/dist/routes/m2m.js +0 -72
- package/dist/routes/metrics.d.ts +0 -8
- package/dist/routes/metrics.js +0 -55
- package/dist/routes/mfa.d.ts +0 -5
- package/dist/routes/mfa.js +0 -628
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/oauth.js +0 -520
- package/dist/routes/oidc.d.ts +0 -2
- package/dist/routes/oidc.js +0 -29
- package/dist/routes/passkey.d.ts +0 -1
- package/dist/routes/passkey.js +0 -157
- package/dist/routes/saml.d.ts +0 -2
- package/dist/routes/saml.js +0 -86
- package/dist/routes/scim.d.ts +0 -2
- package/dist/routes/scim.js +0 -255
- package/dist/routes/uploads.js +0 -227
- package/dist/schemas/auth.js +0 -30
- package/dist/server.d.ts +0 -57
- package/dist/server.js +0 -112
- package/dist/services/auth.d.ts +0 -29
- package/dist/services/auth.js +0 -238
- package/dist/ws/index.d.ts +0 -10
- package/dist/ws/index.js +0 -39
- package/docs/sections/adding-middleware/full.md +0 -35
- package/docs/sections/adding-models/full.md +0 -125
- package/docs/sections/adding-models/overview.md +0 -13
- package/docs/sections/adding-routes/full.md +0 -182
- package/docs/sections/adding-routes/overview.md +0 -23
- package/docs/sections/auth-flow/full.md +0 -790
- package/docs/sections/auth-flow/overview.md +0 -10
- package/docs/sections/auth-security-examples/full.md +0 -388
- package/docs/sections/authentication/full.md +0 -130
- package/docs/sections/authentication/overview.md +0 -5
- package/docs/sections/cli/full.md +0 -42
- package/docs/sections/configuration/full.md +0 -172
- package/docs/sections/configuration/overview.md +0 -18
- package/docs/sections/configuration-example/full.md +0 -117
- package/docs/sections/configuration-example/overview.md +0 -30
- package/docs/sections/documentation/full.md +0 -171
- package/docs/sections/environment-variables/full.md +0 -55
- package/docs/sections/exports/full.md +0 -123
- package/docs/sections/extending-context/full.md +0 -59
- package/docs/sections/header.md +0 -3
- package/docs/sections/installation/full.md +0 -6
- package/docs/sections/jobs/full.md +0 -140
- package/docs/sections/jobs/overview.md +0 -15
- package/docs/sections/logging/full.md +0 -83
- package/docs/sections/metrics/full.md +0 -131
- package/docs/sections/mongodb-connections/full.md +0 -45
- package/docs/sections/mongodb-connections/overview.md +0 -7
- package/docs/sections/multi-tenancy/full.md +0 -66
- package/docs/sections/multi-tenancy/overview.md +0 -15
- package/docs/sections/oauth/full.md +0 -189
- package/docs/sections/oauth/overview.md +0 -16
- package/docs/sections/package-development/full.md +0 -7
- package/docs/sections/pagination/full.md +0 -93
- package/docs/sections/passkey-login/full.md +0 -90
- package/docs/sections/passkey-login/overview.md +0 -1
- package/docs/sections/peer-dependencies/full.md +0 -47
- package/docs/sections/quick-start/full.md +0 -43
- package/docs/sections/response-caching/full.md +0 -117
- package/docs/sections/response-caching/overview.md +0 -13
- package/docs/sections/roles/full.md +0 -225
- package/docs/sections/roles/overview.md +0 -14
- package/docs/sections/running-without-redis/full.md +0 -16
- package/docs/sections/running-without-redis-or-mongodb/full.md +0 -60
- package/docs/sections/signing/full.md +0 -203
- package/docs/sections/stack/full.md +0 -10
- package/docs/sections/uploads/full.md +0 -208
- package/docs/sections/versioning/full.md +0 -85
- package/docs/sections/webhook-auth/full.md +0 -100
- package/docs/sections/websocket/full.md +0 -196
- package/docs/sections/websocket/overview.md +0 -5
- package/docs/sections/websocket-rooms/full.md +0 -102
- package/docs/sections/websocket-rooms/overview.md +0 -5
- /package/dist/{lib/storageAdapter.js → packages/bunshot-admin/src/types/env.js} +0 -0
- /package/dist/{lib → packages/bunshot-auth/src/lib}/fingerprint.d.ts +0 -0
- /package/dist/{lib → packages/bunshot-auth/src/lib}/logger.d.ts +0 -0
- /package/dist/{lib → packages/bunshot-core/src}/constants.d.ts +0 -0
- /package/dist/{lib → packages/bunshot-core/src}/storageAdapter.d.ts +0 -0
- /package/dist/{lib → src/framework/lib}/createDtoMapper.d.ts +0 -0
- /package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.d.ts +0 -0
- /package/dist/{middleware → src/framework/middleware}/cors.d.ts +0 -0
- /package/dist/{middleware → src/framework/middleware}/cors.js +0 -0
- /package/dist/{middleware → src/framework/middleware}/index.d.ts +0 -0
- /package/dist/{middleware → src/framework/middleware}/logger.js +0 -0
- /package/dist/{lib → src/shared/lib}/constants.js +0 -0
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
import { getAuthCookieOptions } from '../lib/cookieOptions';
|
|
2
|
+
import { isProd } from '../lib/env';
|
|
3
|
+
import { generateCodeVerifier, generateState } from '../lib/oauth';
|
|
4
|
+
import { consumeOAuthCode, storeOAuthCode } from '../lib/oauthCode';
|
|
5
|
+
import { consumeReauthConfirmation, storeReauthConfirmation } from '../lib/oauthReauth';
|
|
6
|
+
import { refreshCsrfToken } from '../middleware/csrf';
|
|
7
|
+
import { userAuth } from '../middleware/userAuth';
|
|
8
|
+
import { ErrorResponse as OAuthErrorResponse } from '../schemas/error';
|
|
9
|
+
import { createSessionForUser, emitLoginSuccess, runPreLoginHook } from '../services/auth';
|
|
10
|
+
import { decodeIdToken } from 'arctic';
|
|
11
|
+
import { setCookie } from 'hono/cookie';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { createRoute, withSecurity } from '../../../bunshot-core/src/index.js';
|
|
14
|
+
import { createRouter } from '../../../bunshot-core/src/index.js';
|
|
15
|
+
import { HttpError } from '../../../bunshot-core/src/index.js';
|
|
16
|
+
import { COOKIE_REFRESH_TOKEN, COOKIE_TOKEN } from '../../../bunshot-core/src/index.js';
|
|
17
|
+
import { getClientIp } from '../../../bunshot-core/src/index.js';
|
|
18
|
+
const hookCtx = (c) => ({
|
|
19
|
+
ip: getClientIp(c) !== 'unknown' ? getClientIp(c) : undefined,
|
|
20
|
+
userAgent: c.req.header('user-agent') ?? undefined,
|
|
21
|
+
requestId: c.get('requestId'),
|
|
22
|
+
});
|
|
23
|
+
const tags = ['OAuth'];
|
|
24
|
+
// `postLoginRedirect` is always sourced from server-side config (passed into
|
|
25
|
+
// `createOAuthRouter` at startup) and is never derived from user-supplied input
|
|
26
|
+
// or OAuth callback query parameters. No runtime allowlist validation is required
|
|
27
|
+
// because the value is not attacker-controlled. The `auth.oauth.allowedRedirectUrls`
|
|
28
|
+
// config is available for consuming apps that want to enforce an explicit allowlist
|
|
29
|
+
// at the framework level if they ever pass a dynamic value here.
|
|
30
|
+
const finishOAuth = async (c, runtime, provider, providerId, profile, postLoginRedirect) => {
|
|
31
|
+
const { adapter, eventBus, config } = runtime;
|
|
32
|
+
if (!adapter.findOrCreateByProvider) {
|
|
33
|
+
return c.json({ error: 'Auth adapter does not support social login' }, 500);
|
|
34
|
+
}
|
|
35
|
+
const identifier = profile.email ?? providerId;
|
|
36
|
+
const ctx = hookCtx(c);
|
|
37
|
+
try {
|
|
38
|
+
// Fire preLogin before the adapter so OAuth users are subject to the same
|
|
39
|
+
// access control as email/password users (fixes: #30).
|
|
40
|
+
// preLogin runs for all OAuth sign-ins (new and returning) — a single hook
|
|
41
|
+
// covers the allowlist case. preRegister is intentionally omitted: by the
|
|
42
|
+
// time we know if user.created is true, the record already exists, so it
|
|
43
|
+
// cannot gate registration. preLogin is the correct and sufficient gate.
|
|
44
|
+
await runPreLoginHook(identifier, runtime, ctx);
|
|
45
|
+
const user = await adapter.findOrCreateByProvider(provider, providerId, profile);
|
|
46
|
+
if (user.created) {
|
|
47
|
+
const role = config.defaultRole;
|
|
48
|
+
if (role && adapter.setRoles)
|
|
49
|
+
await adapter.setRoles(user.id, [role]);
|
|
50
|
+
}
|
|
51
|
+
const metadata = {
|
|
52
|
+
ipAddress: getClientIp(c),
|
|
53
|
+
userAgent: c.req.header('user-agent') ?? undefined,
|
|
54
|
+
};
|
|
55
|
+
const session = await createSessionForUser(user.id, runtime, metadata, ctx);
|
|
56
|
+
const { token, refreshToken: refreshTokenValue, sessionId } = session;
|
|
57
|
+
emitLoginSuccess(user.id, sessionId, runtime);
|
|
58
|
+
// Store a one-time authorization code instead of exposing the token in the redirect URL.
|
|
59
|
+
// The client exchanges this code via POST /auth/oauth/exchange to get the session token.
|
|
60
|
+
const code = await storeOAuthCode(runtime.repos.oauthCode, {
|
|
61
|
+
token,
|
|
62
|
+
userId: user.id,
|
|
63
|
+
email: profile.email,
|
|
64
|
+
refreshToken: refreshTokenValue,
|
|
65
|
+
}, runtime.dataEncryptionKeys);
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(postLoginRedirect);
|
|
68
|
+
url.searchParams.set('code', code);
|
|
69
|
+
if (profile.email)
|
|
70
|
+
url.searchParams.set('user', profile.email);
|
|
71
|
+
return c.redirect(url.toString());
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Relative path fallback
|
|
75
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
76
|
+
const userParam = profile.email ? `&user=${encodeURIComponent(profile.email)}` : '';
|
|
77
|
+
return c.redirect(`${postLoginRedirect}${sep}code=${code}${userParam}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const message = err instanceof HttpError ? err.message : 'Authentication failed';
|
|
82
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
83
|
+
return c.redirect(`${postLoginRedirect}${sep}error=${encodeURIComponent(message)}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export const createOAuthRouter = (providers, postLoginRedirect, runtime, rateLimit) => {
|
|
87
|
+
const { adapter, eventBus } = runtime;
|
|
88
|
+
const oauthUnlinkOpts = {
|
|
89
|
+
windowMs: rateLimit?.oauthUnlink?.windowMs ?? 60 * 60 * 1000,
|
|
90
|
+
max: rateLimit?.oauthUnlink?.max ?? 5,
|
|
91
|
+
};
|
|
92
|
+
const getConfig = () => runtime.config;
|
|
93
|
+
const unlinkVerificationSchema = z.object({
|
|
94
|
+
method: z
|
|
95
|
+
.enum(['totp', 'emailOtp', 'webauthn', 'password', 'recovery'])
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('Verification method to use.'),
|
|
98
|
+
code: z.string().optional().describe('TOTP code, email OTP code, or recovery code.'),
|
|
99
|
+
password: z.string().optional().describe('Account password.'),
|
|
100
|
+
reauthToken: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Reauth challenge token (required for emailOtp and webauthn methods).'),
|
|
104
|
+
webauthnResponse: z
|
|
105
|
+
.record(z.string(), z.unknown())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('WebAuthn assertion response (required for webauthn method).'),
|
|
108
|
+
});
|
|
109
|
+
async function verifyUnlinkFactor(userId, sessionId, body) {
|
|
110
|
+
const hasPassword = adapter.hasPassword ? await adapter.hasPassword(userId) : false;
|
|
111
|
+
const mfaMethods = getConfig().mfa && adapter.getMfaMethods ? await adapter.getMfaMethods(userId) : [];
|
|
112
|
+
if (!hasPassword && mfaMethods.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
const method = body.method ?? (body.password ? 'password' : body.code ? 'totp' : undefined);
|
|
115
|
+
if (!method) {
|
|
116
|
+
return {
|
|
117
|
+
error: 'Verification is required to unlink this provider. Provide method and credentials.',
|
|
118
|
+
status: 400,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const { verifyAnyFactor } = await import('../services/mfa');
|
|
122
|
+
const valid = await verifyAnyFactor(userId, sessionId, runtime, {
|
|
123
|
+
method,
|
|
124
|
+
code: body.code,
|
|
125
|
+
password: body.password,
|
|
126
|
+
reauthToken: body.reauthToken,
|
|
127
|
+
webauthnResponse: body.webauthnResponse,
|
|
128
|
+
});
|
|
129
|
+
if (!valid)
|
|
130
|
+
return { error: 'Invalid verification', status: 401 };
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const cookieOptions = (maxAge) => getAuthCookieOptions(isProd(), runtime.config, maxAge);
|
|
134
|
+
const oauthProviders = runtime.oauth.providers;
|
|
135
|
+
const oauthStateStore = runtime.oauth.stateStore;
|
|
136
|
+
const router = createRouter();
|
|
137
|
+
const storeOAuthState = (state, codeVerifier, linkUserId) => oauthStateStore.store(state, codeVerifier, linkUserId);
|
|
138
|
+
const consumeOAuthState = (state) => oauthStateStore.consume(state);
|
|
139
|
+
const getGoogle = () => {
|
|
140
|
+
if (!oauthProviders.google)
|
|
141
|
+
throw new Error('Google OAuth not configured');
|
|
142
|
+
return oauthProviders.google;
|
|
143
|
+
};
|
|
144
|
+
const getApple = () => {
|
|
145
|
+
if (!oauthProviders.apple)
|
|
146
|
+
throw new Error('Apple OAuth not configured');
|
|
147
|
+
return oauthProviders.apple;
|
|
148
|
+
};
|
|
149
|
+
const getMicrosoft = () => {
|
|
150
|
+
if (!oauthProviders.microsoft)
|
|
151
|
+
throw new Error('Microsoft Entra ID OAuth not configured');
|
|
152
|
+
return oauthProviders.microsoft;
|
|
153
|
+
};
|
|
154
|
+
const getGitHub = () => {
|
|
155
|
+
if (!oauthProviders.github)
|
|
156
|
+
throw new Error('GitHub OAuth not configured');
|
|
157
|
+
return oauthProviders.github;
|
|
158
|
+
};
|
|
159
|
+
const getLinkedIn = () => {
|
|
160
|
+
if (!oauthProviders.linkedin)
|
|
161
|
+
throw new Error('LinkedIn OAuth not configured');
|
|
162
|
+
return oauthProviders.linkedin;
|
|
163
|
+
};
|
|
164
|
+
const getTwitter = () => {
|
|
165
|
+
if (!oauthProviders.twitter)
|
|
166
|
+
throw new Error('Twitter OAuth not configured');
|
|
167
|
+
return oauthProviders.twitter;
|
|
168
|
+
};
|
|
169
|
+
const getGitLab = () => {
|
|
170
|
+
if (!oauthProviders.gitlab)
|
|
171
|
+
throw new Error('GitLab OAuth not configured');
|
|
172
|
+
return oauthProviders.gitlab;
|
|
173
|
+
};
|
|
174
|
+
const getSlack = () => {
|
|
175
|
+
if (!oauthProviders.slack)
|
|
176
|
+
throw new Error('Slack OAuth not configured');
|
|
177
|
+
return oauthProviders.slack;
|
|
178
|
+
};
|
|
179
|
+
const getBitbucket = () => {
|
|
180
|
+
if (!oauthProviders.bitbucket)
|
|
181
|
+
throw new Error('Bitbucket OAuth not configured');
|
|
182
|
+
return oauthProviders.bitbucket;
|
|
183
|
+
};
|
|
184
|
+
// ─── Google ───────────────────────────────────────────────────────────────
|
|
185
|
+
if (providers.includes('google')) {
|
|
186
|
+
router.openapi(createRoute({
|
|
187
|
+
method: 'get',
|
|
188
|
+
path: '/auth/google',
|
|
189
|
+
summary: 'Initiate Google OAuth',
|
|
190
|
+
description: "Redirects the user to Google's consent screen to begin the OAuth login flow. After the user authorizes, Google redirects back to `/auth/google/callback`.",
|
|
191
|
+
tags,
|
|
192
|
+
responses: {
|
|
193
|
+
302: { description: "Redirect to Google's OAuth consent screen." },
|
|
194
|
+
500: {
|
|
195
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
196
|
+
description: 'OAuth provider not configured.',
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}), async (c) => {
|
|
200
|
+
const state = generateState();
|
|
201
|
+
const codeVerifier = generateCodeVerifier();
|
|
202
|
+
await storeOAuthState(state, codeVerifier);
|
|
203
|
+
const url = getGoogle().createAuthorizationURL(state, codeVerifier, [
|
|
204
|
+
'openid',
|
|
205
|
+
'profile',
|
|
206
|
+
'email',
|
|
207
|
+
]);
|
|
208
|
+
return c.redirect(url.toString());
|
|
209
|
+
});
|
|
210
|
+
router.openapi(createRoute({
|
|
211
|
+
method: 'get',
|
|
212
|
+
path: '/auth/google/callback',
|
|
213
|
+
summary: 'Google OAuth callback',
|
|
214
|
+
description: 'Handles the redirect from Google after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
215
|
+
tags,
|
|
216
|
+
request: {
|
|
217
|
+
query: z.object({
|
|
218
|
+
code: z.string().describe('Authorization code from Google.'),
|
|
219
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
responses: {
|
|
223
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
224
|
+
400: {
|
|
225
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
226
|
+
description: 'Invalid callback parameters or expired state.',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}), async (c) => {
|
|
230
|
+
const { code, state } = c.req.valid('query');
|
|
231
|
+
if (!code || !state)
|
|
232
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
233
|
+
const stored = await consumeOAuthState(state);
|
|
234
|
+
if (!stored?.codeVerifier)
|
|
235
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
236
|
+
const tokens = await getGoogle().validateAuthorizationCode(code, stored.codeVerifier);
|
|
237
|
+
const info = (await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
238
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
239
|
+
}).then(r => r.json()));
|
|
240
|
+
if (stored.linkUserId) {
|
|
241
|
+
if (!adapter.linkProvider)
|
|
242
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
243
|
+
await adapter.linkProvider(stored.linkUserId, 'google', info.sub);
|
|
244
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
245
|
+
userId: stored.linkUserId,
|
|
246
|
+
meta: { provider: 'google' },
|
|
247
|
+
});
|
|
248
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
249
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=google`);
|
|
250
|
+
}
|
|
251
|
+
return finishOAuth(c, runtime, 'google', info.sub, { email: info.email, name: info.name, avatarUrl: info.picture }, postLoginRedirect);
|
|
252
|
+
});
|
|
253
|
+
router.use('/auth/google/link', userAuth);
|
|
254
|
+
router.openapi(withSecurity(createRoute({
|
|
255
|
+
method: 'get',
|
|
256
|
+
path: '/auth/google/link',
|
|
257
|
+
summary: 'Link Google account',
|
|
258
|
+
description: "Initiates an OAuth flow to link a Google account to the authenticated user. Requires a valid session. Redirects to Google's consent screen.",
|
|
259
|
+
tags,
|
|
260
|
+
responses: {
|
|
261
|
+
302: { description: "Redirect to Google's OAuth consent screen." },
|
|
262
|
+
401: {
|
|
263
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
264
|
+
description: 'No valid session.',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
268
|
+
const state = generateState();
|
|
269
|
+
const codeVerifier = generateCodeVerifier();
|
|
270
|
+
await storeOAuthState(state, codeVerifier, c.get('authUserId'));
|
|
271
|
+
const url = getGoogle().createAuthorizationURL(state, codeVerifier, [
|
|
272
|
+
'openid',
|
|
273
|
+
'profile',
|
|
274
|
+
'email',
|
|
275
|
+
]);
|
|
276
|
+
return c.redirect(url.toString());
|
|
277
|
+
});
|
|
278
|
+
router.openapi(withSecurity(createRoute({
|
|
279
|
+
method: 'delete',
|
|
280
|
+
path: '/auth/google/link',
|
|
281
|
+
summary: 'Unlink Google account',
|
|
282
|
+
description: 'Removes the linked Google OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
283
|
+
tags,
|
|
284
|
+
request: {
|
|
285
|
+
body: {
|
|
286
|
+
required: false,
|
|
287
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
288
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
responses: {
|
|
292
|
+
204: { description: 'Google account unlinked successfully.' },
|
|
293
|
+
400: {
|
|
294
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
295
|
+
description: 'Verification is required but not provided.',
|
|
296
|
+
},
|
|
297
|
+
401: {
|
|
298
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
299
|
+
description: 'No valid session or invalid verification.',
|
|
300
|
+
},
|
|
301
|
+
429: {
|
|
302
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
303
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
304
|
+
},
|
|
305
|
+
500: {
|
|
306
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
307
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
311
|
+
if (!adapter.unlinkProvider) {
|
|
312
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
313
|
+
}
|
|
314
|
+
const userId = c.get('authUserId');
|
|
315
|
+
const sessionId = c.get('sessionId');
|
|
316
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
317
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
318
|
+
}
|
|
319
|
+
const body = c.req.valid('json') ?? {};
|
|
320
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
321
|
+
if (unlinkErr)
|
|
322
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
323
|
+
await adapter.unlinkProvider(userId, 'google');
|
|
324
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'google' } });
|
|
325
|
+
return c.body(null, 204);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// ─── Apple ────────────────────────────────────────────────────────────────
|
|
329
|
+
if (providers.includes('apple')) {
|
|
330
|
+
router.openapi(createRoute({
|
|
331
|
+
method: 'get',
|
|
332
|
+
path: '/auth/apple',
|
|
333
|
+
summary: 'Initiate Apple OAuth',
|
|
334
|
+
description: "Redirects the user to Apple's sign-in page to begin the OAuth login flow. After the user authorizes, Apple posts back to `/auth/apple/callback`.",
|
|
335
|
+
tags,
|
|
336
|
+
responses: {
|
|
337
|
+
302: { description: "Redirect to Apple's OAuth sign-in page." },
|
|
338
|
+
500: {
|
|
339
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
340
|
+
description: 'OAuth provider not configured.',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
}), async (c) => {
|
|
344
|
+
const state = generateState();
|
|
345
|
+
await storeOAuthState(state);
|
|
346
|
+
const url = getApple().createAuthorizationURL(state, ['name', 'email']);
|
|
347
|
+
return c.redirect(url.toString());
|
|
348
|
+
});
|
|
349
|
+
// Apple sends a POST with form data to the callback URL
|
|
350
|
+
router.openapi(createRoute({
|
|
351
|
+
method: 'post',
|
|
352
|
+
path: '/auth/apple/callback',
|
|
353
|
+
summary: 'Apple OAuth callback',
|
|
354
|
+
description: 'Handles the POST redirect from Apple after user authorization. Apple sends form-encoded data containing the authorization code and state. Validates the OAuth state, exchanges the code for tokens, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
355
|
+
tags,
|
|
356
|
+
responses: {
|
|
357
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
358
|
+
400: {
|
|
359
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
360
|
+
description: 'Invalid callback parameters or expired state.',
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}), async (c) => {
|
|
364
|
+
const form = await c.req.formData();
|
|
365
|
+
const code = form.get('code');
|
|
366
|
+
const state = form.get('state');
|
|
367
|
+
if (!code || !state)
|
|
368
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
369
|
+
const stored = await consumeOAuthState(state);
|
|
370
|
+
if (!stored)
|
|
371
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
372
|
+
const tokens = await getApple().validateAuthorizationCode(code);
|
|
373
|
+
const claims = decodeIdToken(tokens.idToken());
|
|
374
|
+
if (stored.linkUserId) {
|
|
375
|
+
if (!adapter.linkProvider)
|
|
376
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
377
|
+
await adapter.linkProvider(stored.linkUserId, 'apple', claims.sub);
|
|
378
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
379
|
+
userId: stored.linkUserId,
|
|
380
|
+
meta: { provider: 'apple' },
|
|
381
|
+
});
|
|
382
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
383
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=apple`);
|
|
384
|
+
}
|
|
385
|
+
// Apple only sends name on the very first sign-in
|
|
386
|
+
const userJSON = form.get('user');
|
|
387
|
+
const userInfo = userJSON
|
|
388
|
+
? JSON.parse(userJSON)
|
|
389
|
+
: {};
|
|
390
|
+
const name = userInfo.name
|
|
391
|
+
? `${userInfo.name.firstName ?? ''} ${userInfo.name.lastName ?? ''}`.trim() || undefined
|
|
392
|
+
: undefined;
|
|
393
|
+
return finishOAuth(c, runtime, 'apple', claims.sub, { email: claims.email, name }, postLoginRedirect);
|
|
394
|
+
});
|
|
395
|
+
router.use('/auth/apple/link', userAuth);
|
|
396
|
+
router.openapi(withSecurity(createRoute({
|
|
397
|
+
method: 'get',
|
|
398
|
+
path: '/auth/apple/link',
|
|
399
|
+
summary: 'Link Apple account',
|
|
400
|
+
description: "Initiates an OAuth flow to link an Apple account to the authenticated user. Requires a valid session. Redirects to Apple's sign-in page.",
|
|
401
|
+
tags,
|
|
402
|
+
responses: {
|
|
403
|
+
302: { description: "Redirect to Apple's OAuth sign-in page." },
|
|
404
|
+
401: {
|
|
405
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
406
|
+
description: 'No valid session.',
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
410
|
+
const state = generateState();
|
|
411
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
412
|
+
const url = getApple().createAuthorizationURL(state, ['name', 'email']);
|
|
413
|
+
return c.redirect(url.toString());
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// ─── Microsoft ──────────────────────────────────────────────────────────
|
|
417
|
+
if (providers.includes('microsoft')) {
|
|
418
|
+
router.openapi(createRoute({
|
|
419
|
+
method: 'get',
|
|
420
|
+
path: '/auth/microsoft',
|
|
421
|
+
summary: 'Initiate Microsoft OAuth',
|
|
422
|
+
description: "Redirects the user to Microsoft's sign-in page to begin the OAuth login flow. After the user authorizes, Microsoft redirects back to `/auth/microsoft/callback`.",
|
|
423
|
+
tags,
|
|
424
|
+
responses: {
|
|
425
|
+
302: { description: "Redirect to Microsoft's OAuth sign-in page." },
|
|
426
|
+
500: {
|
|
427
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
428
|
+
description: 'OAuth provider not configured.',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
}), async (c) => {
|
|
432
|
+
const state = generateState();
|
|
433
|
+
const codeVerifier = generateCodeVerifier();
|
|
434
|
+
await storeOAuthState(state, codeVerifier);
|
|
435
|
+
const url = getMicrosoft().createAuthorizationURL(state, codeVerifier, [
|
|
436
|
+
'openid',
|
|
437
|
+
'profile',
|
|
438
|
+
'email',
|
|
439
|
+
]);
|
|
440
|
+
return c.redirect(url.toString());
|
|
441
|
+
});
|
|
442
|
+
router.openapi(createRoute({
|
|
443
|
+
method: 'get',
|
|
444
|
+
path: '/auth/microsoft/callback',
|
|
445
|
+
summary: 'Microsoft OAuth callback',
|
|
446
|
+
description: 'Handles the redirect from Microsoft after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
447
|
+
tags,
|
|
448
|
+
request: {
|
|
449
|
+
query: z.object({
|
|
450
|
+
code: z.string().describe('Authorization code from Microsoft.'),
|
|
451
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
452
|
+
}),
|
|
453
|
+
},
|
|
454
|
+
responses: {
|
|
455
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
456
|
+
400: {
|
|
457
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
458
|
+
description: 'Invalid callback parameters or expired state.',
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
}), async (c) => {
|
|
462
|
+
const { code, state } = c.req.valid('query');
|
|
463
|
+
if (!code || !state)
|
|
464
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
465
|
+
const stored = await consumeOAuthState(state);
|
|
466
|
+
if (!stored?.codeVerifier)
|
|
467
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
468
|
+
const tokens = await getMicrosoft().validateAuthorizationCode(code, stored.codeVerifier);
|
|
469
|
+
const info = (await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
470
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
471
|
+
}).then(r => r.json()));
|
|
472
|
+
if (stored.linkUserId) {
|
|
473
|
+
if (!adapter.linkProvider)
|
|
474
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
475
|
+
await adapter.linkProvider(stored.linkUserId, 'microsoft', info.id);
|
|
476
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
477
|
+
userId: stored.linkUserId,
|
|
478
|
+
meta: { provider: 'microsoft' },
|
|
479
|
+
});
|
|
480
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
481
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=microsoft`);
|
|
482
|
+
}
|
|
483
|
+
return finishOAuth(c, runtime, 'microsoft', info.id, { email: info.mail ?? info.userPrincipalName, name: info.displayName }, postLoginRedirect);
|
|
484
|
+
});
|
|
485
|
+
router.use('/auth/microsoft/link', userAuth);
|
|
486
|
+
router.openapi(withSecurity(createRoute({
|
|
487
|
+
method: 'get',
|
|
488
|
+
path: '/auth/microsoft/link',
|
|
489
|
+
summary: 'Link Microsoft account',
|
|
490
|
+
description: "Initiates an OAuth flow to link a Microsoft account to the authenticated user. Requires a valid session. Redirects to Microsoft's sign-in page.",
|
|
491
|
+
tags,
|
|
492
|
+
responses: {
|
|
493
|
+
302: { description: "Redirect to Microsoft's OAuth sign-in page." },
|
|
494
|
+
401: {
|
|
495
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
496
|
+
description: 'No valid session.',
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
500
|
+
const state = generateState();
|
|
501
|
+
const codeVerifier = generateCodeVerifier();
|
|
502
|
+
await storeOAuthState(state, codeVerifier, c.get('authUserId'));
|
|
503
|
+
const url = getMicrosoft().createAuthorizationURL(state, codeVerifier, [
|
|
504
|
+
'openid',
|
|
505
|
+
'profile',
|
|
506
|
+
'email',
|
|
507
|
+
]);
|
|
508
|
+
return c.redirect(url.toString());
|
|
509
|
+
});
|
|
510
|
+
router.openapi(withSecurity(createRoute({
|
|
511
|
+
method: 'delete',
|
|
512
|
+
path: '/auth/microsoft/link',
|
|
513
|
+
summary: 'Unlink Microsoft account',
|
|
514
|
+
description: 'Removes the linked Microsoft OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
515
|
+
tags,
|
|
516
|
+
request: {
|
|
517
|
+
body: {
|
|
518
|
+
required: false,
|
|
519
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
520
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
responses: {
|
|
524
|
+
204: { description: 'Microsoft account unlinked successfully.' },
|
|
525
|
+
400: {
|
|
526
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
527
|
+
description: 'Verification is required but not provided.',
|
|
528
|
+
},
|
|
529
|
+
401: {
|
|
530
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
531
|
+
description: 'No valid session or invalid verification.',
|
|
532
|
+
},
|
|
533
|
+
429: {
|
|
534
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
535
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
536
|
+
},
|
|
537
|
+
500: {
|
|
538
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
539
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
543
|
+
if (!adapter.unlinkProvider) {
|
|
544
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
545
|
+
}
|
|
546
|
+
const userId = c.get('authUserId');
|
|
547
|
+
const sessionId = c.get('sessionId');
|
|
548
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
549
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
550
|
+
}
|
|
551
|
+
const body = c.req.valid('json') ?? {};
|
|
552
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
553
|
+
if (unlinkErr)
|
|
554
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
555
|
+
await adapter.unlinkProvider(userId, 'microsoft');
|
|
556
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'microsoft' } });
|
|
557
|
+
return c.body(null, 204);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
// ─── GitHub ────────────────────────────────────────────────────────────
|
|
561
|
+
if (providers.includes('github')) {
|
|
562
|
+
router.openapi(createRoute({
|
|
563
|
+
method: 'get',
|
|
564
|
+
path: '/auth/github',
|
|
565
|
+
summary: 'Initiate GitHub OAuth',
|
|
566
|
+
description: "Redirects the user to GitHub's authorization page to begin the OAuth login flow. After the user authorizes, GitHub redirects back to `/auth/github/callback`.",
|
|
567
|
+
tags,
|
|
568
|
+
responses: {
|
|
569
|
+
302: { description: "Redirect to GitHub's OAuth authorization page." },
|
|
570
|
+
500: {
|
|
571
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
572
|
+
description: 'OAuth provider not configured.',
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
}), async (c) => {
|
|
576
|
+
const state = generateState();
|
|
577
|
+
await storeOAuthState(state);
|
|
578
|
+
const url = getGitHub().createAuthorizationURL(state, ['read:user', 'user:email']);
|
|
579
|
+
return c.redirect(url.toString());
|
|
580
|
+
});
|
|
581
|
+
router.openapi(createRoute({
|
|
582
|
+
method: 'get',
|
|
583
|
+
path: '/auth/github/callback',
|
|
584
|
+
summary: 'GitHub OAuth callback',
|
|
585
|
+
description: 'Handles the redirect from GitHub after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
586
|
+
tags,
|
|
587
|
+
request: {
|
|
588
|
+
query: z.object({
|
|
589
|
+
code: z.string().describe('Authorization code from GitHub.'),
|
|
590
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
591
|
+
}),
|
|
592
|
+
},
|
|
593
|
+
responses: {
|
|
594
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
595
|
+
400: {
|
|
596
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
597
|
+
description: 'Invalid callback parameters or expired state.',
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
}), async (c) => {
|
|
601
|
+
const { code, state } = c.req.valid('query');
|
|
602
|
+
if (!code || !state)
|
|
603
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
604
|
+
const stored = await consumeOAuthState(state);
|
|
605
|
+
if (!stored)
|
|
606
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
607
|
+
const tokens = await getGitHub().validateAuthorizationCode(code);
|
|
608
|
+
const headers = {
|
|
609
|
+
Authorization: `Bearer ${tokens.accessToken()}`,
|
|
610
|
+
'User-Agent': 'bunshot',
|
|
611
|
+
};
|
|
612
|
+
const info = (await fetch('https://api.github.com/user', { headers }).then(r => r.json()));
|
|
613
|
+
// GitHub may not return email on /user if it's private — fetch from /user/emails
|
|
614
|
+
let email = info.email;
|
|
615
|
+
if (!email) {
|
|
616
|
+
const emails = (await fetch('https://api.github.com/user/emails', { headers }).then(r => r.json()));
|
|
617
|
+
email =
|
|
618
|
+
emails.find(e => e.primary && e.verified)?.email ?? emails.find(e => e.verified)?.email;
|
|
619
|
+
}
|
|
620
|
+
if (stored.linkUserId) {
|
|
621
|
+
if (!adapter.linkProvider)
|
|
622
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
623
|
+
await adapter.linkProvider(stored.linkUserId, 'github', String(info.id));
|
|
624
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
625
|
+
userId: stored.linkUserId,
|
|
626
|
+
meta: { provider: 'github' },
|
|
627
|
+
});
|
|
628
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
629
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=github`);
|
|
630
|
+
}
|
|
631
|
+
return finishOAuth(c, runtime, 'github', String(info.id), { email, name: info.name, avatarUrl: info.avatar_url }, postLoginRedirect);
|
|
632
|
+
});
|
|
633
|
+
router.use('/auth/github/link', userAuth);
|
|
634
|
+
router.openapi(withSecurity(createRoute({
|
|
635
|
+
method: 'get',
|
|
636
|
+
path: '/auth/github/link',
|
|
637
|
+
summary: 'Link GitHub account',
|
|
638
|
+
description: "Initiates an OAuth flow to link a GitHub account to the authenticated user. Requires a valid session. Redirects to GitHub's authorization page.",
|
|
639
|
+
tags,
|
|
640
|
+
responses: {
|
|
641
|
+
302: { description: "Redirect to GitHub's OAuth authorization page." },
|
|
642
|
+
401: {
|
|
643
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
644
|
+
description: 'No valid session.',
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
648
|
+
const state = generateState();
|
|
649
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
650
|
+
const url = getGitHub().createAuthorizationURL(state, ['read:user', 'user:email']);
|
|
651
|
+
return c.redirect(url.toString());
|
|
652
|
+
});
|
|
653
|
+
router.openapi(withSecurity(createRoute({
|
|
654
|
+
method: 'delete',
|
|
655
|
+
path: '/auth/github/link',
|
|
656
|
+
summary: 'Unlink GitHub account',
|
|
657
|
+
description: 'Removes the linked GitHub OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
658
|
+
tags,
|
|
659
|
+
request: {
|
|
660
|
+
body: {
|
|
661
|
+
required: false,
|
|
662
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
663
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
responses: {
|
|
667
|
+
204: { description: 'GitHub account unlinked successfully.' },
|
|
668
|
+
400: {
|
|
669
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
670
|
+
description: 'Verification is required but not provided.',
|
|
671
|
+
},
|
|
672
|
+
401: {
|
|
673
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
674
|
+
description: 'No valid session or invalid verification.',
|
|
675
|
+
},
|
|
676
|
+
429: {
|
|
677
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
678
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
679
|
+
},
|
|
680
|
+
500: {
|
|
681
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
682
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
686
|
+
if (!adapter.unlinkProvider) {
|
|
687
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
688
|
+
}
|
|
689
|
+
const userId = c.get('authUserId');
|
|
690
|
+
const sessionId = c.get('sessionId');
|
|
691
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
692
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
693
|
+
}
|
|
694
|
+
const body = c.req.valid('json') ?? {};
|
|
695
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
696
|
+
if (unlinkErr)
|
|
697
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
698
|
+
await adapter.unlinkProvider(userId, 'github');
|
|
699
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'github' } });
|
|
700
|
+
return c.body(null, 204);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
// ─── LinkedIn ────────────────────────────────────────────────────────────
|
|
704
|
+
if (providers.includes('linkedin')) {
|
|
705
|
+
router.openapi(createRoute({
|
|
706
|
+
method: 'get',
|
|
707
|
+
path: '/auth/linkedin',
|
|
708
|
+
summary: 'Initiate LinkedIn OAuth',
|
|
709
|
+
description: "Redirects the user to LinkedIn's authorization page to begin the OAuth login flow. After the user authorizes, LinkedIn redirects back to `/auth/linkedin/callback`.",
|
|
710
|
+
tags,
|
|
711
|
+
responses: {
|
|
712
|
+
302: { description: "Redirect to LinkedIn's OAuth authorization page." },
|
|
713
|
+
500: {
|
|
714
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
715
|
+
description: 'OAuth provider not configured.',
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
}), async (c) => {
|
|
719
|
+
const state = generateState();
|
|
720
|
+
await storeOAuthState(state);
|
|
721
|
+
const url = getLinkedIn().createAuthorizationURL(state, ['openid', 'profile', 'email']);
|
|
722
|
+
return c.redirect(url.toString());
|
|
723
|
+
});
|
|
724
|
+
router.openapi(createRoute({
|
|
725
|
+
method: 'get',
|
|
726
|
+
path: '/auth/linkedin/callback',
|
|
727
|
+
summary: 'LinkedIn OAuth callback',
|
|
728
|
+
description: 'Handles the redirect from LinkedIn after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
729
|
+
tags,
|
|
730
|
+
request: {
|
|
731
|
+
query: z.object({
|
|
732
|
+
code: z.string().describe('Authorization code from LinkedIn.'),
|
|
733
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
734
|
+
}),
|
|
735
|
+
},
|
|
736
|
+
responses: {
|
|
737
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
738
|
+
400: {
|
|
739
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
740
|
+
description: 'Invalid callback parameters or expired state.',
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
}), async (c) => {
|
|
744
|
+
const { code, state } = c.req.valid('query');
|
|
745
|
+
if (!code || !state)
|
|
746
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
747
|
+
const stored = await consumeOAuthState(state);
|
|
748
|
+
if (!stored)
|
|
749
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
750
|
+
const tokens = await getLinkedIn().validateAuthorizationCode(code);
|
|
751
|
+
const info = (await fetch('https://api.linkedin.com/v2/userinfo', {
|
|
752
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
753
|
+
}).then(r => r.json()));
|
|
754
|
+
if (stored.linkUserId) {
|
|
755
|
+
if (!adapter.linkProvider)
|
|
756
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
757
|
+
await adapter.linkProvider(stored.linkUserId, 'linkedin', info.sub);
|
|
758
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
759
|
+
userId: stored.linkUserId,
|
|
760
|
+
meta: { provider: 'linkedin' },
|
|
761
|
+
});
|
|
762
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
763
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=linkedin`);
|
|
764
|
+
}
|
|
765
|
+
return finishOAuth(c, runtime, 'linkedin', info.sub, { email: info.email, name: info.name, avatarUrl: info.picture }, postLoginRedirect);
|
|
766
|
+
});
|
|
767
|
+
router.use('/auth/linkedin/link', userAuth);
|
|
768
|
+
router.openapi(withSecurity(createRoute({
|
|
769
|
+
method: 'get',
|
|
770
|
+
path: '/auth/linkedin/link',
|
|
771
|
+
summary: 'Link LinkedIn account',
|
|
772
|
+
description: "Initiates an OAuth flow to link a LinkedIn account to the authenticated user. Requires a valid session. Redirects to LinkedIn's authorization page.",
|
|
773
|
+
tags,
|
|
774
|
+
responses: {
|
|
775
|
+
302: { description: "Redirect to LinkedIn's OAuth authorization page." },
|
|
776
|
+
401: {
|
|
777
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
778
|
+
description: 'No valid session.',
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
782
|
+
const state = generateState();
|
|
783
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
784
|
+
const url = getLinkedIn().createAuthorizationURL(state, ['openid', 'profile', 'email']);
|
|
785
|
+
return c.redirect(url.toString());
|
|
786
|
+
});
|
|
787
|
+
router.openapi(withSecurity(createRoute({
|
|
788
|
+
method: 'delete',
|
|
789
|
+
path: '/auth/linkedin/link',
|
|
790
|
+
summary: 'Unlink LinkedIn account',
|
|
791
|
+
description: 'Removes the linked LinkedIn OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
792
|
+
tags,
|
|
793
|
+
request: {
|
|
794
|
+
body: {
|
|
795
|
+
required: false,
|
|
796
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
797
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
responses: {
|
|
801
|
+
204: { description: 'LinkedIn account unlinked successfully.' },
|
|
802
|
+
400: {
|
|
803
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
804
|
+
description: 'Verification is required but not provided.',
|
|
805
|
+
},
|
|
806
|
+
401: {
|
|
807
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
808
|
+
description: 'No valid session or invalid verification.',
|
|
809
|
+
},
|
|
810
|
+
429: {
|
|
811
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
812
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
813
|
+
},
|
|
814
|
+
500: {
|
|
815
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
816
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
820
|
+
if (!adapter.unlinkProvider) {
|
|
821
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
822
|
+
}
|
|
823
|
+
const userId = c.get('authUserId');
|
|
824
|
+
const sessionId = c.get('sessionId');
|
|
825
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
826
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
827
|
+
}
|
|
828
|
+
const body = c.req.valid('json') ?? {};
|
|
829
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
830
|
+
if (unlinkErr)
|
|
831
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
832
|
+
await adapter.unlinkProvider(userId, 'linkedin');
|
|
833
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'linkedin' } });
|
|
834
|
+
return c.body(null, 204);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
// ─── Twitter/X ───────────────────────────────────────────────────────────
|
|
838
|
+
if (providers.includes('twitter')) {
|
|
839
|
+
router.openapi(createRoute({
|
|
840
|
+
method: 'get',
|
|
841
|
+
path: '/auth/twitter',
|
|
842
|
+
summary: 'Initiate Twitter/X OAuth',
|
|
843
|
+
description: "Redirects the user to Twitter/X's authorization page to begin the OAuth login flow. After the user authorizes, Twitter redirects back to `/auth/twitter/callback`.",
|
|
844
|
+
tags,
|
|
845
|
+
responses: {
|
|
846
|
+
302: { description: "Redirect to Twitter/X's OAuth authorization page." },
|
|
847
|
+
500: {
|
|
848
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
849
|
+
description: 'OAuth provider not configured.',
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
}), async (c) => {
|
|
853
|
+
const state = generateState();
|
|
854
|
+
const codeVerifier = generateCodeVerifier();
|
|
855
|
+
await storeOAuthState(state, codeVerifier);
|
|
856
|
+
const url = getTwitter().createAuthorizationURL(state, codeVerifier, [
|
|
857
|
+
'tweet.read',
|
|
858
|
+
'users.read',
|
|
859
|
+
]);
|
|
860
|
+
return c.redirect(url.toString());
|
|
861
|
+
});
|
|
862
|
+
router.openapi(createRoute({
|
|
863
|
+
method: 'get',
|
|
864
|
+
path: '/auth/twitter/callback',
|
|
865
|
+
summary: 'Twitter/X OAuth callback',
|
|
866
|
+
description: 'Handles the redirect from Twitter/X after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
867
|
+
tags,
|
|
868
|
+
request: {
|
|
869
|
+
query: z.object({
|
|
870
|
+
code: z.string().describe('Authorization code from Twitter/X.'),
|
|
871
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
872
|
+
}),
|
|
873
|
+
},
|
|
874
|
+
responses: {
|
|
875
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
876
|
+
400: {
|
|
877
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
878
|
+
description: 'Invalid callback parameters or expired state.',
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
}), async (c) => {
|
|
882
|
+
const { code, state } = c.req.valid('query');
|
|
883
|
+
if (!code || !state)
|
|
884
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
885
|
+
const stored = await consumeOAuthState(state);
|
|
886
|
+
if (!stored?.codeVerifier)
|
|
887
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
888
|
+
const tokens = await getTwitter().validateAuthorizationCode(code, stored.codeVerifier);
|
|
889
|
+
const info = (await fetch('https://api.twitter.com/2/users/me?user.fields=name,profile_image_url', {
|
|
890
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
891
|
+
}).then(r => r.json()));
|
|
892
|
+
const user = info.data;
|
|
893
|
+
if (!user?.id)
|
|
894
|
+
return c.json({ error: 'Failed to retrieve Twitter user info' }, 400);
|
|
895
|
+
if (stored.linkUserId) {
|
|
896
|
+
if (!adapter.linkProvider)
|
|
897
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
898
|
+
await adapter.linkProvider(stored.linkUserId, 'twitter', user.id);
|
|
899
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
900
|
+
userId: stored.linkUserId,
|
|
901
|
+
meta: { provider: 'twitter' },
|
|
902
|
+
});
|
|
903
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
904
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=twitter`);
|
|
905
|
+
}
|
|
906
|
+
return finishOAuth(c, runtime, 'twitter', user.id, { name: user.name, avatarUrl: user.profile_image_url }, postLoginRedirect);
|
|
907
|
+
});
|
|
908
|
+
router.use('/auth/twitter/link', userAuth);
|
|
909
|
+
router.openapi(withSecurity(createRoute({
|
|
910
|
+
method: 'get',
|
|
911
|
+
path: '/auth/twitter/link',
|
|
912
|
+
summary: 'Link Twitter/X account',
|
|
913
|
+
description: "Initiates an OAuth flow to link a Twitter/X account to the authenticated user. Requires a valid session. Redirects to Twitter/X's authorization page.",
|
|
914
|
+
tags,
|
|
915
|
+
responses: {
|
|
916
|
+
302: { description: "Redirect to Twitter/X's OAuth authorization page." },
|
|
917
|
+
401: {
|
|
918
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
919
|
+
description: 'No valid session.',
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
923
|
+
const state = generateState();
|
|
924
|
+
const codeVerifier = generateCodeVerifier();
|
|
925
|
+
await storeOAuthState(state, codeVerifier, c.get('authUserId'));
|
|
926
|
+
const url = getTwitter().createAuthorizationURL(state, codeVerifier, [
|
|
927
|
+
'tweet.read',
|
|
928
|
+
'users.read',
|
|
929
|
+
]);
|
|
930
|
+
return c.redirect(url.toString());
|
|
931
|
+
});
|
|
932
|
+
router.openapi(withSecurity(createRoute({
|
|
933
|
+
method: 'delete',
|
|
934
|
+
path: '/auth/twitter/link',
|
|
935
|
+
summary: 'Unlink Twitter/X account',
|
|
936
|
+
description: 'Removes the linked Twitter/X OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
937
|
+
tags,
|
|
938
|
+
request: {
|
|
939
|
+
body: {
|
|
940
|
+
required: false,
|
|
941
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
942
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
responses: {
|
|
946
|
+
204: { description: 'Twitter/X account unlinked successfully.' },
|
|
947
|
+
400: {
|
|
948
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
949
|
+
description: 'Verification is required but not provided.',
|
|
950
|
+
},
|
|
951
|
+
401: {
|
|
952
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
953
|
+
description: 'No valid session or invalid verification.',
|
|
954
|
+
},
|
|
955
|
+
429: {
|
|
956
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
957
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
958
|
+
},
|
|
959
|
+
500: {
|
|
960
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
961
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
965
|
+
if (!adapter.unlinkProvider) {
|
|
966
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
967
|
+
}
|
|
968
|
+
const userId = c.get('authUserId');
|
|
969
|
+
const sessionId = c.get('sessionId');
|
|
970
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
971
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
972
|
+
}
|
|
973
|
+
const body = c.req.valid('json') ?? {};
|
|
974
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
975
|
+
if (unlinkErr)
|
|
976
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
977
|
+
await adapter.unlinkProvider(userId, 'twitter');
|
|
978
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'twitter' } });
|
|
979
|
+
return c.body(null, 204);
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
// ─── GitLab ──────────────────────────────────────────────────────────────
|
|
983
|
+
if (providers.includes('gitlab')) {
|
|
984
|
+
router.openapi(createRoute({
|
|
985
|
+
method: 'get',
|
|
986
|
+
path: '/auth/gitlab',
|
|
987
|
+
summary: 'Initiate GitLab OAuth',
|
|
988
|
+
description: "Redirects the user to GitLab's authorization page to begin the OAuth login flow. After the user authorizes, GitLab redirects back to `/auth/gitlab/callback`.",
|
|
989
|
+
tags,
|
|
990
|
+
responses: {
|
|
991
|
+
302: { description: "Redirect to GitLab's OAuth authorization page." },
|
|
992
|
+
500: {
|
|
993
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
994
|
+
description: 'OAuth provider not configured.',
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
}), async (c) => {
|
|
998
|
+
const state = generateState();
|
|
999
|
+
await storeOAuthState(state);
|
|
1000
|
+
const url = getGitLab().createAuthorizationURL(state, ['read_user']);
|
|
1001
|
+
return c.redirect(url.toString());
|
|
1002
|
+
});
|
|
1003
|
+
router.openapi(createRoute({
|
|
1004
|
+
method: 'get',
|
|
1005
|
+
path: '/auth/gitlab/callback',
|
|
1006
|
+
summary: 'GitLab OAuth callback',
|
|
1007
|
+
description: 'Handles the redirect from GitLab after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
1008
|
+
tags,
|
|
1009
|
+
request: {
|
|
1010
|
+
query: z.object({
|
|
1011
|
+
code: z.string().describe('Authorization code from GitLab.'),
|
|
1012
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
1013
|
+
}),
|
|
1014
|
+
},
|
|
1015
|
+
responses: {
|
|
1016
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
1017
|
+
400: {
|
|
1018
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1019
|
+
description: 'Invalid callback parameters or expired state.',
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
}), async (c) => {
|
|
1023
|
+
const { code, state } = c.req.valid('query');
|
|
1024
|
+
if (!code || !state)
|
|
1025
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
1026
|
+
const stored = await consumeOAuthState(state);
|
|
1027
|
+
if (!stored)
|
|
1028
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
1029
|
+
const tokens = await getGitLab().validateAuthorizationCode(code);
|
|
1030
|
+
const info = (await fetch('https://gitlab.com/api/v4/user', {
|
|
1031
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1032
|
+
}).then(r => r.json()));
|
|
1033
|
+
if (stored.linkUserId) {
|
|
1034
|
+
if (!adapter.linkProvider)
|
|
1035
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
1036
|
+
await adapter.linkProvider(stored.linkUserId, 'gitlab', String(info.id));
|
|
1037
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
1038
|
+
userId: stored.linkUserId,
|
|
1039
|
+
meta: { provider: 'gitlab' },
|
|
1040
|
+
});
|
|
1041
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
1042
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=gitlab`);
|
|
1043
|
+
}
|
|
1044
|
+
return finishOAuth(c, runtime, 'gitlab', String(info.id), { email: info.email, name: info.name, avatarUrl: info.avatar_url }, postLoginRedirect);
|
|
1045
|
+
});
|
|
1046
|
+
router.use('/auth/gitlab/link', userAuth);
|
|
1047
|
+
router.openapi(withSecurity(createRoute({
|
|
1048
|
+
method: 'get',
|
|
1049
|
+
path: '/auth/gitlab/link',
|
|
1050
|
+
summary: 'Link GitLab account',
|
|
1051
|
+
description: "Initiates an OAuth flow to link a GitLab account to the authenticated user. Requires a valid session. Redirects to GitLab's authorization page.",
|
|
1052
|
+
tags,
|
|
1053
|
+
responses: {
|
|
1054
|
+
302: { description: "Redirect to GitLab's OAuth authorization page." },
|
|
1055
|
+
401: {
|
|
1056
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1057
|
+
description: 'No valid session.',
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1061
|
+
const state = generateState();
|
|
1062
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
1063
|
+
const url = getGitLab().createAuthorizationURL(state, ['read_user']);
|
|
1064
|
+
return c.redirect(url.toString());
|
|
1065
|
+
});
|
|
1066
|
+
router.openapi(withSecurity(createRoute({
|
|
1067
|
+
method: 'delete',
|
|
1068
|
+
path: '/auth/gitlab/link',
|
|
1069
|
+
summary: 'Unlink GitLab account',
|
|
1070
|
+
description: 'Removes the linked GitLab OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
1071
|
+
tags,
|
|
1072
|
+
request: {
|
|
1073
|
+
body: {
|
|
1074
|
+
required: false,
|
|
1075
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
1076
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
responses: {
|
|
1080
|
+
204: { description: 'GitLab account unlinked successfully.' },
|
|
1081
|
+
400: {
|
|
1082
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1083
|
+
description: 'Verification is required but not provided.',
|
|
1084
|
+
},
|
|
1085
|
+
401: {
|
|
1086
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1087
|
+
description: 'No valid session or invalid verification.',
|
|
1088
|
+
},
|
|
1089
|
+
429: {
|
|
1090
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1091
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
1092
|
+
},
|
|
1093
|
+
500: {
|
|
1094
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1095
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1099
|
+
if (!adapter.unlinkProvider) {
|
|
1100
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
1101
|
+
}
|
|
1102
|
+
const userId = c.get('authUserId');
|
|
1103
|
+
const sessionId = c.get('sessionId');
|
|
1104
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
1105
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
1106
|
+
}
|
|
1107
|
+
const body = c.req.valid('json') ?? {};
|
|
1108
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
1109
|
+
if (unlinkErr)
|
|
1110
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
1111
|
+
await adapter.unlinkProvider(userId, 'gitlab');
|
|
1112
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'gitlab' } });
|
|
1113
|
+
return c.body(null, 204);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
// ─── Slack ───────────────────────────────────────────────────────────────
|
|
1117
|
+
if (providers.includes('slack')) {
|
|
1118
|
+
router.openapi(createRoute({
|
|
1119
|
+
method: 'get',
|
|
1120
|
+
path: '/auth/slack',
|
|
1121
|
+
summary: 'Initiate Slack OAuth',
|
|
1122
|
+
description: "Redirects the user to Slack's authorization page to begin the OAuth login flow. After the user authorizes, Slack redirects back to `/auth/slack/callback`.",
|
|
1123
|
+
tags,
|
|
1124
|
+
responses: {
|
|
1125
|
+
302: { description: "Redirect to Slack's OAuth authorization page." },
|
|
1126
|
+
500: {
|
|
1127
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1128
|
+
description: 'OAuth provider not configured.',
|
|
1129
|
+
},
|
|
1130
|
+
},
|
|
1131
|
+
}), async (c) => {
|
|
1132
|
+
const state = generateState();
|
|
1133
|
+
await storeOAuthState(state);
|
|
1134
|
+
const url = getSlack().createAuthorizationURL(state, ['openid', 'profile', 'email']);
|
|
1135
|
+
return c.redirect(url.toString());
|
|
1136
|
+
});
|
|
1137
|
+
router.openapi(createRoute({
|
|
1138
|
+
method: 'get',
|
|
1139
|
+
path: '/auth/slack/callback',
|
|
1140
|
+
summary: 'Slack OAuth callback',
|
|
1141
|
+
description: 'Handles the redirect from Slack after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
1142
|
+
tags,
|
|
1143
|
+
request: {
|
|
1144
|
+
query: z.object({
|
|
1145
|
+
code: z.string().describe('Authorization code from Slack.'),
|
|
1146
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
1147
|
+
}),
|
|
1148
|
+
},
|
|
1149
|
+
responses: {
|
|
1150
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
1151
|
+
400: {
|
|
1152
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1153
|
+
description: 'Invalid callback parameters or expired state.',
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
}), async (c) => {
|
|
1157
|
+
const { code, state } = c.req.valid('query');
|
|
1158
|
+
if (!code || !state)
|
|
1159
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
1160
|
+
const stored = await consumeOAuthState(state);
|
|
1161
|
+
if (!stored)
|
|
1162
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
1163
|
+
const tokens = await getSlack().validateAuthorizationCode(code);
|
|
1164
|
+
const info = (await fetch('https://slack.com/api/openid.connect.userInfo', {
|
|
1165
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1166
|
+
}).then(r => r.json()));
|
|
1167
|
+
if (stored.linkUserId) {
|
|
1168
|
+
if (!adapter.linkProvider)
|
|
1169
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
1170
|
+
await adapter.linkProvider(stored.linkUserId, 'slack', info.sub);
|
|
1171
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
1172
|
+
userId: stored.linkUserId,
|
|
1173
|
+
meta: { provider: 'slack' },
|
|
1174
|
+
});
|
|
1175
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
1176
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=slack`);
|
|
1177
|
+
}
|
|
1178
|
+
return finishOAuth(c, runtime, 'slack', info.sub, { email: info.email, name: info.name, avatarUrl: info.picture }, postLoginRedirect);
|
|
1179
|
+
});
|
|
1180
|
+
router.use('/auth/slack/link', userAuth);
|
|
1181
|
+
router.openapi(withSecurity(createRoute({
|
|
1182
|
+
method: 'get',
|
|
1183
|
+
path: '/auth/slack/link',
|
|
1184
|
+
summary: 'Link Slack account',
|
|
1185
|
+
description: "Initiates an OAuth flow to link a Slack account to the authenticated user. Requires a valid session. Redirects to Slack's authorization page.",
|
|
1186
|
+
tags,
|
|
1187
|
+
responses: {
|
|
1188
|
+
302: { description: "Redirect to Slack's OAuth authorization page." },
|
|
1189
|
+
401: {
|
|
1190
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1191
|
+
description: 'No valid session.',
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1195
|
+
const state = generateState();
|
|
1196
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
1197
|
+
const url = getSlack().createAuthorizationURL(state, ['openid', 'profile', 'email']);
|
|
1198
|
+
return c.redirect(url.toString());
|
|
1199
|
+
});
|
|
1200
|
+
router.openapi(withSecurity(createRoute({
|
|
1201
|
+
method: 'delete',
|
|
1202
|
+
path: '/auth/slack/link',
|
|
1203
|
+
summary: 'Unlink Slack account',
|
|
1204
|
+
description: 'Removes the linked Slack OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
1205
|
+
tags,
|
|
1206
|
+
request: {
|
|
1207
|
+
body: {
|
|
1208
|
+
required: false,
|
|
1209
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
1210
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
responses: {
|
|
1214
|
+
204: { description: 'Slack account unlinked successfully.' },
|
|
1215
|
+
400: {
|
|
1216
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1217
|
+
description: 'Verification is required but not provided.',
|
|
1218
|
+
},
|
|
1219
|
+
401: {
|
|
1220
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1221
|
+
description: 'No valid session or invalid verification.',
|
|
1222
|
+
},
|
|
1223
|
+
429: {
|
|
1224
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1225
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
1226
|
+
},
|
|
1227
|
+
500: {
|
|
1228
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1229
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1233
|
+
if (!adapter.unlinkProvider) {
|
|
1234
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
1235
|
+
}
|
|
1236
|
+
const userId = c.get('authUserId');
|
|
1237
|
+
const sessionId = c.get('sessionId');
|
|
1238
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
1239
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
1240
|
+
}
|
|
1241
|
+
const body = c.req.valid('json') ?? {};
|
|
1242
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
1243
|
+
if (unlinkErr)
|
|
1244
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
1245
|
+
await adapter.unlinkProvider(userId, 'slack');
|
|
1246
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'slack' } });
|
|
1247
|
+
return c.body(null, 204);
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
// ─── Bitbucket ───────────────────────────────────────────────────────────
|
|
1251
|
+
if (providers.includes('bitbucket')) {
|
|
1252
|
+
router.openapi(createRoute({
|
|
1253
|
+
method: 'get',
|
|
1254
|
+
path: '/auth/bitbucket',
|
|
1255
|
+
summary: 'Initiate Bitbucket OAuth',
|
|
1256
|
+
description: "Redirects the user to Bitbucket's authorization page to begin the OAuth login flow. After the user authorizes, Bitbucket redirects back to `/auth/bitbucket/callback`.",
|
|
1257
|
+
tags,
|
|
1258
|
+
responses: {
|
|
1259
|
+
302: { description: "Redirect to Bitbucket's OAuth authorization page." },
|
|
1260
|
+
500: {
|
|
1261
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1262
|
+
description: 'OAuth provider not configured.',
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
}), async (c) => {
|
|
1266
|
+
const state = generateState();
|
|
1267
|
+
await storeOAuthState(state);
|
|
1268
|
+
const url = getBitbucket().createAuthorizationURL(state);
|
|
1269
|
+
return c.redirect(url.toString());
|
|
1270
|
+
});
|
|
1271
|
+
router.openapi(createRoute({
|
|
1272
|
+
method: 'get',
|
|
1273
|
+
path: '/auth/bitbucket/callback',
|
|
1274
|
+
summary: 'Bitbucket OAuth callback',
|
|
1275
|
+
description: 'Handles the redirect from Bitbucket after user authorization. Validates the OAuth state and code, then creates or finds the user account. Sets a session cookie and redirects to the configured post-login URL.',
|
|
1276
|
+
tags,
|
|
1277
|
+
request: {
|
|
1278
|
+
query: z.object({
|
|
1279
|
+
code: z.string().describe('Authorization code from Bitbucket.'),
|
|
1280
|
+
state: z.string().describe('OAuth state parameter for CSRF protection.'),
|
|
1281
|
+
}),
|
|
1282
|
+
},
|
|
1283
|
+
responses: {
|
|
1284
|
+
302: { description: 'Redirect to the post-login URL with session token.' },
|
|
1285
|
+
400: {
|
|
1286
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1287
|
+
description: 'Invalid callback parameters or expired state.',
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
}), async (c) => {
|
|
1291
|
+
const { code, state } = c.req.valid('query');
|
|
1292
|
+
if (!code || !state)
|
|
1293
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
1294
|
+
const stored = await consumeOAuthState(state);
|
|
1295
|
+
if (!stored)
|
|
1296
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
1297
|
+
const tokens = await getBitbucket().validateAuthorizationCode(code);
|
|
1298
|
+
const info = (await fetch('https://api.bitbucket.org/2.0/user', {
|
|
1299
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1300
|
+
}).then(r => r.json()));
|
|
1301
|
+
// Bitbucket may not expose email on /user — fetch from /user/emails
|
|
1302
|
+
const emails = (await fetch('https://api.bitbucket.org/2.0/user/emails', {
|
|
1303
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1304
|
+
}).then(r => r.json()));
|
|
1305
|
+
const email = emails.values?.find(e => e.is_primary && e.is_confirmed)?.email ??
|
|
1306
|
+
emails.values?.find(e => e.is_confirmed)?.email;
|
|
1307
|
+
if (stored.linkUserId) {
|
|
1308
|
+
if (!adapter.linkProvider)
|
|
1309
|
+
return c.json({ error: 'Auth adapter does not support linkProvider' }, 500);
|
|
1310
|
+
await adapter.linkProvider(stored.linkUserId, 'bitbucket', info.account_id);
|
|
1311
|
+
eventBus.emit('security.auth.oauth.linked', {
|
|
1312
|
+
userId: stored.linkUserId,
|
|
1313
|
+
meta: { provider: 'bitbucket' },
|
|
1314
|
+
});
|
|
1315
|
+
const sep = postLoginRedirect.includes('?') ? '&' : '?';
|
|
1316
|
+
return c.redirect(`${postLoginRedirect}${sep}linked=bitbucket`);
|
|
1317
|
+
}
|
|
1318
|
+
return finishOAuth(c, runtime, 'bitbucket', info.account_id, { email, name: info.display_name, avatarUrl: info.links?.avatar?.href }, postLoginRedirect);
|
|
1319
|
+
});
|
|
1320
|
+
router.use('/auth/bitbucket/link', userAuth);
|
|
1321
|
+
router.openapi(withSecurity(createRoute({
|
|
1322
|
+
method: 'get',
|
|
1323
|
+
path: '/auth/bitbucket/link',
|
|
1324
|
+
summary: 'Link Bitbucket account',
|
|
1325
|
+
description: "Initiates an OAuth flow to link a Bitbucket account to the authenticated user. Requires a valid session. Redirects to Bitbucket's authorization page.",
|
|
1326
|
+
tags,
|
|
1327
|
+
responses: {
|
|
1328
|
+
302: { description: "Redirect to Bitbucket's OAuth authorization page." },
|
|
1329
|
+
401: {
|
|
1330
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1331
|
+
description: 'No valid session.',
|
|
1332
|
+
},
|
|
1333
|
+
},
|
|
1334
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1335
|
+
const state = generateState();
|
|
1336
|
+
await storeOAuthState(state, undefined, c.get('authUserId'));
|
|
1337
|
+
const url = getBitbucket().createAuthorizationURL(state);
|
|
1338
|
+
return c.redirect(url.toString());
|
|
1339
|
+
});
|
|
1340
|
+
router.openapi(withSecurity(createRoute({
|
|
1341
|
+
method: 'delete',
|
|
1342
|
+
path: '/auth/bitbucket/link',
|
|
1343
|
+
summary: 'Unlink Bitbucket account',
|
|
1344
|
+
description: 'Removes the linked Bitbucket OAuth account from the authenticated user. Requires a valid session and factor verification when the account has a password or MFA enabled.',
|
|
1345
|
+
tags,
|
|
1346
|
+
request: {
|
|
1347
|
+
body: {
|
|
1348
|
+
required: false,
|
|
1349
|
+
content: { 'application/json': { schema: unlinkVerificationSchema } },
|
|
1350
|
+
description: 'Factor verification (required when the account has a password or MFA enabled).',
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
responses: {
|
|
1354
|
+
204: { description: 'Bitbucket account unlinked successfully.' },
|
|
1355
|
+
400: {
|
|
1356
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1357
|
+
description: 'Verification is required but not provided.',
|
|
1358
|
+
},
|
|
1359
|
+
401: {
|
|
1360
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1361
|
+
description: 'No valid session or invalid verification.',
|
|
1362
|
+
},
|
|
1363
|
+
429: {
|
|
1364
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1365
|
+
description: 'Too many unlink attempts. Try again later.',
|
|
1366
|
+
},
|
|
1367
|
+
500: {
|
|
1368
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1369
|
+
description: 'Auth adapter does not support unlinkProvider.',
|
|
1370
|
+
},
|
|
1371
|
+
},
|
|
1372
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1373
|
+
if (!adapter.unlinkProvider) {
|
|
1374
|
+
return c.json({ error: 'Auth adapter does not support unlinkProvider' }, 500);
|
|
1375
|
+
}
|
|
1376
|
+
const userId = c.get('authUserId');
|
|
1377
|
+
const sessionId = c.get('sessionId');
|
|
1378
|
+
if (await runtime.rateLimit.trackAttempt(`oauth-unlink:${userId}`, oauthUnlinkOpts)) {
|
|
1379
|
+
return c.json({ error: 'Too many unlink attempts. Try again later.' }, 429);
|
|
1380
|
+
}
|
|
1381
|
+
const body = c.req.valid('json') ?? {};
|
|
1382
|
+
const unlinkErr = await verifyUnlinkFactor(userId, sessionId, body);
|
|
1383
|
+
if (unlinkErr)
|
|
1384
|
+
return c.json({ error: unlinkErr.error }, unlinkErr.status);
|
|
1385
|
+
await adapter.unlinkProvider(userId, 'bitbucket');
|
|
1386
|
+
eventBus.emit('security.auth.oauth.unlinked', { userId, meta: { provider: 'bitbucket' } });
|
|
1387
|
+
return c.body(null, 204);
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
// ─── OAuth Re-auth ──────────────────────────────────────────────────────
|
|
1391
|
+
// Per-provider re-auth routes — only mounted when reauth is enabled in config.
|
|
1392
|
+
// Allows forcing the user to re-authenticate with their OAuth provider before
|
|
1393
|
+
// sensitive operations (e.g. account deletion, MFA changes).
|
|
1394
|
+
if (getConfig().oauthReauth?.enabled ?? false) {
|
|
1395
|
+
for (const provider of providers) {
|
|
1396
|
+
// Skip Apple — Apple does not support prompt= parameter for re-auth
|
|
1397
|
+
if (provider === 'apple')
|
|
1398
|
+
continue;
|
|
1399
|
+
router.use(`/auth/${provider}/reauth`, userAuth);
|
|
1400
|
+
router.openapi(withSecurity(createRoute({
|
|
1401
|
+
method: 'get',
|
|
1402
|
+
path: `/auth/${provider}/reauth`,
|
|
1403
|
+
summary: `Initiate ${provider} OAuth re-authentication`,
|
|
1404
|
+
description: `Forces the authenticated user to re-authenticate with ${provider} before a sensitive operation. Requires a valid session. Redirects to the provider with \`prompt=${getConfig().oauthReauth?.promptType ?? 'login'}\`.`,
|
|
1405
|
+
tags,
|
|
1406
|
+
request: {
|
|
1407
|
+
query: z.object({
|
|
1408
|
+
purpose: z
|
|
1409
|
+
.string()
|
|
1410
|
+
.describe('Reason for re-auth (e.g. delete_account, change_password).'),
|
|
1411
|
+
returnUrl: z
|
|
1412
|
+
.string()
|
|
1413
|
+
.optional()
|
|
1414
|
+
.describe('URL to redirect to after successful re-auth. Must be a relative path.'),
|
|
1415
|
+
}),
|
|
1416
|
+
},
|
|
1417
|
+
responses: {
|
|
1418
|
+
302: { description: 'Redirect to provider re-auth page.' },
|
|
1419
|
+
400: {
|
|
1420
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1421
|
+
description: 'Provider not linked to this account.',
|
|
1422
|
+
},
|
|
1423
|
+
401: {
|
|
1424
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1425
|
+
description: 'No valid session.',
|
|
1426
|
+
},
|
|
1427
|
+
500: {
|
|
1428
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1429
|
+
description: 'OAuth provider not configured.',
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
}), { cookieAuth: [] }, { userToken: [] }), async (c) => {
|
|
1433
|
+
const userId = c.get('authUserId');
|
|
1434
|
+
const sessionId = c.get('sessionId');
|
|
1435
|
+
const { purpose, returnUrl } = c.req.valid('query');
|
|
1436
|
+
// Verify user has this provider linked
|
|
1437
|
+
const user = adapter.getUser ? await adapter.getUser(userId) : null;
|
|
1438
|
+
const providerKey = `${provider}:`;
|
|
1439
|
+
const hasProvider = user?.providerIds?.some(id => id.startsWith(providerKey));
|
|
1440
|
+
if (!hasProvider) {
|
|
1441
|
+
return c.json({ error: `No ${provider} account linked to this user` }, 400);
|
|
1442
|
+
}
|
|
1443
|
+
const state = generateState();
|
|
1444
|
+
const codeVerifier = provider === 'google' || provider === 'microsoft' ? generateCodeVerifier() : undefined;
|
|
1445
|
+
// Encode re-auth context into the OAuth state's linkUserId field using a
|
|
1446
|
+
// "reauth:" prefix, so the callback can recover it after consuming the state.
|
|
1447
|
+
// Format: "reauth:userId:sessionId:purpose[:returnUrl]"
|
|
1448
|
+
await storeOAuthState(state, codeVerifier, `reauth:${userId}:${sessionId}:${encodeURIComponent(purpose)}${returnUrl ? `:${encodeURIComponent(returnUrl)}` : ''}`);
|
|
1449
|
+
const promptType = getConfig().oauthReauth?.promptType ?? 'login';
|
|
1450
|
+
let url;
|
|
1451
|
+
if (provider === 'google') {
|
|
1452
|
+
url = getGoogle().createAuthorizationURL(state, codeVerifier, [
|
|
1453
|
+
'openid',
|
|
1454
|
+
'profile',
|
|
1455
|
+
'email',
|
|
1456
|
+
]);
|
|
1457
|
+
}
|
|
1458
|
+
else if (provider === 'microsoft') {
|
|
1459
|
+
url = getMicrosoft().createAuthorizationURL(state, codeVerifier, [
|
|
1460
|
+
'openid',
|
|
1461
|
+
'profile',
|
|
1462
|
+
'email',
|
|
1463
|
+
]);
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
// GitHub
|
|
1467
|
+
url = getGitHub().createAuthorizationURL(state, ['read:user', 'user:email']);
|
|
1468
|
+
}
|
|
1469
|
+
url.searchParams.set('prompt', promptType);
|
|
1470
|
+
return c.redirect(url.toString());
|
|
1471
|
+
});
|
|
1472
|
+
router.openapi(createRoute({
|
|
1473
|
+
method: 'get',
|
|
1474
|
+
path: `/auth/${provider}/reauth/callback`,
|
|
1475
|
+
summary: `${provider} OAuth re-auth callback`,
|
|
1476
|
+
description: `Handles the redirect from ${provider} after re-authentication. Verifies the provider account matches the linked account, then issues a short-lived confirmation code for the client to exchange.`,
|
|
1477
|
+
tags,
|
|
1478
|
+
request: {
|
|
1479
|
+
query: z.object({
|
|
1480
|
+
code: z.string().describe('Authorization code from provider.'),
|
|
1481
|
+
state: z.string().describe('OAuth state parameter.'),
|
|
1482
|
+
}),
|
|
1483
|
+
},
|
|
1484
|
+
responses: {
|
|
1485
|
+
302: { description: 'Redirect with confirmation code (or error).' },
|
|
1486
|
+
400: {
|
|
1487
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1488
|
+
description: 'Invalid state, expired session, or provider account mismatch.',
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
}), async (c) => {
|
|
1492
|
+
const { code, state } = c.req.valid('query');
|
|
1493
|
+
if (!code || !state)
|
|
1494
|
+
return c.json({ error: 'Invalid callback' }, 400);
|
|
1495
|
+
const stored = await consumeOAuthState(state);
|
|
1496
|
+
if (!stored?.linkUserId?.startsWith('reauth:')) {
|
|
1497
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
1498
|
+
}
|
|
1499
|
+
// Parse reauth info encoded in linkUserId: "reauth:userId:sessionId:purpose[:returnUrl]"
|
|
1500
|
+
// userId and sessionId are UUID-safe (no colons); purpose and returnUrl are encodeURIComponent'd.
|
|
1501
|
+
const parts = stored.linkUserId.slice('reauth:'.length).split(':');
|
|
1502
|
+
if (parts.length < 3)
|
|
1503
|
+
return c.json({ error: 'Invalid or expired state' }, 400);
|
|
1504
|
+
const [userId, sessionId, encodedPurpose, encodedReturnUrl] = parts;
|
|
1505
|
+
const purpose = decodeURIComponent(encodedPurpose);
|
|
1506
|
+
const returnUrl = encodedReturnUrl ? decodeURIComponent(encodedReturnUrl) : undefined;
|
|
1507
|
+
// Exchange code for tokens and get the provider user ID
|
|
1508
|
+
let providerUserId;
|
|
1509
|
+
try {
|
|
1510
|
+
if (provider === 'google') {
|
|
1511
|
+
const tokens = await getGoogle().validateAuthorizationCode(code, stored.codeVerifier);
|
|
1512
|
+
const info = (await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
1513
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1514
|
+
}).then(r => r.json()));
|
|
1515
|
+
providerUserId = info.sub;
|
|
1516
|
+
}
|
|
1517
|
+
else if (provider === 'microsoft') {
|
|
1518
|
+
const tokens = await getMicrosoft().validateAuthorizationCode(code, stored.codeVerifier);
|
|
1519
|
+
const info = (await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
1520
|
+
headers: { Authorization: `Bearer ${tokens.accessToken()}` },
|
|
1521
|
+
}).then(r => r.json()));
|
|
1522
|
+
providerUserId = info.id;
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
// GitHub
|
|
1526
|
+
const tokens = await getGitHub().validateAuthorizationCode(code);
|
|
1527
|
+
const info = (await fetch('https://api.github.com/user', {
|
|
1528
|
+
headers: {
|
|
1529
|
+
Authorization: `Bearer ${tokens.accessToken()}`,
|
|
1530
|
+
'User-Agent': 'bunshot',
|
|
1531
|
+
},
|
|
1532
|
+
}).then(r => r.json()));
|
|
1533
|
+
providerUserId = String(info.id);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return c.json({ error: 'Failed to verify provider identity' }, 400);
|
|
1538
|
+
}
|
|
1539
|
+
// Verify the provider account matches what is linked to this user
|
|
1540
|
+
const user = adapter.getUser ? await adapter.getUser(userId) : null;
|
|
1541
|
+
const expectedKey = `${provider}:${providerUserId}`;
|
|
1542
|
+
const isLinked = user?.providerIds?.includes(expectedKey);
|
|
1543
|
+
if (!isLinked) {
|
|
1544
|
+
eventBus.emit('security.auth.oauth.reauthed', {
|
|
1545
|
+
userId,
|
|
1546
|
+
sessionId,
|
|
1547
|
+
meta: { provider, purpose, mismatch: true },
|
|
1548
|
+
});
|
|
1549
|
+
return c.json({ error: 'Provider account mismatch' }, 400);
|
|
1550
|
+
}
|
|
1551
|
+
// Issue a confirmation code
|
|
1552
|
+
const confirmationCode = await storeReauthConfirmation(runtime.repos.oauthReauth, {
|
|
1553
|
+
userId,
|
|
1554
|
+
purpose,
|
|
1555
|
+
});
|
|
1556
|
+
eventBus.emit('security.auth.oauth.reauthed', {
|
|
1557
|
+
userId,
|
|
1558
|
+
sessionId,
|
|
1559
|
+
meta: { provider, purpose },
|
|
1560
|
+
});
|
|
1561
|
+
// Redirect with confirmation code — validate returnUrl against open redirect
|
|
1562
|
+
const isSafeRedirect = (url) => url.startsWith('/') && !url.startsWith('//') && !url.includes('://');
|
|
1563
|
+
const redirectBase = returnUrl && isSafeRedirect(returnUrl) ? returnUrl : '/';
|
|
1564
|
+
try {
|
|
1565
|
+
const url = new URL(redirectBase, 'http://localhost');
|
|
1566
|
+
url.searchParams.set('reauth_code', confirmationCode);
|
|
1567
|
+
// Use relative redirect for relative paths
|
|
1568
|
+
return c.redirect(`${url.pathname}${url.search}`);
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
// Fallback also uses the already-validated redirectBase
|
|
1572
|
+
const sep = redirectBase.includes('?') ? '&' : '?';
|
|
1573
|
+
return c.redirect(`${redirectBase}${sep}reauth_code=${encodeURIComponent(confirmationCode)}`);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
// ─── Code Exchange ─────────────────────────────────────────────────────
|
|
1579
|
+
router.openapi(createRoute({
|
|
1580
|
+
method: 'post',
|
|
1581
|
+
path: '/auth/oauth/exchange',
|
|
1582
|
+
summary: 'Exchange OAuth authorization code for session token',
|
|
1583
|
+
description: 'Exchanges a one-time authorization code (received from the OAuth redirect) for a session token. The code is single-use and expires after 60 seconds. Sets session cookies for browser clients; returns the token in the JSON response for mobile/SPA clients.',
|
|
1584
|
+
tags,
|
|
1585
|
+
request: {
|
|
1586
|
+
body: {
|
|
1587
|
+
content: {
|
|
1588
|
+
'application/json': {
|
|
1589
|
+
schema: z.object({
|
|
1590
|
+
code: z.string().describe('One-time authorization code from the OAuth redirect.'),
|
|
1591
|
+
}),
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1596
|
+
responses: {
|
|
1597
|
+
200: {
|
|
1598
|
+
content: {
|
|
1599
|
+
'application/json': {
|
|
1600
|
+
schema: z.object({
|
|
1601
|
+
token: z.string().describe('Session JWT.'),
|
|
1602
|
+
userId: z.string().describe('Authenticated user ID.'),
|
|
1603
|
+
email: z.string().optional().describe('User email if available.'),
|
|
1604
|
+
refreshToken: z
|
|
1605
|
+
.string()
|
|
1606
|
+
.optional()
|
|
1607
|
+
.describe('Refresh token if refresh tokens are configured.'),
|
|
1608
|
+
}),
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
description: 'Session token and user info.',
|
|
1612
|
+
},
|
|
1613
|
+
400: {
|
|
1614
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1615
|
+
description: 'Missing code parameter.',
|
|
1616
|
+
},
|
|
1617
|
+
401: {
|
|
1618
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1619
|
+
description: 'Invalid, expired, or already-used code.',
|
|
1620
|
+
},
|
|
1621
|
+
429: {
|
|
1622
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1623
|
+
description: 'Rate limit exceeded.',
|
|
1624
|
+
},
|
|
1625
|
+
},
|
|
1626
|
+
}), async (c) => {
|
|
1627
|
+
// Rate limit by IP to prevent brute-forcing codes within the 60s TTL
|
|
1628
|
+
const ip = getClientIp(c);
|
|
1629
|
+
const limited = await runtime.rateLimit.trackAttempt(`oauth-exchange:ip:${ip}`, {
|
|
1630
|
+
max: 20,
|
|
1631
|
+
windowMs: 60_000,
|
|
1632
|
+
});
|
|
1633
|
+
if (limited) {
|
|
1634
|
+
return c.json({ error: 'Too many requests' }, 429);
|
|
1635
|
+
}
|
|
1636
|
+
const { code } = c.req.valid('json');
|
|
1637
|
+
if (!code)
|
|
1638
|
+
return c.json({ error: 'Missing code' }, 400);
|
|
1639
|
+
const payload = await consumeOAuthCode(runtime.repos.oauthCode, code, runtime.dataEncryptionKeys);
|
|
1640
|
+
if (!payload)
|
|
1641
|
+
return c.json({ error: 'Invalid or expired code' }, 401);
|
|
1642
|
+
// Set session cookies for browser clients
|
|
1643
|
+
const rtConfig = getConfig().refreshToken;
|
|
1644
|
+
setCookie(c, COOKIE_TOKEN, payload.token, cookieOptions(rtConfig ? (rtConfig.accessTokenExpiry ?? 900) : undefined));
|
|
1645
|
+
if (payload.refreshToken && rtConfig) {
|
|
1646
|
+
setCookie(c, COOKIE_REFRESH_TOKEN, payload.refreshToken, cookieOptions(rtConfig.refreshTokenExpiry ?? 2_592_000));
|
|
1647
|
+
}
|
|
1648
|
+
if (getConfig().csrfEnabled)
|
|
1649
|
+
refreshCsrfToken(c);
|
|
1650
|
+
return c.json({
|
|
1651
|
+
token: payload.token,
|
|
1652
|
+
userId: payload.userId,
|
|
1653
|
+
email: payload.email,
|
|
1654
|
+
refreshToken: payload.refreshToken,
|
|
1655
|
+
}, 200);
|
|
1656
|
+
});
|
|
1657
|
+
// ─── Re-auth Confirmation Exchange ──────────────────────────────────────
|
|
1658
|
+
if (getConfig().oauthReauth?.enabled ?? false) {
|
|
1659
|
+
router.openapi(createRoute({
|
|
1660
|
+
method: 'post',
|
|
1661
|
+
path: '/auth/oauth/reauth/exchange',
|
|
1662
|
+
summary: 'Exchange OAuth re-auth confirmation code for step-up proof',
|
|
1663
|
+
description: 'Exchanges a one-time re-auth confirmation code (received from the re-auth callback redirect) for a step-up proof. The code is single-use and expires after 5 minutes. Returns confirmation that the user successfully re-authenticated with their OAuth provider.',
|
|
1664
|
+
tags,
|
|
1665
|
+
request: {
|
|
1666
|
+
body: {
|
|
1667
|
+
content: {
|
|
1668
|
+
'application/json': {
|
|
1669
|
+
schema: z.object({
|
|
1670
|
+
code: z
|
|
1671
|
+
.string()
|
|
1672
|
+
.describe('One-time re-auth confirmation code from the redirect.'),
|
|
1673
|
+
}),
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
responses: {
|
|
1679
|
+
200: {
|
|
1680
|
+
content: {
|
|
1681
|
+
'application/json': {
|
|
1682
|
+
schema: z.object({
|
|
1683
|
+
reauthConfirmed: z.literal(true).describe('Always true on success.'),
|
|
1684
|
+
purpose: z.string().describe('The purpose the re-auth was requested for.'),
|
|
1685
|
+
userId: z.string().describe('The re-authenticated user ID.'),
|
|
1686
|
+
}),
|
|
1687
|
+
},
|
|
1688
|
+
},
|
|
1689
|
+
description: 'Re-auth confirmed.',
|
|
1690
|
+
},
|
|
1691
|
+
400: {
|
|
1692
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1693
|
+
description: 'Missing code parameter.',
|
|
1694
|
+
},
|
|
1695
|
+
401: {
|
|
1696
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1697
|
+
description: 'Invalid, expired, or already-used code.',
|
|
1698
|
+
},
|
|
1699
|
+
429: {
|
|
1700
|
+
content: { 'application/json': { schema: OAuthErrorResponse } },
|
|
1701
|
+
description: 'Rate limit exceeded.',
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
}), async (c) => {
|
|
1705
|
+
const ip = getClientIp(c);
|
|
1706
|
+
const limited = await runtime.rateLimit.trackAttempt(`oauth-reauth-exchange:ip:${ip}`, {
|
|
1707
|
+
max: 20,
|
|
1708
|
+
windowMs: 60_000,
|
|
1709
|
+
});
|
|
1710
|
+
if (limited) {
|
|
1711
|
+
return c.json({ error: 'Too many requests' }, 429);
|
|
1712
|
+
}
|
|
1713
|
+
const { code } = c.req.valid('json');
|
|
1714
|
+
if (!code)
|
|
1715
|
+
return c.json({ error: 'Missing code' }, 400);
|
|
1716
|
+
const payload = await consumeReauthConfirmation(runtime.repos.oauthReauth, code);
|
|
1717
|
+
if (!payload)
|
|
1718
|
+
return c.json({ error: 'Invalid or expired code' }, 401);
|
|
1719
|
+
return c.json({
|
|
1720
|
+
reauthConfirmed: true,
|
|
1721
|
+
purpose: payload.purpose,
|
|
1722
|
+
userId: payload.userId,
|
|
1723
|
+
}, 200);
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
return router;
|
|
1727
|
+
};
|