@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.
Files changed (742) hide show
  1. package/.oclif.manifest.json +39 -0
  2. package/README.md +8282 -2147
  3. package/dist/cli/commands/init.js +690 -0
  4. package/dist/cli/index.js +6 -0
  5. package/dist/cli.js +4 -4
  6. package/dist/packages/bunshot-admin/src/index.d.ts +15 -0
  7. package/dist/packages/bunshot-admin/src/index.js +11 -0
  8. package/dist/packages/bunshot-admin/src/lib/resourceTypes.d.ts +8 -0
  9. package/dist/packages/bunshot-admin/src/lib/resourceTypes.js +33 -0
  10. package/dist/packages/bunshot-admin/src/lib/typedRoute.d.ts +14 -0
  11. package/dist/packages/bunshot-admin/src/lib/typedRoute.js +17 -0
  12. package/dist/packages/bunshot-admin/src/plugin.d.ts +4 -0
  13. package/dist/packages/bunshot-admin/src/plugin.js +46 -0
  14. package/dist/packages/bunshot-admin/src/providers/auth0Access.d.ts +6 -0
  15. package/dist/packages/bunshot-admin/src/providers/auth0Access.js +32 -0
  16. package/dist/packages/bunshot-admin/src/routes/admin.d.ts +10 -0
  17. package/dist/packages/bunshot-admin/src/routes/admin.js +923 -0
  18. package/dist/packages/bunshot-admin/src/routes/mail.d.ts +6 -0
  19. package/dist/packages/bunshot-admin/src/routes/mail.js +114 -0
  20. package/dist/packages/bunshot-admin/src/routes/permissions.d.ts +8 -0
  21. package/dist/packages/bunshot-admin/src/routes/permissions.js +315 -0
  22. package/dist/packages/bunshot-admin/src/types/config.d.ts +16 -0
  23. package/dist/packages/bunshot-admin/src/types/config.js +37 -0
  24. package/dist/packages/bunshot-admin/src/types/env.d.ts +14 -0
  25. package/dist/packages/bunshot-admin/src/types/provider.d.ts +1 -0
  26. package/dist/packages/bunshot-admin/src/types/provider.js +4 -0
  27. package/dist/packages/bunshot-auth/src/adapters/memoryAuth.d.ts +66 -0
  28. package/dist/packages/bunshot-auth/src/adapters/memoryAuth.js +1063 -0
  29. package/dist/packages/bunshot-auth/src/adapters/mongoAuth.d.ts +2 -0
  30. package/dist/packages/bunshot-auth/src/adapters/mongoAuth.js +536 -0
  31. package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.d.ts +88 -0
  32. package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.js +1366 -0
  33. package/dist/packages/bunshot-auth/src/admin/bunshotAccess.d.ts +2 -0
  34. package/dist/packages/bunshot-auth/src/admin/bunshotAccess.js +23 -0
  35. package/dist/packages/bunshot-auth/src/admin/bunshotUsers.d.ts +5 -0
  36. package/dist/packages/bunshot-auth/src/admin/bunshotUsers.js +131 -0
  37. package/dist/packages/bunshot-auth/src/bootstrap.d.ts +38 -0
  38. package/dist/packages/bunshot-auth/src/bootstrap.js +384 -0
  39. package/dist/packages/bunshot-auth/src/config/appConfig.d.ts +3 -0
  40. package/dist/packages/bunshot-auth/src/config/appConfig.js +4 -0
  41. package/dist/packages/bunshot-auth/src/config/authConfig.d.ts +478 -0
  42. package/dist/packages/bunshot-auth/src/config/authConfig.js +46 -0
  43. package/dist/packages/bunshot-auth/src/config/configLock.d.ts +2 -0
  44. package/dist/packages/bunshot-auth/src/config/configLock.js +10 -0
  45. package/dist/packages/bunshot-auth/src/index.d.ts +25 -0
  46. package/dist/packages/bunshot-auth/src/index.js +23 -0
  47. package/dist/packages/bunshot-auth/src/infra/mongo.d.ts +15 -0
  48. package/dist/packages/bunshot-auth/src/infra/mongo.js +44 -0
  49. package/dist/packages/bunshot-auth/src/infra/queue.d.ts +14 -0
  50. package/dist/packages/bunshot-auth/src/infra/queue.js +27 -0
  51. package/dist/packages/bunshot-auth/src/infra/redis.d.ts +5 -0
  52. package/dist/packages/bunshot-auth/src/infra/redis.js +15 -0
  53. package/dist/packages/bunshot-auth/src/infra/signing.d.ts +7 -0
  54. package/dist/packages/bunshot-auth/src/infra/signing.js +8 -0
  55. package/dist/packages/bunshot-auth/src/lib/accountLockout.d.ts +34 -0
  56. package/dist/packages/bunshot-auth/src/lib/accountLockout.js +244 -0
  57. package/dist/packages/bunshot-auth/src/lib/adapterTiers.d.ts +1 -0
  58. package/dist/packages/bunshot-auth/src/lib/adapterTiers.js +1 -0
  59. package/dist/packages/bunshot-auth/src/lib/authAdapter.d.ts +1 -0
  60. package/dist/packages/bunshot-auth/src/lib/authAdapter.js +1 -0
  61. package/dist/packages/bunshot-auth/src/lib/authContext.d.ts +15 -0
  62. package/dist/packages/bunshot-auth/src/lib/authContext.js +1 -0
  63. package/dist/packages/bunshot-auth/src/lib/authEventBus.d.ts +4 -0
  64. package/dist/packages/bunshot-auth/src/lib/authEventBus.js +15 -0
  65. package/dist/packages/bunshot-auth/src/lib/authRateLimit.d.ts +28 -0
  66. package/dist/packages/bunshot-auth/src/lib/authRateLimit.js +205 -0
  67. package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.d.ts +8 -2
  68. package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.js +22 -9
  69. package/dist/packages/bunshot-auth/src/lib/cache.d.ts +12 -0
  70. package/dist/packages/bunshot-auth/src/lib/cache.js +120 -0
  71. package/dist/packages/bunshot-auth/src/lib/clientIp.d.ts +4 -0
  72. package/dist/{lib → packages/bunshot-auth/src/lib}/clientIp.js +14 -7
  73. package/dist/packages/bunshot-auth/src/lib/cookieOptions.d.ts +27 -0
  74. package/dist/packages/bunshot-auth/src/lib/cookieOptions.js +33 -0
  75. package/dist/packages/bunshot-auth/src/lib/credentialStuffing.d.ts +40 -0
  76. package/dist/packages/bunshot-auth/src/lib/credentialStuffing.js +221 -0
  77. package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.d.ts +19 -0
  78. package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.js +148 -0
  79. package/dist/packages/bunshot-auth/src/lib/emailTemplates.d.ts +23 -0
  80. package/dist/packages/bunshot-auth/src/lib/emailTemplates.js +265 -0
  81. package/dist/packages/bunshot-auth/src/lib/emailVerification.d.ts +30 -0
  82. package/dist/packages/bunshot-auth/src/lib/emailVerification.js +200 -0
  83. package/dist/packages/bunshot-auth/src/lib/env.d.ts +1 -0
  84. package/dist/packages/bunshot-auth/src/lib/env.js +3 -0
  85. package/dist/packages/bunshot-auth/src/lib/fingerprint.js +36 -0
  86. package/dist/{lib → packages/bunshot-auth/src/lib}/groups.d.ts +15 -16
  87. package/dist/{lib → packages/bunshot-auth/src/lib}/groups.js +22 -34
  88. package/dist/packages/bunshot-auth/src/lib/jwks.d.ts +28 -0
  89. package/dist/packages/bunshot-auth/src/lib/jwks.js +79 -0
  90. package/dist/packages/bunshot-auth/src/lib/jwt.d.ts +12 -0
  91. package/dist/packages/bunshot-auth/src/lib/jwt.js +86 -0
  92. package/dist/{lib → packages/bunshot-auth/src/lib}/logger.js +3 -3
  93. package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.d.ts +5 -4
  94. package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.js +6 -10
  95. package/dist/packages/bunshot-auth/src/lib/magicLink.d.ts +13 -0
  96. package/dist/packages/bunshot-auth/src/lib/magicLink.js +145 -0
  97. package/dist/packages/bunshot-auth/src/lib/mfaChallenge.d.ts +60 -0
  98. package/dist/packages/bunshot-auth/src/lib/mfaChallenge.js +419 -0
  99. package/dist/packages/bunshot-auth/src/lib/oauth.d.ts +82 -0
  100. package/dist/packages/bunshot-auth/src/lib/oauth.js +177 -0
  101. package/dist/packages/bunshot-auth/src/lib/oauthCode.d.ts +19 -0
  102. package/dist/packages/bunshot-auth/src/lib/oauthCode.js +182 -0
  103. package/dist/packages/bunshot-auth/src/lib/oauthReauth.d.ts +19 -0
  104. package/dist/packages/bunshot-auth/src/lib/oauthReauth.js +255 -0
  105. package/dist/packages/bunshot-auth/src/lib/organization.d.ts +66 -0
  106. package/dist/packages/bunshot-auth/src/lib/organization.js +225 -0
  107. package/dist/packages/bunshot-auth/src/lib/passwordHistory.d.ts +12 -0
  108. package/dist/packages/bunshot-auth/src/lib/passwordHistory.js +31 -0
  109. package/dist/packages/bunshot-auth/src/lib/resetPassword.d.ts +20 -0
  110. package/dist/packages/bunshot-auth/src/lib/resetPassword.js +148 -0
  111. package/dist/packages/bunshot-auth/src/lib/roles.d.ts +9 -0
  112. package/dist/packages/bunshot-auth/src/lib/roles.js +93 -0
  113. package/dist/packages/bunshot-auth/src/lib/saml.d.ts +29 -0
  114. package/dist/packages/bunshot-auth/src/lib/saml.js +73 -0
  115. package/dist/packages/bunshot-auth/src/lib/samlRequestId.d.ts +13 -0
  116. package/dist/packages/bunshot-auth/src/lib/samlRequestId.js +129 -0
  117. package/dist/{lib → packages/bunshot-auth/src/lib}/scim.d.ts +7 -7
  118. package/dist/{lib → packages/bunshot-auth/src/lib}/scim.js +15 -13
  119. package/dist/packages/bunshot-auth/src/lib/securityEventWiring.d.ts +22 -0
  120. package/dist/packages/bunshot-auth/src/lib/securityEventWiring.js +65 -0
  121. package/dist/packages/bunshot-auth/src/lib/session.d.ts +45 -0
  122. package/dist/packages/bunshot-auth/src/lib/session.js +1211 -0
  123. package/dist/packages/bunshot-auth/src/lib/storeInfra.d.ts +26 -0
  124. package/dist/packages/bunshot-auth/src/lib/storeInfra.js +18 -0
  125. package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.d.ts +3 -2
  126. package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.js +2 -5
  127. package/dist/packages/bunshot-auth/src/lib/validateAdapter.d.ts +16 -0
  128. package/dist/packages/bunshot-auth/src/lib/validateAdapter.js +161 -0
  129. package/dist/packages/bunshot-auth/src/middleware/bearerAuth.d.ts +13 -0
  130. package/dist/packages/bunshot-auth/src/middleware/bearerAuth.js +58 -0
  131. package/dist/{middleware → packages/bunshot-auth/src/middleware}/csrf.d.ts +5 -4
  132. package/dist/packages/bunshot-auth/src/middleware/csrf.js +138 -0
  133. package/dist/packages/bunshot-auth/src/middleware/identify.d.ts +4 -0
  134. package/dist/packages/bunshot-auth/src/middleware/identify.js +124 -0
  135. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.d.ts +2 -2
  136. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.js +10 -8
  137. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.d.ts +2 -2
  138. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.js +20 -16
  139. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.d.ts +2 -2
  140. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.js +6 -6
  141. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.d.ts +2 -2
  142. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.js +8 -7
  143. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.d.ts +2 -2
  144. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.js +7 -6
  145. package/dist/packages/bunshot-auth/src/middleware/scimAuth.d.ts +8 -0
  146. package/dist/packages/bunshot-auth/src/middleware/scimAuth.js +29 -0
  147. package/dist/packages/bunshot-auth/src/middleware/userAuth.d.ts +3 -0
  148. package/dist/packages/bunshot-auth/src/middleware/userAuth.js +6 -0
  149. package/dist/{models → packages/bunshot-auth/src/models}/AuthUser.d.ts +12 -8
  150. package/dist/packages/bunshot-auth/src/models/AuthUser.js +53 -0
  151. package/dist/packages/bunshot-auth/src/models/Group.d.ts +19 -0
  152. package/dist/packages/bunshot-auth/src/models/Group.js +22 -0
  153. package/dist/{models → packages/bunshot-auth/src/models}/GroupMembership.d.ts +6 -8
  154. package/dist/packages/bunshot-auth/src/models/GroupMembership.js +19 -0
  155. package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.d.ts +1 -1
  156. package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.js +5 -5
  157. package/dist/packages/bunshot-auth/src/models/TenantRole.d.ts +13 -0
  158. package/dist/packages/bunshot-auth/src/models/TenantRole.js +17 -0
  159. package/dist/packages/bunshot-auth/src/plugin.d.ts +4 -0
  160. package/dist/packages/bunshot-auth/src/plugin.js +274 -0
  161. package/dist/packages/bunshot-auth/src/routes/auth.d.ts +15 -0
  162. package/dist/packages/bunshot-auth/src/routes/auth.js +1624 -0
  163. package/dist/packages/bunshot-auth/src/routes/groups.d.ts +4 -0
  164. package/dist/packages/bunshot-auth/src/routes/groups.js +481 -0
  165. package/dist/packages/bunshot-auth/src/routes/m2m.d.ts +2 -0
  166. package/dist/packages/bunshot-auth/src/routes/m2m.js +145 -0
  167. package/dist/packages/bunshot-auth/src/routes/mfa.d.ts +6 -0
  168. package/dist/packages/bunshot-auth/src/routes/mfa.js +991 -0
  169. package/dist/packages/bunshot-auth/src/routes/oauth.d.ts +3 -0
  170. package/dist/packages/bunshot-auth/src/routes/oauth.js +1727 -0
  171. package/dist/packages/bunshot-auth/src/routes/oidc.d.ts +2 -0
  172. package/dist/packages/bunshot-auth/src/routes/oidc.js +84 -0
  173. package/dist/packages/bunshot-auth/src/routes/organizations.d.ts +3 -0
  174. package/dist/packages/bunshot-auth/src/routes/organizations.js +741 -0
  175. package/dist/packages/bunshot-auth/src/routes/passkey.d.ts +2 -0
  176. package/dist/packages/bunshot-auth/src/routes/passkey.js +199 -0
  177. package/dist/packages/bunshot-auth/src/routes/saml.d.ts +2 -0
  178. package/dist/packages/bunshot-auth/src/routes/saml.js +226 -0
  179. package/dist/packages/bunshot-auth/src/routes/scim.d.ts +3 -0
  180. package/dist/packages/bunshot-auth/src/routes/scim.js +588 -0
  181. package/dist/packages/bunshot-auth/src/runtime.d.ts +52 -0
  182. package/dist/packages/bunshot-auth/src/runtime.js +11 -0
  183. package/dist/{schemas → packages/bunshot-auth/src/schemas}/auth.d.ts +4 -5
  184. package/dist/packages/bunshot-auth/src/schemas/auth.js +24 -0
  185. package/dist/packages/bunshot-auth/src/schemas/error.d.ts +10 -0
  186. package/dist/packages/bunshot-auth/src/schemas/error.js +10 -0
  187. package/dist/packages/bunshot-auth/src/schemas/success.d.ts +10 -0
  188. package/dist/packages/bunshot-auth/src/schemas/success.js +10 -0
  189. package/dist/packages/bunshot-auth/src/services/auth.d.ts +39 -0
  190. package/dist/packages/bunshot-auth/src/services/auth.js +378 -0
  191. package/dist/{services → packages/bunshot-auth/src/services}/mfa.d.ts +41 -17
  192. package/dist/{services → packages/bunshot-auth/src/services}/mfa.js +259 -183
  193. package/dist/packages/bunshot-auth/src/testing.d.ts +31 -0
  194. package/dist/packages/bunshot-auth/src/testing.js +23 -0
  195. package/dist/packages/bunshot-auth/src/types/adapter.d.ts +1 -0
  196. package/dist/packages/bunshot-auth/src/types/adapter.js +1 -0
  197. package/dist/packages/bunshot-auth/src/types/config.d.ts +152 -0
  198. package/dist/packages/bunshot-auth/src/types/config.js +179 -0
  199. package/dist/{routes → packages/bunshot-auth/src/types}/groups.d.ts +2 -3
  200. package/dist/packages/bunshot-auth/src/types/groups.js +1 -0
  201. package/dist/packages/bunshot-auth/src/types/oauthCode.d.ts +6 -0
  202. package/dist/packages/bunshot-auth/src/types/oauthCode.js +1 -0
  203. package/dist/packages/bunshot-auth/src/types/oauthReauth.d.ts +13 -0
  204. package/dist/packages/bunshot-auth/src/types/oauthReauth.js +1 -0
  205. package/dist/packages/bunshot-auth/src/types/redis.d.ts +1 -0
  206. package/dist/packages/bunshot-auth/src/types/redis.js +1 -0
  207. package/dist/packages/bunshot-auth/src/types/saml.d.ts +10 -0
  208. package/dist/packages/bunshot-auth/src/types/saml.js +1 -0
  209. package/dist/packages/bunshot-auth/src/types/session.d.ts +18 -0
  210. package/dist/packages/bunshot-auth/src/types/session.js +1 -0
  211. package/dist/packages/bunshot-auth/src/types/store.d.ts +1 -0
  212. package/dist/packages/bunshot-auth/src/types/store.js +1 -0
  213. package/dist/packages/bunshot-core/src/adminProvider.d.ts +95 -0
  214. package/dist/packages/bunshot-core/src/adminProvider.js +1 -0
  215. package/dist/packages/bunshot-core/src/auditLog.d.ts +34 -0
  216. package/dist/packages/bunshot-core/src/auditLog.js +1 -0
  217. package/dist/packages/bunshot-core/src/auth-adapter.d.ts +227 -0
  218. package/dist/packages/bunshot-core/src/auth-adapter.js +4 -0
  219. package/dist/packages/bunshot-core/src/authVariables.d.ts +14 -0
  220. package/dist/packages/bunshot-core/src/authVariables.js +4 -0
  221. package/dist/packages/bunshot-core/src/cache.d.ts +12 -0
  222. package/dist/packages/bunshot-core/src/cache.js +21 -0
  223. package/dist/{lib → packages/bunshot-core/src}/captcha.d.ts +1 -10
  224. package/dist/packages/bunshot-core/src/captcha.js +1 -0
  225. package/dist/packages/bunshot-core/src/clearRegistry.d.ts +6 -0
  226. package/dist/packages/bunshot-core/src/clearRegistry.js +17 -0
  227. package/dist/packages/bunshot-core/src/clientIp.d.ts +3 -0
  228. package/dist/packages/bunshot-core/src/clientIp.js +45 -0
  229. package/dist/packages/bunshot-core/src/configLock.d.ts +4 -0
  230. package/dist/packages/bunshot-core/src/configLock.js +7 -0
  231. package/dist/packages/bunshot-core/src/configValidation.d.ts +22 -0
  232. package/dist/packages/bunshot-core/src/configValidation.js +39 -0
  233. package/dist/packages/bunshot-core/src/constants.js +10 -0
  234. package/dist/packages/bunshot-core/src/context/bunshotContext.d.ts +232 -0
  235. package/dist/packages/bunshot-core/src/context/bunshotContext.js +1 -0
  236. package/dist/packages/bunshot-core/src/context/contextAccess.d.ts +3 -0
  237. package/dist/packages/bunshot-core/src/context/contextAccess.js +16 -0
  238. package/dist/packages/bunshot-core/src/context/contextStore.d.ts +16 -0
  239. package/dist/packages/bunshot-core/src/context/contextStore.js +31 -0
  240. package/dist/packages/bunshot-core/src/context/frameworkConfig.d.ts +38 -0
  241. package/dist/packages/bunshot-core/src/context/frameworkConfig.js +1 -0
  242. package/dist/packages/bunshot-core/src/context/index.d.ts +4 -0
  243. package/dist/packages/bunshot-core/src/context/index.js +2 -0
  244. package/dist/packages/bunshot-core/src/context.d.ts +40 -0
  245. package/dist/packages/bunshot-core/src/context.js +35 -0
  246. package/dist/packages/bunshot-core/src/coreContracts.d.ts +47 -0
  247. package/dist/packages/bunshot-core/src/coreContracts.js +1 -0
  248. package/dist/packages/bunshot-core/src/coreRegistrar.d.ts +6 -0
  249. package/dist/packages/bunshot-core/src/coreRegistrar.js +42 -0
  250. package/dist/{lib → packages/bunshot-core/src}/createRoute.d.ts +4 -30
  251. package/dist/{lib → packages/bunshot-core/src}/createRoute.js +39 -88
  252. package/dist/packages/bunshot-core/src/cronRegistry.d.ts +11 -0
  253. package/dist/packages/bunshot-core/src/cronRegistry.js +1 -0
  254. package/dist/packages/bunshot-core/src/crypto.d.ts +43 -0
  255. package/dist/packages/bunshot-core/src/crypto.js +74 -0
  256. package/dist/packages/bunshot-core/src/csrf.d.ts +8 -0
  257. package/dist/packages/bunshot-core/src/csrf.js +1 -0
  258. package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.d.ts +7 -0
  259. package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.js +19 -0
  260. package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.d.ts +6 -0
  261. package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.js +40 -0
  262. package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.d.ts +6 -0
  263. package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.js +24 -0
  264. package/dist/packages/bunshot-core/src/emailTemplates.d.ts +5 -0
  265. package/dist/packages/bunshot-core/src/emailTemplates.js +10 -0
  266. package/dist/{lib/HttpError.d.ts → packages/bunshot-core/src/errors.d.ts} +4 -1
  267. package/dist/{lib/HttpError.js → packages/bunshot-core/src/errors.js} +7 -1
  268. package/dist/packages/bunshot-core/src/eventBus.d.ts +270 -0
  269. package/dist/packages/bunshot-core/src/eventBus.js +143 -0
  270. package/dist/packages/bunshot-core/src/idempotency.d.ts +18 -0
  271. package/dist/packages/bunshot-core/src/idempotency.js +1 -0
  272. package/dist/packages/bunshot-core/src/index.d.ts +60 -0
  273. package/dist/packages/bunshot-core/src/index.js +34 -0
  274. package/dist/packages/bunshot-core/src/mail.d.ts +14 -0
  275. package/dist/packages/bunshot-core/src/mail.js +8 -0
  276. package/dist/packages/bunshot-core/src/memoryEviction.d.ts +24 -0
  277. package/dist/packages/bunshot-core/src/memoryEviction.js +52 -0
  278. package/dist/packages/bunshot-core/src/pagination.d.ts +45 -0
  279. package/dist/packages/bunshot-core/src/pagination.js +61 -0
  280. package/dist/packages/bunshot-core/src/permissions.d.ts +64 -0
  281. package/dist/packages/bunshot-core/src/permissions.js +27 -0
  282. package/dist/packages/bunshot-core/src/plugin.d.ts +44 -0
  283. package/dist/packages/bunshot-core/src/plugin.js +1 -0
  284. package/dist/packages/bunshot-core/src/rateLimit.d.ts +5 -0
  285. package/dist/packages/bunshot-core/src/rateLimit.js +18 -0
  286. package/dist/packages/bunshot-core/src/redis.d.ts +21 -0
  287. package/dist/packages/bunshot-core/src/redis.js +1 -0
  288. package/dist/packages/bunshot-core/src/routeAuth.d.ts +5 -0
  289. package/dist/packages/bunshot-core/src/routeAuth.js +11 -0
  290. package/dist/packages/bunshot-core/src/routeOverrides.d.ts +24 -0
  291. package/dist/packages/bunshot-core/src/routeOverrides.js +25 -0
  292. package/dist/packages/bunshot-core/src/routerAdapter.d.ts +6 -0
  293. package/dist/packages/bunshot-core/src/routerAdapter.js +56 -0
  294. package/dist/packages/bunshot-core/src/secrets.d.ts +48 -0
  295. package/dist/packages/bunshot-core/src/secrets.js +8 -0
  296. package/dist/packages/bunshot-core/src/signing.d.ts +41 -0
  297. package/dist/packages/bunshot-core/src/signing.js +1 -0
  298. package/dist/packages/bunshot-core/src/sse.d.ts +36 -0
  299. package/dist/packages/bunshot-core/src/sse.js +1 -0
  300. package/dist/packages/bunshot-core/src/storageAdapter.js +1 -0
  301. package/dist/packages/bunshot-core/src/storeInfra.d.ts +44 -0
  302. package/dist/packages/bunshot-core/src/storeInfra.js +18 -0
  303. package/dist/packages/bunshot-core/src/storeType.d.ts +7 -0
  304. package/dist/packages/bunshot-core/src/storeType.js +1 -0
  305. package/dist/packages/bunshot-core/src/testing.d.ts +1 -0
  306. package/dist/packages/bunshot-core/src/testing.js +1 -0
  307. package/dist/packages/bunshot-core/src/uploadRegistry.d.ts +23 -0
  308. package/dist/packages/bunshot-core/src/uploadRegistry.js +4 -0
  309. package/dist/packages/bunshot-core/src/userResolver.d.ts +5 -0
  310. package/dist/packages/bunshot-core/src/userResolver.js +14 -0
  311. package/dist/packages/bunshot-core/src/wsMessages.d.ts +42 -0
  312. package/dist/packages/bunshot-core/src/wsMessages.js +4 -0
  313. package/dist/packages/bunshot-permissions/src/adapters/memory.d.ts +7 -0
  314. package/dist/packages/bunshot-permissions/src/adapters/memory.js +73 -0
  315. package/dist/packages/bunshot-permissions/src/index.d.ts +10 -0
  316. package/dist/packages/bunshot-permissions/src/index.js +5 -0
  317. package/dist/packages/bunshot-permissions/src/lib/bootstrap.d.ts +7 -0
  318. package/dist/packages/bunshot-permissions/src/lib/bootstrap.js +12 -0
  319. package/dist/packages/bunshot-permissions/src/lib/evaluator.d.ts +10 -0
  320. package/dist/packages/bunshot-permissions/src/lib/evaluator.js +165 -0
  321. package/dist/packages/bunshot-permissions/src/lib/registry.d.ts +2 -0
  322. package/dist/packages/bunshot-permissions/src/lib/registry.js +31 -0
  323. package/dist/packages/bunshot-permissions/src/lib/validation.d.ts +1 -0
  324. package/dist/packages/bunshot-permissions/src/lib/validation.js +1 -0
  325. package/dist/packages/bunshot-permissions/src/types/adapter.d.ts +1 -0
  326. package/dist/packages/bunshot-permissions/src/types/adapter.js +1 -0
  327. package/dist/packages/bunshot-permissions/src/types/evaluator.d.ts +1 -0
  328. package/dist/packages/bunshot-permissions/src/types/evaluator.js +1 -0
  329. package/dist/packages/bunshot-permissions/src/types/models.d.ts +1 -0
  330. package/dist/packages/bunshot-permissions/src/types/models.js +1 -0
  331. package/dist/packages/bunshot-permissions/src/types/registry.d.ts +1 -0
  332. package/dist/packages/bunshot-permissions/src/types/registry.js +1 -0
  333. package/dist/packages/bunshot-postgres/src/adapter.d.ts +6 -0
  334. package/dist/packages/bunshot-postgres/src/adapter.js +794 -0
  335. package/dist/packages/bunshot-postgres/src/connection.d.ts +15 -0
  336. package/dist/packages/bunshot-postgres/src/connection.js +16 -0
  337. package/dist/packages/bunshot-postgres/src/index.d.ts +4 -0
  338. package/dist/packages/bunshot-postgres/src/index.js +2 -0
  339. package/dist/packages/bunshot-postgres/src/schema.d.ts +997 -0
  340. package/dist/packages/bunshot-postgres/src/schema.js +105 -0
  341. package/dist/src/app.d.ts +230 -0
  342. package/dist/src/app.js +182 -0
  343. package/dist/src/cli/commands/init.d.ts +10 -0
  344. package/dist/src/cli/commands/init.js +709 -0
  345. package/dist/src/cli/index.d.ts +1 -0
  346. package/dist/src/cli/index.js +3 -0
  347. package/dist/src/entrypoints/mongo.d.ts +6 -0
  348. package/dist/src/entrypoints/mongo.js +4 -0
  349. package/dist/src/entrypoints/queue.d.ts +2 -0
  350. package/dist/src/entrypoints/queue.js +1 -0
  351. package/dist/src/entrypoints/redis.d.ts +1 -0
  352. package/dist/src/entrypoints/redis.js +1 -0
  353. package/dist/{adapters → src/framework/adapters}/localStorage.d.ts +1 -1
  354. package/dist/{adapters → src/framework/adapters}/localStorage.js +10 -10
  355. package/dist/src/framework/adapters/memoryStorage.d.ts +2 -0
  356. package/dist/src/framework/adapters/memoryStorage.js +45 -0
  357. package/dist/{adapters → src/framework/adapters}/s3Storage.d.ts +1 -1
  358. package/dist/{adapters → src/framework/adapters}/s3Storage.js +12 -12
  359. package/dist/src/framework/admin/bunshotAccess.d.ts +2 -0
  360. package/dist/src/framework/admin/bunshotAccess.js +23 -0
  361. package/dist/src/framework/admin/bunshotUsers.d.ts +2 -0
  362. package/dist/src/framework/admin/bunshotUsers.js +103 -0
  363. package/dist/src/framework/admin/index.d.ts +7 -0
  364. package/dist/src/framework/admin/index.js +21 -0
  365. package/dist/src/framework/boundaryAdapters/cacheFactories.d.ts +13 -0
  366. package/dist/src/framework/boundaryAdapters/cacheFactories.js +86 -0
  367. package/dist/src/framework/boundaryAdapters/index.d.ts +2 -0
  368. package/dist/src/framework/boundaryAdapters/index.js +1 -0
  369. package/dist/src/framework/boundaryAdapters.d.ts +17 -0
  370. package/dist/src/framework/boundaryAdapters.js +62 -0
  371. package/dist/src/framework/buildContext.d.ts +33 -0
  372. package/dist/src/framework/buildContext.js +119 -0
  373. package/dist/src/framework/config/schema.d.ts +447 -0
  374. package/dist/src/framework/config/schema.js +528 -0
  375. package/dist/src/framework/createInfrastructure.d.ts +76 -0
  376. package/dist/src/framework/createInfrastructure.js +221 -0
  377. package/dist/src/framework/lib/auditLog.d.ts +23 -0
  378. package/dist/src/framework/lib/auditLog.js +416 -0
  379. package/dist/src/framework/lib/captcha.d.ts +11 -0
  380. package/dist/{lib → src/framework/lib}/captcha.js +13 -10
  381. package/dist/{lib → src/framework/lib}/createDtoMapper.js +4 -4
  382. package/dist/src/framework/lib/createRoute.d.ts +1 -0
  383. package/dist/src/framework/lib/createRoute.js +2 -0
  384. package/dist/{lib → src/framework/lib}/idempotency.d.ts +2 -6
  385. package/dist/src/framework/lib/idempotency.js +74 -0
  386. package/dist/src/framework/lib/logger.d.ts +3 -0
  387. package/dist/src/framework/lib/logger.js +14 -0
  388. package/dist/src/framework/lib/metrics.d.ts +34 -0
  389. package/dist/{lib → src/framework/lib}/metrics.js +49 -57
  390. package/dist/src/framework/lib/pagination.d.ts +42 -0
  391. package/dist/src/framework/lib/pagination.js +51 -0
  392. package/dist/src/framework/lib/redisTransport.d.ts +38 -0
  393. package/dist/src/framework/lib/redisTransport.js +107 -0
  394. package/dist/src/framework/lib/resolveUserId.d.ts +2 -0
  395. package/dist/src/framework/lib/resolveUserId.js +5 -0
  396. package/dist/src/framework/lib/sseCollision.d.ts +6 -0
  397. package/dist/src/framework/lib/sseCollision.js +26 -0
  398. package/dist/src/framework/lib/storageAdapter.d.ts +1 -0
  399. package/dist/src/framework/lib/storageAdapter.js +1 -0
  400. package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.js +4 -4
  401. package/dist/src/framework/lib/tenant.d.ts +21 -0
  402. package/dist/src/framework/lib/tenant.js +70 -0
  403. package/dist/{lib → src/framework/lib}/upload.d.ts +11 -10
  404. package/dist/src/framework/lib/upload.js +132 -0
  405. package/dist/src/framework/lib/uploadRegistry.d.ts +23 -0
  406. package/dist/src/framework/lib/uploadRegistry.js +34 -0
  407. package/dist/{lib → src/framework/lib}/validate.d.ts +1 -1
  408. package/dist/{lib → src/framework/lib}/validate.js +2 -2
  409. package/dist/src/framework/lib/ws.d.ts +19 -0
  410. package/dist/src/framework/lib/ws.js +130 -0
  411. package/dist/src/framework/lib/wsHeartbeat.d.ts +12 -0
  412. package/dist/src/framework/lib/wsHeartbeat.js +53 -0
  413. package/dist/src/framework/lib/wsMessages.d.ts +25 -0
  414. package/dist/src/framework/lib/wsMessages.js +45 -0
  415. package/dist/src/framework/lib/wsNamespace.d.ts +17 -0
  416. package/dist/src/framework/lib/wsNamespace.js +19 -0
  417. package/dist/src/framework/lib/wsPresence.d.ts +17 -0
  418. package/dist/src/framework/lib/wsPresence.js +84 -0
  419. package/dist/src/framework/lib/wsTransport.d.ts +38 -0
  420. package/dist/src/framework/lib/wsTransport.js +9 -0
  421. package/dist/{lib → src/framework/lib}/zodToMongoose.d.ts +1 -1
  422. package/dist/{lib → src/framework/lib}/zodToMongoose.js +11 -11
  423. package/dist/{middleware → src/framework/middleware}/auditLog.d.ts +4 -3
  424. package/dist/src/framework/middleware/auditLog.js +42 -0
  425. package/dist/{middleware → src/framework/middleware}/botProtection.d.ts +2 -2
  426. package/dist/{middleware → src/framework/middleware}/botProtection.js +8 -9
  427. package/dist/src/framework/middleware/cacheResponse.d.ts +35 -0
  428. package/dist/src/framework/middleware/cacheResponse.js +126 -0
  429. package/dist/{middleware → src/framework/middleware}/captcha.d.ts +2 -3
  430. package/dist/src/framework/middleware/captcha.js +37 -0
  431. package/dist/{middleware → src/framework/middleware}/errorHandler.d.ts +1 -1
  432. package/dist/{middleware → src/framework/middleware}/errorHandler.js +2 -2
  433. package/dist/src/framework/middleware/index.js +1 -0
  434. package/dist/{middleware → src/framework/middleware}/logger.d.ts +1 -1
  435. package/dist/src/framework/middleware/metrics.d.ts +12 -0
  436. package/dist/src/framework/middleware/metrics.js +26 -0
  437. package/dist/{middleware → src/framework/middleware}/rateLimit.d.ts +2 -2
  438. package/dist/src/framework/middleware/rateLimit.js +22 -0
  439. package/dist/src/framework/middleware/requestId.d.ts +3 -0
  440. package/dist/{middleware → src/framework/middleware}/requestId.js +2 -2
  441. package/dist/{middleware → src/framework/middleware}/requestLogger.d.ts +3 -3
  442. package/dist/{middleware → src/framework/middleware}/requestLogger.js +17 -12
  443. package/dist/{middleware → src/framework/middleware}/requestSigning.d.ts +2 -2
  444. package/dist/{middleware → src/framework/middleware}/requestSigning.js +18 -20
  445. package/dist/src/framework/middleware/tenant.d.ts +14 -0
  446. package/dist/{middleware → src/framework/middleware}/tenant.js +31 -27
  447. package/dist/src/framework/middleware/upload.d.ts +5 -0
  448. package/dist/{middleware → src/framework/middleware}/upload.js +4 -4
  449. package/dist/{middleware → src/framework/middleware}/webhookAuth.d.ts +3 -3
  450. package/dist/{middleware → src/framework/middleware}/webhookAuth.js +11 -12
  451. package/dist/src/framework/models/AuditLog.d.ts +21 -0
  452. package/dist/src/framework/models/AuditLog.js +31 -0
  453. package/dist/src/framework/mountMiddleware.d.ts +91 -0
  454. package/dist/src/framework/mountMiddleware.js +128 -0
  455. package/dist/src/framework/mountOptionalEndpoints.d.ts +103 -0
  456. package/dist/src/framework/mountOptionalEndpoints.js +47 -0
  457. package/dist/src/framework/mountRoutes.d.ts +21 -0
  458. package/dist/src/framework/mountRoutes.js +144 -0
  459. package/dist/src/framework/persistence/cronRegistry.d.ts +28 -0
  460. package/dist/src/framework/persistence/cronRegistry.js +139 -0
  461. package/dist/src/framework/persistence/idempotency.d.ts +26 -0
  462. package/dist/src/framework/persistence/idempotency.js +178 -0
  463. package/dist/src/framework/persistence/index.d.ts +6 -0
  464. package/dist/src/framework/persistence/index.js +8 -0
  465. package/dist/src/framework/persistence/storeInfra.d.ts +9 -0
  466. package/dist/src/framework/persistence/storeInfra.js +1 -0
  467. package/dist/src/framework/persistence/uploadRegistry.d.ts +35 -0
  468. package/dist/src/framework/persistence/uploadRegistry.js +235 -0
  469. package/dist/src/framework/persistence/wsMessages.d.ts +22 -0
  470. package/dist/src/framework/persistence/wsMessages.js +296 -0
  471. package/dist/src/framework/preloadSchemas.d.ts +24 -0
  472. package/dist/src/framework/preloadSchemas.js +42 -0
  473. package/dist/src/framework/registerBoundaryAdapters.d.ts +23 -0
  474. package/dist/src/framework/registerBoundaryAdapters.js +46 -0
  475. package/dist/src/framework/routes/admin.d.ts +9 -0
  476. package/dist/src/framework/routes/admin.js +361 -0
  477. package/dist/src/framework/routes/health.d.ts +1 -0
  478. package/dist/src/framework/routes/health.js +21 -0
  479. package/dist/src/framework/routes/home.d.ts +1 -0
  480. package/dist/src/framework/routes/home.js +18 -0
  481. package/dist/src/framework/routes/jobs.d.ts +3 -0
  482. package/dist/{routes → src/framework/routes}/jobs.js +128 -103
  483. package/dist/src/framework/routes/metrics.d.ts +10 -0
  484. package/dist/src/framework/routes/metrics.js +57 -0
  485. package/dist/{routes → src/framework/routes}/uploads.d.ts +3 -3
  486. package/dist/src/framework/routes/uploads.js +262 -0
  487. package/dist/src/framework/runPluginLifecycle.d.ts +27 -0
  488. package/dist/src/framework/runPluginLifecycle.js +121 -0
  489. package/dist/src/framework/secrets/frameworkSecretSchema.d.ts +58 -0
  490. package/dist/src/framework/secrets/frameworkSecretSchema.js +20 -0
  491. package/dist/src/framework/secrets/index.d.ts +9 -0
  492. package/dist/src/framework/secrets/index.js +7 -0
  493. package/dist/src/framework/secrets/providers/envProvider.d.ts +15 -0
  494. package/dist/src/framework/secrets/providers/envProvider.js +18 -0
  495. package/dist/src/framework/secrets/providers/fileProvider.d.ts +8 -0
  496. package/dist/src/framework/secrets/providers/fileProvider.js +82 -0
  497. package/dist/src/framework/secrets/providers/ssmProvider.d.ts +20 -0
  498. package/dist/src/framework/secrets/providers/ssmProvider.js +127 -0
  499. package/dist/src/framework/secrets/resolveSecretBundle.d.ts +53 -0
  500. package/dist/src/framework/secrets/resolveSecretBundle.js +84 -0
  501. package/dist/src/framework/secrets/resolveSecrets.d.ts +18 -0
  502. package/dist/src/framework/secrets/resolveSecrets.js +34 -0
  503. package/dist/src/framework/sse/index.d.ts +21 -0
  504. package/dist/src/framework/sse/index.js +109 -0
  505. package/dist/src/framework/ws/index.d.ts +11 -0
  506. package/dist/src/framework/ws/index.js +8 -0
  507. package/dist/src/index.d.ts +87 -0
  508. package/dist/src/index.js +58 -0
  509. package/dist/src/lib/appConfig.d.ts +7 -0
  510. package/dist/src/lib/appConfig.js +27 -0
  511. package/dist/src/lib/appMeta.d.ts +7 -0
  512. package/dist/src/lib/appMeta.js +3 -0
  513. package/dist/src/lib/authConfig.d.ts +532 -0
  514. package/dist/{lib/appConfig.js → src/lib/authConfig.js} +75 -17
  515. package/dist/{lib → src/lib}/context.d.ts +6 -12
  516. package/dist/{lib → src/lib}/context.js +5 -5
  517. package/dist/src/lib/logger.d.ts +1 -0
  518. package/dist/src/lib/logger.js +1 -0
  519. package/dist/src/lib/mongo.d.ts +58 -0
  520. package/dist/src/lib/mongo.js +96 -0
  521. package/dist/src/lib/queue.d.ts +72 -0
  522. package/dist/src/lib/queue.js +152 -0
  523. package/dist/src/lib/redis.d.ts +28 -0
  524. package/dist/src/lib/redis.js +72 -0
  525. package/dist/{lib → src/lib}/signing.d.ts +2 -2
  526. package/dist/src/lib/signing.js +210 -0
  527. package/dist/src/lib/signingConfig.d.ts +40 -0
  528. package/dist/src/lib/signingConfig.js +28 -0
  529. package/dist/src/server.d.ts +146 -0
  530. package/dist/src/server.js +469 -0
  531. package/dist/src/shared/lib/HttpError.d.ts +1 -0
  532. package/dist/src/shared/lib/HttpError.js +2 -0
  533. package/dist/src/shared/lib/constants.d.ts +10 -0
  534. package/dist/src/shared/lib/crypto.d.ts +43 -0
  535. package/dist/src/shared/lib/crypto.js +74 -0
  536. package/dist/src/shared/lib/signing.d.ts +52 -0
  537. package/dist/{lib → src/shared/lib}/signing.js +35 -8
  538. package/dist/src/testing.d.ts +34 -0
  539. package/dist/src/testing.js +93 -0
  540. package/package.json +60 -24
  541. package/dist/adapters/memoryAuth.d.ts +0 -52
  542. package/dist/adapters/memoryAuth.js +0 -749
  543. package/dist/adapters/memoryStorage.d.ts +0 -3
  544. package/dist/adapters/memoryStorage.js +0 -44
  545. package/dist/adapters/mongoAuth.d.ts +0 -2
  546. package/dist/adapters/mongoAuth.js +0 -403
  547. package/dist/adapters/sqliteAuth.d.ts +0 -72
  548. package/dist/adapters/sqliteAuth.js +0 -858
  549. package/dist/app.d.ts +0 -559
  550. package/dist/app.js +0 -651
  551. package/dist/entrypoints/mongo.d.ts +0 -5
  552. package/dist/entrypoints/mongo.js +0 -4
  553. package/dist/entrypoints/queue.d.ts +0 -2
  554. package/dist/entrypoints/queue.js +0 -1
  555. package/dist/entrypoints/redis.d.ts +0 -1
  556. package/dist/entrypoints/redis.js +0 -1
  557. package/dist/index.d.ts +0 -117
  558. package/dist/index.js +0 -88
  559. package/dist/lib/appConfig.d.ts +0 -275
  560. package/dist/lib/auditLog.d.ts +0 -58
  561. package/dist/lib/auditLog.js +0 -218
  562. package/dist/lib/authAdapter.d.ts +0 -246
  563. package/dist/lib/authAdapter.js +0 -7
  564. package/dist/lib/authRateLimit.d.ts +0 -13
  565. package/dist/lib/authRateLimit.js +0 -117
  566. package/dist/lib/clientIp.d.ts +0 -14
  567. package/dist/lib/credentialStuffing.d.ts +0 -31
  568. package/dist/lib/credentialStuffing.js +0 -77
  569. package/dist/lib/crypto.d.ts +0 -11
  570. package/dist/lib/crypto.js +0 -22
  571. package/dist/lib/deletionCancelToken.d.ts +0 -12
  572. package/dist/lib/deletionCancelToken.js +0 -88
  573. package/dist/lib/emailVerification.d.ts +0 -19
  574. package/dist/lib/emailVerification.js +0 -129
  575. package/dist/lib/fingerprint.js +0 -36
  576. package/dist/lib/idempotency.js +0 -182
  577. package/dist/lib/jwks.d.ts +0 -25
  578. package/dist/lib/jwks.js +0 -51
  579. package/dist/lib/jwt.d.ts +0 -15
  580. package/dist/lib/jwt.js +0 -111
  581. package/dist/lib/metrics.d.ts +0 -14
  582. package/dist/lib/mfaChallenge.d.ts +0 -55
  583. package/dist/lib/mfaChallenge.js +0 -398
  584. package/dist/lib/mongo.d.ts +0 -39
  585. package/dist/lib/mongo.js +0 -124
  586. package/dist/lib/oauth.d.ts +0 -40
  587. package/dist/lib/oauth.js +0 -101
  588. package/dist/lib/oauthCode.d.ts +0 -15
  589. package/dist/lib/oauthCode.js +0 -95
  590. package/dist/lib/pagination.d.ts +0 -119
  591. package/dist/lib/pagination.js +0 -166
  592. package/dist/lib/queue.d.ts +0 -37
  593. package/dist/lib/queue.js +0 -117
  594. package/dist/lib/redis.d.ts +0 -9
  595. package/dist/lib/redis.js +0 -61
  596. package/dist/lib/resetPassword.d.ts +0 -12
  597. package/dist/lib/resetPassword.js +0 -93
  598. package/dist/lib/roles.d.ts +0 -7
  599. package/dist/lib/roles.js +0 -49
  600. package/dist/lib/saml.d.ts +0 -25
  601. package/dist/lib/saml.js +0 -64
  602. package/dist/lib/securityEvents.d.ts +0 -28
  603. package/dist/lib/securityEvents.js +0 -26
  604. package/dist/lib/session.d.ts +0 -49
  605. package/dist/lib/session.js +0 -597
  606. package/dist/lib/tenant.d.ts +0 -15
  607. package/dist/lib/tenant.js +0 -65
  608. package/dist/lib/upload.js +0 -112
  609. package/dist/lib/uploadRegistry.d.ts +0 -18
  610. package/dist/lib/uploadRegistry.js +0 -83
  611. package/dist/lib/ws.d.ts +0 -22
  612. package/dist/lib/ws.js +0 -96
  613. package/dist/lib/wsHeartbeat.d.ts +0 -12
  614. package/dist/lib/wsHeartbeat.js +0 -57
  615. package/dist/lib/wsMessages.d.ts +0 -40
  616. package/dist/lib/wsMessages.js +0 -330
  617. package/dist/lib/wsPresence.d.ts +0 -25
  618. package/dist/lib/wsPresence.js +0 -99
  619. package/dist/middleware/auditLog.js +0 -39
  620. package/dist/middleware/bearerAuth.d.ts +0 -2
  621. package/dist/middleware/bearerAuth.js +0 -11
  622. package/dist/middleware/cacheResponse.d.ts +0 -15
  623. package/dist/middleware/cacheResponse.js +0 -178
  624. package/dist/middleware/captcha.js +0 -36
  625. package/dist/middleware/csrf.js +0 -129
  626. package/dist/middleware/identify.d.ts +0 -3
  627. package/dist/middleware/identify.js +0 -122
  628. package/dist/middleware/index.js +0 -1
  629. package/dist/middleware/metrics.d.ts +0 -9
  630. package/dist/middleware/metrics.js +0 -26
  631. package/dist/middleware/rateLimit.js +0 -22
  632. package/dist/middleware/requestId.d.ts +0 -3
  633. package/dist/middleware/scimAuth.d.ts +0 -8
  634. package/dist/middleware/scimAuth.js +0 -29
  635. package/dist/middleware/tenant.d.ts +0 -5
  636. package/dist/middleware/upload.d.ts +0 -5
  637. package/dist/middleware/userAuth.d.ts +0 -3
  638. package/dist/middleware/userAuth.js +0 -6
  639. package/dist/models/AuditLog.d.ts +0 -30
  640. package/dist/models/AuditLog.js +0 -39
  641. package/dist/models/AuthUser.js +0 -55
  642. package/dist/models/Group.d.ts +0 -21
  643. package/dist/models/Group.js +0 -28
  644. package/dist/models/GroupMembership.js +0 -25
  645. package/dist/models/TenantRole.d.ts +0 -15
  646. package/dist/models/TenantRole.js +0 -23
  647. package/dist/routes/auth.d.ts +0 -12
  648. package/dist/routes/auth.js +0 -744
  649. package/dist/routes/groups.js +0 -346
  650. package/dist/routes/health.d.ts +0 -1
  651. package/dist/routes/health.js +0 -22
  652. package/dist/routes/home.d.ts +0 -1
  653. package/dist/routes/home.js +0 -16
  654. package/dist/routes/jobs.d.ts +0 -2
  655. package/dist/routes/m2m.d.ts +0 -2
  656. package/dist/routes/m2m.js +0 -72
  657. package/dist/routes/metrics.d.ts +0 -8
  658. package/dist/routes/metrics.js +0 -55
  659. package/dist/routes/mfa.d.ts +0 -5
  660. package/dist/routes/mfa.js +0 -628
  661. package/dist/routes/oauth.d.ts +0 -2
  662. package/dist/routes/oauth.js +0 -520
  663. package/dist/routes/oidc.d.ts +0 -2
  664. package/dist/routes/oidc.js +0 -29
  665. package/dist/routes/passkey.d.ts +0 -1
  666. package/dist/routes/passkey.js +0 -157
  667. package/dist/routes/saml.d.ts +0 -2
  668. package/dist/routes/saml.js +0 -86
  669. package/dist/routes/scim.d.ts +0 -2
  670. package/dist/routes/scim.js +0 -255
  671. package/dist/routes/uploads.js +0 -227
  672. package/dist/schemas/auth.js +0 -30
  673. package/dist/server.d.ts +0 -57
  674. package/dist/server.js +0 -112
  675. package/dist/services/auth.d.ts +0 -29
  676. package/dist/services/auth.js +0 -238
  677. package/dist/ws/index.d.ts +0 -10
  678. package/dist/ws/index.js +0 -39
  679. package/docs/sections/adding-middleware/full.md +0 -35
  680. package/docs/sections/adding-models/full.md +0 -125
  681. package/docs/sections/adding-models/overview.md +0 -13
  682. package/docs/sections/adding-routes/full.md +0 -182
  683. package/docs/sections/adding-routes/overview.md +0 -23
  684. package/docs/sections/auth-flow/full.md +0 -790
  685. package/docs/sections/auth-flow/overview.md +0 -10
  686. package/docs/sections/auth-security-examples/full.md +0 -388
  687. package/docs/sections/authentication/full.md +0 -130
  688. package/docs/sections/authentication/overview.md +0 -5
  689. package/docs/sections/cli/full.md +0 -42
  690. package/docs/sections/configuration/full.md +0 -172
  691. package/docs/sections/configuration/overview.md +0 -18
  692. package/docs/sections/configuration-example/full.md +0 -117
  693. package/docs/sections/configuration-example/overview.md +0 -30
  694. package/docs/sections/documentation/full.md +0 -171
  695. package/docs/sections/environment-variables/full.md +0 -55
  696. package/docs/sections/exports/full.md +0 -123
  697. package/docs/sections/extending-context/full.md +0 -59
  698. package/docs/sections/header.md +0 -3
  699. package/docs/sections/installation/full.md +0 -6
  700. package/docs/sections/jobs/full.md +0 -140
  701. package/docs/sections/jobs/overview.md +0 -15
  702. package/docs/sections/logging/full.md +0 -83
  703. package/docs/sections/metrics/full.md +0 -131
  704. package/docs/sections/mongodb-connections/full.md +0 -45
  705. package/docs/sections/mongodb-connections/overview.md +0 -7
  706. package/docs/sections/multi-tenancy/full.md +0 -66
  707. package/docs/sections/multi-tenancy/overview.md +0 -15
  708. package/docs/sections/oauth/full.md +0 -189
  709. package/docs/sections/oauth/overview.md +0 -16
  710. package/docs/sections/package-development/full.md +0 -7
  711. package/docs/sections/pagination/full.md +0 -93
  712. package/docs/sections/passkey-login/full.md +0 -90
  713. package/docs/sections/passkey-login/overview.md +0 -1
  714. package/docs/sections/peer-dependencies/full.md +0 -47
  715. package/docs/sections/quick-start/full.md +0 -43
  716. package/docs/sections/response-caching/full.md +0 -117
  717. package/docs/sections/response-caching/overview.md +0 -13
  718. package/docs/sections/roles/full.md +0 -225
  719. package/docs/sections/roles/overview.md +0 -14
  720. package/docs/sections/running-without-redis/full.md +0 -16
  721. package/docs/sections/running-without-redis-or-mongodb/full.md +0 -60
  722. package/docs/sections/signing/full.md +0 -203
  723. package/docs/sections/stack/full.md +0 -10
  724. package/docs/sections/uploads/full.md +0 -208
  725. package/docs/sections/versioning/full.md +0 -85
  726. package/docs/sections/webhook-auth/full.md +0 -100
  727. package/docs/sections/websocket/full.md +0 -196
  728. package/docs/sections/websocket/overview.md +0 -5
  729. package/docs/sections/websocket-rooms/full.md +0 -102
  730. package/docs/sections/websocket-rooms/overview.md +0 -5
  731. /package/dist/{lib/storageAdapter.js → packages/bunshot-admin/src/types/env.js} +0 -0
  732. /package/dist/{lib → packages/bunshot-auth/src/lib}/fingerprint.d.ts +0 -0
  733. /package/dist/{lib → packages/bunshot-auth/src/lib}/logger.d.ts +0 -0
  734. /package/dist/{lib → packages/bunshot-core/src}/constants.d.ts +0 -0
  735. /package/dist/{lib → packages/bunshot-core/src}/storageAdapter.d.ts +0 -0
  736. /package/dist/{lib → src/framework/lib}/createDtoMapper.d.ts +0 -0
  737. /package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.d.ts +0 -0
  738. /package/dist/{middleware → src/framework/middleware}/cors.d.ts +0 -0
  739. /package/dist/{middleware → src/framework/middleware}/cors.js +0 -0
  740. /package/dist/{middleware → src/framework/middleware}/index.d.ts +0 -0
  741. /package/dist/{middleware → src/framework/middleware}/logger.js +0 -0
  742. /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
+ };