@udibo/oauth2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +174 -0
  3. package/SECURITY.md +77 -0
  4. package/esm/_dnt.polyfills.d.ts +17 -0
  5. package/esm/_dnt.polyfills.js +15 -0
  6. package/esm/adapters/hono/_common.d.ts +66 -0
  7. package/esm/adapters/hono/_common.js +108 -0
  8. package/esm/adapters/hono/authorization-server.d.ts +211 -0
  9. package/esm/adapters/hono/authorization-server.js +214 -0
  10. package/esm/adapters/hono/bff/auth-request-store.d.ts +164 -0
  11. package/esm/adapters/hono/bff/auth-request-store.js +154 -0
  12. package/esm/adapters/hono/bff/bff.d.ts +852 -0
  13. package/esm/adapters/hono/bff/bff.js +1194 -0
  14. package/esm/adapters/hono/bff/cookie-prefix.d.ts +72 -0
  15. package/esm/adapters/hono/bff/cookie-prefix.js +85 -0
  16. package/esm/adapters/hono/bff/mod.d.ts +19 -0
  17. package/esm/adapters/hono/bff/mod.js +19 -0
  18. package/esm/adapters/hono/bff/proxy.d.ts +108 -0
  19. package/esm/adapters/hono/bff/proxy.js +314 -0
  20. package/esm/adapters/hono/bff/session-store.d.ts +250 -0
  21. package/esm/adapters/hono/bff/session-store.js +216 -0
  22. package/esm/adapters/hono/bff/testing.d.ts +165 -0
  23. package/esm/adapters/hono/bff/testing.js +254 -0
  24. package/esm/adapters/hono/identity.d.ts +117 -0
  25. package/esm/adapters/hono/identity.js +215 -0
  26. package/esm/adapters/hono/log.d.ts +49 -0
  27. package/esm/adapters/hono/log.js +66 -0
  28. package/esm/adapters/hono/resource-server.d.ts +101 -0
  29. package/esm/adapters/hono/resource-server.js +108 -0
  30. package/esm/client/_http.d.ts +91 -0
  31. package/esm/client/_http.js +303 -0
  32. package/esm/client/base.d.ts +184 -0
  33. package/esm/client/base.js +93 -0
  34. package/esm/client/bff-client.d.ts +235 -0
  35. package/esm/client/bff-client.js +280 -0
  36. package/esm/client/browser-storage.d.ts +152 -0
  37. package/esm/client/browser-storage.js +246 -0
  38. package/esm/client/check.d.ts +114 -0
  39. package/esm/client/check.js +97 -0
  40. package/esm/client/direct-client.d.ts +707 -0
  41. package/esm/client/direct-client.js +1237 -0
  42. package/esm/client/discovery-cache.d.ts +138 -0
  43. package/esm/client/discovery-cache.js +124 -0
  44. package/esm/client/events.d.ts +54 -0
  45. package/esm/client/events.js +37 -0
  46. package/esm/client/mod.d.ts +35 -0
  47. package/esm/client/mod.js +33 -0
  48. package/esm/client/storage.d.ts +127 -0
  49. package/esm/client/storage.js +81 -0
  50. package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
  51. package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
  52. package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
  53. package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
  54. package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
  55. package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
  56. package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
  57. package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
  58. package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
  59. package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +193 -0
  60. package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
  61. package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
  62. package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
  63. package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
  64. package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
  65. package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
  66. package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
  67. package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
  68. package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
  69. package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
  70. package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
  71. package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
  72. package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
  73. package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
  74. package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
  75. package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
  76. package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
  77. package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
  78. package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
  79. package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
  80. package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
  81. package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
  82. package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
  83. package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
  84. package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
  85. package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
  86. package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
  87. package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
  88. package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
  89. package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
  90. package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
  91. package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
  92. package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
  93. package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
  94. package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
  95. package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
  96. package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
  97. package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
  98. package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
  99. package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
  100. package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
  101. package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
  102. package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
  103. package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
  104. package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
  105. package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
  106. package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +44 -0
  107. package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +84 -0
  108. package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +39 -0
  109. package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
  110. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +22 -0
  111. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +51 -0
  112. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +34 -0
  113. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +192 -0
  114. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +34 -0
  115. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +113 -0
  116. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +3 -0
  117. package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +13 -0
  118. package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +8 -0
  119. package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +2 -0
  120. package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +1 -0
  121. package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +26 -0
  122. package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +60 -0
  123. package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +152 -0
  124. package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +39 -0
  125. package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +87 -0
  126. package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +39 -0
  127. package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +131 -0
  128. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +39 -0
  129. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +82 -0
  130. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +39 -0
  131. package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +72 -0
  132. package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +38 -0
  133. package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +87 -0
  134. package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +97 -0
  135. package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +99 -0
  136. package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +119 -0
  137. package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +209 -0
  138. package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +343 -0
  139. package/esm/deps/jsr.io/@std/http/1.1.3/status.js +343 -0
  140. package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +159 -0
  141. package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +223 -0
  142. package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
  143. package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
  144. package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
  145. package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
  146. package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
  147. package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
  148. package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +29 -0
  149. package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +68 -0
  150. package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
  151. package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +206 -0
  152. package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
  153. package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
  154. package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +91 -0
  155. package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +347 -0
  156. package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +382 -0
  157. package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +1110 -0
  158. package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +363 -0
  159. package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +616 -0
  160. package/esm/errors.d.ts +203 -0
  161. package/esm/errors.js +287 -0
  162. package/esm/identity/captcha.d.ts +147 -0
  163. package/esm/identity/captcha.js +64 -0
  164. package/esm/identity/delivery.d.ts +96 -0
  165. package/esm/identity/delivery.js +41 -0
  166. package/esm/identity/errors.d.ts +39 -0
  167. package/esm/identity/errors.js +46 -0
  168. package/esm/identity/events.d.ts +242 -0
  169. package/esm/identity/events.js +39 -0
  170. package/esm/identity/external/_shared.d.ts +86 -0
  171. package/esm/identity/external/_shared.js +107 -0
  172. package/esm/identity/external/_token-exchange.d.ts +61 -0
  173. package/esm/identity/external/_token-exchange.js +179 -0
  174. package/esm/identity/external/apple-client-secret.d.ts +82 -0
  175. package/esm/identity/external/apple-client-secret.js +133 -0
  176. package/esm/identity/external/apple.d.ts +64 -0
  177. package/esm/identity/external/apple.js +248 -0
  178. package/esm/identity/external/discord.d.ts +38 -0
  179. package/esm/identity/external/discord.js +62 -0
  180. package/esm/identity/external/errors.d.ts +48 -0
  181. package/esm/identity/external/errors.js +36 -0
  182. package/esm/identity/external/flow.d.ts +107 -0
  183. package/esm/identity/external/flow.js +158 -0
  184. package/esm/identity/external/github.d.ts +42 -0
  185. package/esm/identity/external/github.js +164 -0
  186. package/esm/identity/external/google.d.ts +52 -0
  187. package/esm/identity/external/google.js +38 -0
  188. package/esm/identity/external/mod.d.ts +63 -0
  189. package/esm/identity/external/mod.js +61 -0
  190. package/esm/identity/external/oauth2.d.ts +110 -0
  191. package/esm/identity/external/oauth2.js +158 -0
  192. package/esm/identity/external/oidc.d.ts +81 -0
  193. package/esm/identity/external/oidc.js +250 -0
  194. package/esm/identity/external/provider.d.ts +132 -0
  195. package/esm/identity/external/provider.js +12 -0
  196. package/esm/identity/hibp.d.ts +67 -0
  197. package/esm/identity/hibp.js +87 -0
  198. package/esm/identity/identifier.d.ts +44 -0
  199. package/esm/identity/identifier.js +47 -0
  200. package/esm/identity/lockout.d.ts +155 -0
  201. package/esm/identity/lockout.js +117 -0
  202. package/esm/identity/mfa/mod.d.ts +42 -0
  203. package/esm/identity/mfa/mod.js +42 -0
  204. package/esm/identity/mfa/recovery-codes.d.ts +96 -0
  205. package/esm/identity/mfa/recovery-codes.js +80 -0
  206. package/esm/identity/mfa/service.d.ts +338 -0
  207. package/esm/identity/mfa/service.js +351 -0
  208. package/esm/identity/mfa/totp.d.ts +186 -0
  209. package/esm/identity/mfa/totp.js +194 -0
  210. package/esm/identity/migration.d.ts +155 -0
  211. package/esm/identity/migration.js +243 -0
  212. package/esm/identity/mod.d.ts +65 -0
  213. package/esm/identity/mod.js +64 -0
  214. package/esm/identity/otp.d.ts +255 -0
  215. package/esm/identity/otp.js +245 -0
  216. package/esm/identity/password-policy.d.ts +45 -0
  217. package/esm/identity/password-policy.js +60 -0
  218. package/esm/identity/password.d.ts +171 -0
  219. package/esm/identity/password.js +160 -0
  220. package/esm/identity/rate-limit.d.ts +192 -0
  221. package/esm/identity/rate-limit.js +168 -0
  222. package/esm/identity/service.d.ts +579 -0
  223. package/esm/identity/service.js +835 -0
  224. package/esm/identity/session.d.ts +122 -0
  225. package/esm/identity/session.js +48 -0
  226. package/esm/identity/token-flow.d.ts +214 -0
  227. package/esm/identity/token-flow.js +173 -0
  228. package/esm/models/authentication.d.ts +24 -0
  229. package/esm/models/authentication.js +1 -0
  230. package/esm/models/authorization-code.d.ts +63 -0
  231. package/esm/models/authorization-code.js +1 -0
  232. package/esm/models/authorization.d.ts +165 -0
  233. package/esm/models/authorization.js +160 -0
  234. package/esm/models/client.d.ts +78 -0
  235. package/esm/models/client.js +1 -0
  236. package/esm/models/device-authorization.d.ts +30 -0
  237. package/esm/models/device-authorization.js +1 -0
  238. package/esm/models/responses.d.ts +93 -0
  239. package/esm/models/responses.js +1 -0
  240. package/esm/models/scope.d.ts +81 -0
  241. package/esm/models/scope.js +125 -0
  242. package/esm/models/token.d.ts +135 -0
  243. package/esm/models/token.js +1 -0
  244. package/esm/package.json +3 -0
  245. package/esm/react/callback.d.ts +52 -0
  246. package/esm/react/callback.js +71 -0
  247. package/esm/react/components/class-names.d.ts +91 -0
  248. package/esm/react/components/class-names.js +118 -0
  249. package/esm/react/components/internal.d.ts +62 -0
  250. package/esm/react/components/internal.js +102 -0
  251. package/esm/react/components/mfa-challenge-form.d.ts +66 -0
  252. package/esm/react/components/mfa-challenge-form.js +46 -0
  253. package/esm/react/components/mfa-enrollment-form.d.ts +83 -0
  254. package/esm/react/components/mfa-enrollment-form.js +41 -0
  255. package/esm/react/components/mod.d.ts +103 -0
  256. package/esm/react/components/mod.js +93 -0
  257. package/esm/react/components/request-password-reset-form.d.ts +61 -0
  258. package/esm/react/components/request-password-reset-form.js +41 -0
  259. package/esm/react/components/reset-password-form.d.ts +66 -0
  260. package/esm/react/components/reset-password-form.js +56 -0
  261. package/esm/react/components/sign-in-form.d.ts +74 -0
  262. package/esm/react/components/sign-in-form.js +43 -0
  263. package/esm/react/components/sign-up-form.d.ts +85 -0
  264. package/esm/react/components/sign-up-form.js +57 -0
  265. package/esm/react/components/types.d.ts +208 -0
  266. package/esm/react/components/types.js +14 -0
  267. package/esm/react/components/use-auth-form.d.ts +118 -0
  268. package/esm/react/components/use-auth-form.js +172 -0
  269. package/esm/react/components/user-menu.d.ts +79 -0
  270. package/esm/react/components/user-menu.js +73 -0
  271. package/esm/react/context.d.ts +51 -0
  272. package/esm/react/context.js +16 -0
  273. package/esm/react/mod.d.ts +26 -0
  274. package/esm/react/mod.js +26 -0
  275. package/esm/react/provider.d.ts +40 -0
  276. package/esm/react/provider.js +189 -0
  277. package/esm/react/require-auth.d.ts +31 -0
  278. package/esm/react/require-auth.js +37 -0
  279. package/esm/react/testing.d.ts +322 -0
  280. package/esm/react/testing.js +461 -0
  281. package/esm/react/use-authorization.d.ts +31 -0
  282. package/esm/react/use-authorization.js +36 -0
  283. package/esm/react/use-oauth2.d.ts +112 -0
  284. package/esm/react/use-oauth2.js +127 -0
  285. package/esm/server/authorization-server.d.ts +736 -0
  286. package/esm/server/authorization-server.js +1304 -0
  287. package/esm/server/client-authentication.d.ts +28 -0
  288. package/esm/server/client-authentication.js +51 -0
  289. package/esm/server/grants/authorization-code.d.ts +179 -0
  290. package/esm/server/grants/authorization-code.js +232 -0
  291. package/esm/server/grants/client-credentials.d.ts +52 -0
  292. package/esm/server/grants/client-credentials.js +53 -0
  293. package/esm/server/grants/device-authorization.d.ts +103 -0
  294. package/esm/server/grants/device-authorization.js +156 -0
  295. package/esm/server/grants/grant.d.ts +165 -0
  296. package/esm/server/grants/grant.js +173 -0
  297. package/esm/server/grants/password.d.ts +80 -0
  298. package/esm/server/grants/password.js +92 -0
  299. package/esm/server/grants/refresh-token.d.ts +80 -0
  300. package/esm/server/grants/refresh-token.js +136 -0
  301. package/esm/server/introspection-token-reader.d.ts +153 -0
  302. package/esm/server/introspection-token-reader.js +164 -0
  303. package/esm/server/jwks-token-reader.d.ts +263 -0
  304. package/esm/server/jwks-token-reader.js +508 -0
  305. package/esm/server/mod.d.ts +23 -0
  306. package/esm/server/mod.js +15 -0
  307. package/esm/server/public-suffix/mod.d.ts +62 -0
  308. package/esm/server/public-suffix/mod.js +103 -0
  309. package/esm/server/public-suffix/rules.d.ts +23 -0
  310. package/esm/server/public-suffix/rules.js +23 -0
  311. package/esm/server/redirect-uri.d.ts +187 -0
  312. package/esm/server/redirect-uri.js +331 -0
  313. package/esm/server/resource-server.d.ts +286 -0
  314. package/esm/server/resource-server.js +365 -0
  315. package/esm/server/services/authorization-code.d.ts +84 -0
  316. package/esm/server/services/authorization-code.js +35 -0
  317. package/esm/server/services/client.d.ts +35 -0
  318. package/esm/server/services/client.js +1 -0
  319. package/esm/server/services/device-authorization.d.ts +134 -0
  320. package/esm/server/services/device-authorization.js +7 -0
  321. package/esm/server/services/token.d.ts +242 -0
  322. package/esm/server/services/token.js +78 -0
  323. package/esm/server/services/user.d.ts +29 -0
  324. package/esm/server/services/user.js +7 -0
  325. package/esm/server/signing-keys.d.ts +201 -0
  326. package/esm/server/signing-keys.js +225 -0
  327. package/esm/server/utils/hash.d.ts +6 -0
  328. package/esm/server/utils/hash.js +11 -0
  329. package/esm/testing/contract/authorization-code.d.ts +37 -0
  330. package/esm/testing/contract/authorization-code.js +93 -0
  331. package/esm/testing/contract/client.d.ts +85 -0
  332. package/esm/testing/contract/client.js +126 -0
  333. package/esm/testing/contract/device-authorization.d.ts +37 -0
  334. package/esm/testing/contract/device-authorization.js +193 -0
  335. package/esm/testing/contract/lockout-store.d.ts +47 -0
  336. package/esm/testing/contract/lockout-store.js +122 -0
  337. package/esm/testing/contract/mfa-store.d.ts +50 -0
  338. package/esm/testing/contract/mfa-store.js +226 -0
  339. package/esm/testing/contract/mod.d.ts +39 -0
  340. package/esm/testing/contract/mod.js +39 -0
  341. package/esm/testing/contract/otp-store.d.ts +48 -0
  342. package/esm/testing/contract/otp-store.js +197 -0
  343. package/esm/testing/contract/rate-limit-store.d.ts +52 -0
  344. package/esm/testing/contract/rate-limit-store.js +105 -0
  345. package/esm/testing/contract/token-flow-store.d.ts +54 -0
  346. package/esm/testing/contract/token-flow-store.js +179 -0
  347. package/esm/testing/contract/token-reader.d.ts +77 -0
  348. package/esm/testing/contract/token-reader.js +78 -0
  349. package/esm/testing/contract/token.d.ts +61 -0
  350. package/esm/testing/contract/token.js +287 -0
  351. package/esm/testing/contract/user.d.ts +63 -0
  352. package/esm/testing/contract/user.js +75 -0
  353. package/esm/testing/mod.d.ts +34 -0
  354. package/esm/testing/mod.js +34 -0
  355. package/esm/testing/server.d.ts +197 -0
  356. package/esm/testing/server.js +193 -0
  357. package/esm/testing/services.d.ts +373 -0
  358. package/esm/testing/services.js +703 -0
  359. package/esm/utils/_buffer.d.ts +10 -0
  360. package/esm/utils/_buffer.js +12 -0
  361. package/esm/utils/_timing-safe.d.ts +13 -0
  362. package/esm/utils/_timing-safe.js +22 -0
  363. package/esm/utils/authentication-context.d.ts +3 -0
  364. package/esm/utils/authentication-context.js +14 -0
  365. package/esm/utils/basic-auth.d.ts +50 -0
  366. package/esm/utils/basic-auth.js +106 -0
  367. package/esm/utils/crypto.d.ts +97 -0
  368. package/esm/utils/crypto.js +162 -0
  369. package/esm/utils/pkce.d.ts +151 -0
  370. package/esm/utils/pkce.js +179 -0
  371. package/esm/utils/text.d.ts +24 -0
  372. package/esm/utils/text.js +28 -0
  373. package/esm/utils/url.d.ts +66 -0
  374. package/esm/utils/url.js +90 -0
  375. package/package.json +92 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The contract between {@link ExternalAuthFlow} and a concrete external
3
+ * provider connector ({@link oidcProvider}, {@link googleProvider},
4
+ * {@link githubProvider}, or your own).
5
+ *
6
+ * A provider's job ends at returning a verified, normalized
7
+ * {@link ExternalProfile}. What the app does with it — create a user, link an
8
+ * account, reject — is app code; the library deliberately stores nothing.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * A verified, normalized profile returned by an external provider after a
14
+ * successful sign-in. This is the connector layer's entire output — account
15
+ * creation/linking is the app's job.
16
+ */
17
+ export interface ExternalProfile {
18
+ /** The provider id the profile came from (e.g. `"google"`). */
19
+ provider: string;
20
+ /**
21
+ * The provider's stable unique id for the user (OIDC `sub`, GitHub user
22
+ * id). Key identities by `(provider, subject)` — never by email, which can
23
+ * change or be reassigned.
24
+ */
25
+ subject: string;
26
+ /** The user's email address, when the provider shared one. */
27
+ email?: string;
28
+ /**
29
+ * `true` only when the provider positively asserted the email is verified
30
+ * (OIDC `email_verified: true`, GitHub verified primary email). Defaults to
31
+ * `false` — treat unverified emails as attacker-controllable input and do
32
+ * not auto-link accounts on them.
33
+ */
34
+ emailVerified: boolean;
35
+ /** Display name, when shared. */
36
+ name?: string;
37
+ /** Given (first) name, when shared. */
38
+ givenName?: string;
39
+ /** Family (last) name, when shared. */
40
+ familyName?: string;
41
+ /** Avatar/profile picture URL, when shared. */
42
+ picture?: string;
43
+ /**
44
+ * The provider's raw claims/profile payload, for app-specific fields the
45
+ * normalized shape omits (e.g. Google's `hd`, GitHub's `login`).
46
+ */
47
+ raw: Record<string, unknown>;
48
+ }
49
+ /**
50
+ * The per-authorization transient state {@link ExternalAuthFlow.start}
51
+ * returns. JSON-serializable by construction — put it in a sealed cookie or
52
+ * your session store and hand it back to {@link ExternalAuthFlow.finish} on
53
+ * the callback. It is the only state the flow needs; the library stores
54
+ * nothing.
55
+ */
56
+ export interface ExternalAuthTransient {
57
+ /** CSRF token echoed back by the provider as the callback `state` param. */
58
+ state: string;
59
+ /** PKCE code verifier, present when the provider uses PKCE. */
60
+ codeVerifier?: string;
61
+ /** OIDC replay-protection nonce, present when the provider uses one. */
62
+ nonce?: string;
63
+ /** The id of the provider the attempt was started with. */
64
+ provider: string;
65
+ /** The redirect URI the authorization request was built with. */
66
+ redirectUri: string;
67
+ /** Unix ms when the attempt started; bounds the callback window. */
68
+ createdAt: number;
69
+ }
70
+ /** Input to {@link ExternalProvider.buildAuthorizationUrl}. */
71
+ export interface ExternalAuthorizationUrlInput {
72
+ /** The callback URL registered with the provider for this app. */
73
+ redirectUri: string;
74
+ /** CSRF token to send as the `state` parameter. */
75
+ state: string;
76
+ /** PKCE code verifier to derive the S256 challenge from, when PKCE is on. */
77
+ codeVerifier?: string;
78
+ /** OIDC nonce to bind into the id_token, when the provider uses one. */
79
+ nonce?: string;
80
+ /** Scopes to request (already defaulted by the flow). */
81
+ scopes: string[];
82
+ /** OIDC `prompt` parameter (e.g. `"select_account"`), when requested. */
83
+ prompt?: string;
84
+ }
85
+ /** Input to {@link ExternalProvider.fetchProfile}. */
86
+ export interface ExternalProfileInput {
87
+ /** The authorization code from the callback. */
88
+ code: string;
89
+ /** The redirect URI the flow was started with (must match the exchange). */
90
+ redirectUri: string;
91
+ /** The PKCE code verifier from the transient, when PKCE was used. */
92
+ codeVerifier?: string;
93
+ /** The expected id_token `nonce` from the transient, when one was sent. */
94
+ nonce?: string;
95
+ }
96
+ /**
97
+ * What a connector must provide for {@link ExternalAuthFlow} to drive it.
98
+ *
99
+ * The flow owns the cross-provider policy (state generation and timing-safe
100
+ * verification, transient expiry, callback error surfacing); the provider
101
+ * owns the wire protocol (authorize URL shape, code exchange, id_token nonce
102
+ * validation, profile normalization). Implement this only for providers the
103
+ * built-in {@link oidcProvider} / {@link githubProvider} cannot cover.
104
+ */
105
+ export interface ExternalProvider {
106
+ /** Stable id used in {@link ExternalProfile.provider} and error messages. */
107
+ readonly id: string;
108
+ /** Human-readable name for buttons and logs (e.g. `"Google"`). */
109
+ readonly displayName: string;
110
+ /** Scopes requested when the caller passes none. */
111
+ readonly defaultScopes: string[];
112
+ /** Whether the flow should generate a PKCE code verifier for this provider. */
113
+ readonly usesPkce: boolean;
114
+ /** Whether the flow should generate an OIDC nonce for this provider. */
115
+ readonly usesNonce: boolean;
116
+ /**
117
+ * Builds the provider's authorization URL for the given per-attempt values.
118
+ *
119
+ * @throws {ExternalAuthError} `configuration` when the provider cannot
120
+ * resolve its authorization endpoint (e.g. OIDC discovery failed).
121
+ */
122
+ buildAuthorizationUrl(input: ExternalAuthorizationUrlInput): Promise<string>;
123
+ /**
124
+ * Exchanges the authorization code and returns the verified, normalized
125
+ * profile. Implementations must validate the id_token `nonce` against
126
+ * `input.nonce` when one was sent.
127
+ *
128
+ * @throws {ExternalAuthError} `provider_error` when the exchange or profile
129
+ * fetch fails, `nonce_mismatch` when the id_token nonce does not match.
130
+ */
131
+ fetchProfile(input: ExternalProfileInput): Promise<ExternalProfile>;
132
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The contract between {@link ExternalAuthFlow} and a concrete external
3
+ * provider connector ({@link oidcProvider}, {@link googleProvider},
4
+ * {@link githubProvider}, or your own).
5
+ *
6
+ * A provider's job ends at returning a verified, normalized
7
+ * {@link ExternalProfile}. What the app does with it — create a user, link an
8
+ * account, reject — is app code; the library deliberately stores nothing.
9
+ *
10
+ * @module
11
+ */
12
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Breached-password rejection via the Have I Been Pwned range API — the
3
+ * privacy-preserving k-anonymity model: only the first five characters of the
4
+ * password's SHA-1 hash ever leave the process; the full password and full
5
+ * hash never do.
6
+ *
7
+ * {@link breachedPasswordValidator} returns an async validator for
8
+ * {@link PasswordPolicy.validators}, so a policy rejects known-breached
9
+ * passwords at sign-up and password reset. **Fail-open by default**: if the
10
+ * range API is unreachable, the password is allowed —
11
+ * availability of your sign-up flow shouldn't hinge on a third party. Set
12
+ * `failOpen: false` to invert that.
13
+ *
14
+ * Pass `onEvent` so those fail-opens reach your audit store as
15
+ * `password_policy.check_unavailable` events; an egress rule that blocks
16
+ * `api.pwnedpasswords.com` disables this control silently otherwise.
17
+ *
18
+ * @module
19
+ */
20
+ import { type IdentityEventHook } from "./events.js";
21
+ /** Options for {@link breachedPasswordValidator}. */
22
+ export interface BreachedPasswordOptions {
23
+ /**
24
+ * Reject when the password appears in at least this many breaches.
25
+ * Defaults to 1.
26
+ */
27
+ threshold?: number;
28
+ /**
29
+ * Allow the password when the range API can't be reached (default `true`).
30
+ * `false` rejects with a retry message instead.
31
+ */
32
+ failOpen?: boolean;
33
+ /** Abort the range request after this many ms. Defaults to 3000. */
34
+ timeoutMs?: number;
35
+ /** Fetch implementation — inject for tests. Defaults to `globalThis.fetch`. */
36
+ fetch?: typeof fetch;
37
+ /** Range API base URL. Defaults to `https://api.pwnedpasswords.com/range/`. */
38
+ baseUrl?: string;
39
+ /**
40
+ * Receives a `password_policy.check_unavailable` {@link IdentityEvent} every
41
+ * time the range lookup fails, so a fail-open is answerable from the same
42
+ * audit store as every other identity outcome ("was this check actually
43
+ * running last month?"). Pass the hook you gave `IdentityService`'s
44
+ * `onEvent`.
45
+ *
46
+ * Without it the failure is only a `console.warn` — greppable in container
47
+ * logs, invisible to an audit query. The hook follows the
48
+ * {@link IdentityEventHook} contract: awaited, and a rejection is logged
49
+ * rather than rethrown, so the sign-up it observes still completes.
50
+ */
51
+ onEvent?: IdentityEventHook;
52
+ }
53
+ /** Uppercase hex SHA-1, the digest format the HIBP range API keys on. */
54
+ export declare function sha1Hex(input: string): Promise<string>;
55
+ /**
56
+ * Build a {@link PasswordPolicy} validator that rejects passwords found in the
57
+ * HIBP breach corpus.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const policy: PasswordPolicy = {
62
+ * minLength: 8,
63
+ * validators: [breachedPasswordValidator({ onEvent })],
64
+ * };
65
+ * ```
66
+ */
67
+ export declare function breachedPasswordValidator(options?: BreachedPasswordOptions): (password: string) => Promise<string | undefined>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Breached-password rejection via the Have I Been Pwned range API — the
3
+ * privacy-preserving k-anonymity model: only the first five characters of the
4
+ * password's SHA-1 hash ever leave the process; the full password and full
5
+ * hash never do.
6
+ *
7
+ * {@link breachedPasswordValidator} returns an async validator for
8
+ * {@link PasswordPolicy.validators}, so a policy rejects known-breached
9
+ * passwords at sign-up and password reset. **Fail-open by default**: if the
10
+ * range API is unreachable, the password is allowed —
11
+ * availability of your sign-up flow shouldn't hinge on a third party. Set
12
+ * `failOpen: false` to invert that.
13
+ *
14
+ * Pass `onEvent` so those fail-opens reach your audit store as
15
+ * `password_policy.check_unavailable` events; an egress rule that blocks
16
+ * `api.pwnedpasswords.com` disables this control silently otherwise.
17
+ *
18
+ * @module
19
+ */
20
+ import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.11/hex.js";
21
+ import { dispatchIdentityEvent } from "./events.js";
22
+ const DEFAULT_BASE_URL = "https://api.pwnedpasswords.com/range/";
23
+ const VALIDATOR_NAME = "breached_password";
24
+ const BREACHED_MESSAGE = "This password has appeared in a data breach; choose a different one.";
25
+ const UNAVAILABLE_MESSAGE = "The password couldn't be checked against known breaches; try again.";
26
+ /** Uppercase hex SHA-1, the digest format the HIBP range API keys on. */
27
+ export async function sha1Hex(input) {
28
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
29
+ return encodeHex(digest).toUpperCase();
30
+ }
31
+ /**
32
+ * Build a {@link PasswordPolicy} validator that rejects passwords found in the
33
+ * HIBP breach corpus.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const policy: PasswordPolicy = {
38
+ * minLength: 8,
39
+ * validators: [breachedPasswordValidator({ onEvent })],
40
+ * };
41
+ * ```
42
+ */
43
+ export function breachedPasswordValidator(options = {}) {
44
+ const threshold = options.threshold ?? 1;
45
+ const failOpen = options.failOpen ?? true;
46
+ const timeoutMs = options.timeoutMs ?? 3000;
47
+ const fetchImpl = options.fetch ?? fetch;
48
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
49
+ const onEvent = options.onEvent;
50
+ return async (password) => {
51
+ try {
52
+ const hash = await sha1Hex(password);
53
+ const prefix = hash.slice(0, 5);
54
+ const suffix = hash.slice(5);
55
+ const response = await fetchImpl(`${baseUrl}${prefix}`, {
56
+ headers: { "Add-Padding": "true" },
57
+ signal: AbortSignal.timeout(timeoutMs),
58
+ });
59
+ if (!response.ok) {
60
+ await response.body?.cancel();
61
+ throw new Error(`range API responded ${response.status}`);
62
+ }
63
+ const body = await response.text();
64
+ for (const line of body.split("\n")) {
65
+ const [lineSuffix, countText] = line.trim().split(":");
66
+ if (lineSuffix === suffix && Number(countText) >= threshold) {
67
+ return BREACHED_MESSAGE;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ catch (error) {
73
+ if (onEvent) {
74
+ await dispatchIdentityEvent(onEvent, "breachedPasswordValidator", {
75
+ type: "password_policy.check_unavailable",
76
+ validator: VALIDATOR_NAME,
77
+ failedOpen: failOpen,
78
+ error: error instanceof Error ? error.message : String(error),
79
+ });
80
+ }
81
+ else {
82
+ console.warn("[@udibo/oauth2] breached-password check failed:", error);
83
+ }
84
+ return failOpen ? undefined : UNAVAILABLE_MESSAGE;
85
+ }
86
+ };
87
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Sign-in identifier classification + resolution.
3
+ *
4
+ * A sign-in form usually accepts "email or username" (sometimes phone), and
5
+ * every app hand-rolls the `identifier.includes("@")` branch to decide which
6
+ * lookup to run. This makes that a configured concern instead.
7
+ *
8
+ * @module
9
+ */
10
+ /** How a sign-in identifier was classified. */
11
+ export type IdentifierKind = "email" | "username" | "phone";
12
+ /**
13
+ * Classify a sign-in identifier as an email, phone number, or username.
14
+ *
15
+ * Heuristic: an `@` → `email`; otherwise a value made only of phone characters
16
+ * (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
17
+ * everything else → `username`. The value is trimmed first.
18
+ */
19
+ export declare function classifyIdentifier(identifier: string): IdentifierKind;
20
+ /** Per-kind user lookups supplied by the app. Omit a kind to disallow it. */
21
+ export interface IdentifierLookups<User> {
22
+ /** Resolve a user by email; omit to disallow signing in with an email. */
23
+ email?: (email: string) => Promise<User | undefined> | User | undefined;
24
+ /** Resolve a user by username; omit to disallow signing in with a username. */
25
+ username?: (username: string) => Promise<User | undefined> | User | undefined;
26
+ /** Resolve a user by phone number; omit to disallow signing in with a phone. */
27
+ phone?: (phone: string) => Promise<User | undefined> | User | undefined;
28
+ }
29
+ /**
30
+ * Build a resolver that classifies an identifier ({@link classifyIdentifier})
31
+ * and dispatches to the matching app-supplied lookup. Returns `undefined` when
32
+ * no lookup is configured for the classified kind, or the user isn't found —
33
+ * callers should treat both as "invalid credentials" without revealing which.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const resolve = createIdentifierResolver({
38
+ * email: (e) => userService.getByEmail(e),
39
+ * username: (u) => userService.getByUsername(u),
40
+ * });
41
+ * const user = await resolve(form.get("identifier"));
42
+ * ```
43
+ */
44
+ export declare function createIdentifierResolver<User>(lookups: IdentifierLookups<User>): (identifier: string) => Promise<User | undefined>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Sign-in identifier classification + resolution.
3
+ *
4
+ * A sign-in form usually accepts "email or username" (sometimes phone), and
5
+ * every app hand-rolls the `identifier.includes("@")` branch to decide which
6
+ * lookup to run. This makes that a configured concern instead.
7
+ *
8
+ * @module
9
+ */
10
+ /**
11
+ * Classify a sign-in identifier as an email, phone number, or username.
12
+ *
13
+ * Heuristic: an `@` → `email`; otherwise a value made only of phone characters
14
+ * (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
15
+ * everything else → `username`. The value is trimmed first.
16
+ */
17
+ export function classifyIdentifier(identifier) {
18
+ const value = identifier.trim();
19
+ if (value.includes("@"))
20
+ return "email";
21
+ const digitCount = (value.match(/\d/g) ?? []).length;
22
+ if (digitCount >= 7 && /^\+?[\d\s\-().]+$/.test(value))
23
+ return "phone";
24
+ return "username";
25
+ }
26
+ /**
27
+ * Build a resolver that classifies an identifier ({@link classifyIdentifier})
28
+ * and dispatches to the matching app-supplied lookup. Returns `undefined` when
29
+ * no lookup is configured for the classified kind, or the user isn't found —
30
+ * callers should treat both as "invalid credentials" without revealing which.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const resolve = createIdentifierResolver({
35
+ * email: (e) => userService.getByEmail(e),
36
+ * username: (u) => userService.getByUsername(u),
37
+ * });
38
+ * const user = await resolve(form.get("identifier"));
39
+ * ```
40
+ */
41
+ export function createIdentifierResolver(lookups) {
42
+ return async (identifier) => {
43
+ const value = identifier.trim();
44
+ const lookup = lookups[classifyIdentifier(value)];
45
+ return lookup ? await lookup(value) : undefined;
46
+ };
47
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Failed-attempt account lockout — the brute-force floor for sign-in.
3
+ *
4
+ * Distinct from rate limiting: the {@link RateLimiter} throttles *attempts per
5
+ * window* keyed however you like (identifier, IP), while lockout tracks
6
+ * *consecutive failures per account* and locks the account for a cooldown once
7
+ * a threshold is crossed. Storage is a pluggable {@link LockoutStore}
8
+ * (a {@link MemoryLockoutStore} ships for dev/single-process; back it with your
9
+ * database to share state across instances — e.g. columns on your users table,
10
+ * which also gives admins visibility).
11
+ *
12
+ * `AccountLockout` is a pure state machine: it never decides whether to block.
13
+ * `IdentityService` consults it and applies its `protectionMode`
14
+ * (enforce vs log-only), so adopters can observe lockout behavior before
15
+ * turning it on.
16
+ *
17
+ * @module
18
+ */
19
+ /** Per-account lockout state. */
20
+ export interface LockoutRecord {
21
+ /** Consecutive failed sign-in attempts since the last success/unlock. */
22
+ failures: number;
23
+ /** When the active lock expires (epoch ms); absent when not locked. */
24
+ lockedUntil?: number;
25
+ }
26
+ /** Pluggable per-account state store for {@link AccountLockout}. */
27
+ export interface LockoutStore {
28
+ /** The account's current record, or `undefined` when it has none. */
29
+ get(userId: string): Promise<LockoutRecord | undefined>;
30
+ /**
31
+ * Add one failure and return the new record: an expired lock resets the
32
+ * count to 1 and drops the lock; otherwise `failures + 1` with any active
33
+ * lock kept. Must be atomic (a single statement / compare-and-set in shared
34
+ * stores) — a read-modify-write here lets concurrent failed attempts lose
35
+ * counts and undershoot the lockout threshold.
36
+ */
37
+ increment(userId: string, now: number): Promise<LockoutRecord>;
38
+ /**
39
+ * Persist the account's record, replacing any existing one rather than
40
+ * merging: a record with no `lockedUntil` leaves the account unlocked.
41
+ */
42
+ set(userId: string, record: LockoutRecord): Promise<void>;
43
+ /** Clear the account's record (successful sign-in or unlock). */
44
+ clear(userId: string): Promise<void>;
45
+ }
46
+ /** Options for {@link AccountLockout}. */
47
+ export interface AccountLockoutOptions {
48
+ /** Consecutive failures that trigger a lock. Defaults to 10. */
49
+ maxAttempts?: number;
50
+ /** How long a lock lasts, in ms. Defaults to 15 minutes. */
51
+ lockDurationMs?: number;
52
+ /** State storage. Defaults to {@link MemoryLockoutStore}. */
53
+ store?: LockoutStore;
54
+ }
55
+ /** The outcome of {@link AccountLockout.recordFailure}. */
56
+ export interface LockoutFailureResult {
57
+ /** Consecutive failures after this one. */
58
+ failures: number;
59
+ /** Whether the account is now locked. */
60
+ locked: boolean;
61
+ /** Whether THIS failure crossed the threshold and created the lock. */
62
+ justLocked: boolean;
63
+ /** When the lock expires (epoch ms); absent when not locked. */
64
+ lockedUntil?: number;
65
+ }
66
+ /** The outcome of {@link AccountLockout.status}. */
67
+ export interface LockoutStatus {
68
+ /** Whether an unexpired lock is active. */
69
+ locked: boolean;
70
+ /** Consecutive failures recorded so far. */
71
+ failures: number;
72
+ /** When the active lock expires (epoch ms); absent when not locked. */
73
+ lockedUntil?: number;
74
+ }
75
+ /**
76
+ * The lockout contract {@link IdentityService} accepts — the
77
+ * bring-your-own-policy seam, symmetric with `RateLimiterLike`.
78
+ * {@link AccountLockout} is the shipped consecutive-failure implementation and
79
+ * satisfies it; implement this directly for a different policy (exponential
80
+ * backoff, per-IP lockout, a lockout your admin tooling already owns).
81
+ *
82
+ * Prefer {@link LockoutStore} when you only need the built-in policy to be
83
+ * shared across instances — that seam swaps storage, not the state machine.
84
+ * Like the class, an implementation decides state only: whether a locked
85
+ * account is refused is `IdentityService`'s `protectionMode`, never yours.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * import type { AccountLockoutLike } from "@udibo/oauth2/identity";
90
+ *
91
+ * declare const backoff: {
92
+ * read(userId: string): Promise<{ failures: number; until?: number }>;
93
+ * bump(userId: string): Promise<{ failures: number; until?: number }>;
94
+ * clear(userId: string): Promise<void>;
95
+ * };
96
+ *
97
+ * const lockout: AccountLockoutLike = {
98
+ * async status(userId, now = Date.now()) {
99
+ * const { failures, until } = await backoff.read(userId);
100
+ * const locked = until !== undefined && until > now;
101
+ * return { locked, failures, lockedUntil: locked ? until : undefined };
102
+ * },
103
+ * async recordFailure(userId, now = Date.now()) {
104
+ * const { failures, until } = await backoff.bump(userId);
105
+ * const locked = until !== undefined && until > now;
106
+ * return { failures, locked, justLocked: locked, lockedUntil: until };
107
+ * },
108
+ * reset: (userId) => backoff.clear(userId),
109
+ * };
110
+ * ```
111
+ */
112
+ export interface AccountLockoutLike {
113
+ /** The account's current lock state. An expired lock must read as unlocked. */
114
+ status(userId: string, now?: number): Promise<LockoutStatus>;
115
+ /**
116
+ * Record one failed attempt and report the resulting state. `justLocked` must
117
+ * be true for exactly one attempt per lock — `IdentityService` fires the
118
+ * lockout event and any unlock email off it.
119
+ */
120
+ recordFailure(userId: string, now?: number): Promise<LockoutFailureResult>;
121
+ /** Clear the account's failures and any lock (successful sign-in, unlock). */
122
+ reset(userId: string): Promise<void>;
123
+ }
124
+ /**
125
+ * Consecutive-failure lockout with safe defaults (10 failures → 15-minute
126
+ * lock) — the default {@link AccountLockoutLike}. Wire into
127
+ * {@link IdentityService} via its `lockout` option; call {@link reset} from
128
+ * your unlock flow.
129
+ */
130
+ export declare class AccountLockout implements AccountLockoutLike {
131
+ #private;
132
+ /** Fixes thresholds and the backing store (defaults to {@linkcode MemoryLockoutStore}). */
133
+ constructor(options?: AccountLockoutOptions);
134
+ /** The account's current lock state. An expired lock reads as not locked. */
135
+ status(userId: string, now?: number): Promise<LockoutStatus>;
136
+ /**
137
+ * Record one failed attempt, locking the account when the consecutive-failure
138
+ * threshold is crossed. An attempt after a lock expired starts a fresh count.
139
+ */
140
+ recordFailure(userId: string, now?: number): Promise<LockoutFailureResult>;
141
+ /** Clear the account's failures and any lock (successful sign-in, unlock). */
142
+ reset(userId: string): Promise<void>;
143
+ }
144
+ /** In-memory {@link LockoutStore} for dev / single-process use. */
145
+ export declare class MemoryLockoutStore implements LockoutStore {
146
+ #private;
147
+ /** The stored record for `userId`, if any. */
148
+ get(userId: string): Promise<LockoutRecord | undefined>;
149
+ /** Add one failure synchronously (atomic in a single-process store). */
150
+ increment(userId: string, now: number): Promise<LockoutRecord>;
151
+ /** Store `record` for `userId`. */
152
+ set(userId: string, record: LockoutRecord): Promise<void>;
153
+ /** Drop `userId`'s record. */
154
+ clear(userId: string): Promise<void>;
155
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Failed-attempt account lockout — the brute-force floor for sign-in.
3
+ *
4
+ * Distinct from rate limiting: the {@link RateLimiter} throttles *attempts per
5
+ * window* keyed however you like (identifier, IP), while lockout tracks
6
+ * *consecutive failures per account* and locks the account for a cooldown once
7
+ * a threshold is crossed. Storage is a pluggable {@link LockoutStore}
8
+ * (a {@link MemoryLockoutStore} ships for dev/single-process; back it with your
9
+ * database to share state across instances — e.g. columns on your users table,
10
+ * which also gives admins visibility).
11
+ *
12
+ * `AccountLockout` is a pure state machine: it never decides whether to block.
13
+ * `IdentityService` consults it and applies its `protectionMode`
14
+ * (enforce vs log-only), so adopters can observe lockout behavior before
15
+ * turning it on.
16
+ *
17
+ * @module
18
+ */
19
+ const DEFAULT_MAX_ATTEMPTS = 10;
20
+ const DEFAULT_LOCK_DURATION_MS = 15 * 60 * 1000;
21
+ /**
22
+ * Consecutive-failure lockout with safe defaults (10 failures → 15-minute
23
+ * lock) — the default {@link AccountLockoutLike}. Wire into
24
+ * {@link IdentityService} via its `lockout` option; call {@link reset} from
25
+ * your unlock flow.
26
+ */
27
+ export class AccountLockout {
28
+ #maxAttempts;
29
+ #lockDurationMs;
30
+ #store;
31
+ /** Fixes thresholds and the backing store (defaults to {@linkcode MemoryLockoutStore}). */
32
+ constructor(options = {}) {
33
+ this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
34
+ this.#lockDurationMs = options.lockDurationMs ?? DEFAULT_LOCK_DURATION_MS;
35
+ this.#store = options.store ?? new MemoryLockoutStore();
36
+ }
37
+ /** The account's current lock state. An expired lock reads as not locked. */
38
+ async status(userId, now = Date.now()) {
39
+ const record = await this.#store.get(userId);
40
+ if (!record)
41
+ return { locked: false, failures: 0 };
42
+ const locked = record.lockedUntil !== undefined && record.lockedUntil > now;
43
+ return {
44
+ locked,
45
+ failures: record.failures,
46
+ lockedUntil: locked ? record.lockedUntil : undefined,
47
+ };
48
+ }
49
+ /**
50
+ * Record one failed attempt, locking the account when the consecutive-failure
51
+ * threshold is crossed. An attempt after a lock expired starts a fresh count.
52
+ */
53
+ async recordFailure(userId, now = Date.now()) {
54
+ const record = await this.#store.increment(userId, now);
55
+ const { failures } = record;
56
+ if (record.lockedUntil !== undefined && record.lockedUntil > now) {
57
+ return {
58
+ failures,
59
+ locked: true,
60
+ justLocked: false,
61
+ lockedUntil: record.lockedUntil,
62
+ };
63
+ }
64
+ if (failures >= this.#maxAttempts) {
65
+ const lockedUntil = now + this.#lockDurationMs;
66
+ await this.#store.set(userId, { failures, lockedUntil });
67
+ // Concurrent attempts can overshoot the threshold before the lock lands;
68
+ // only the attempt that hits it exactly reports justLocked, so the
69
+ // lockout event (and any unlock email) fires once.
70
+ return {
71
+ failures,
72
+ locked: true,
73
+ justLocked: failures === this.#maxAttempts,
74
+ lockedUntil,
75
+ };
76
+ }
77
+ return { failures, locked: false, justLocked: false };
78
+ }
79
+ /** Clear the account's failures and any lock (successful sign-in, unlock). */
80
+ reset(userId) {
81
+ return this.#store.clear(userId);
82
+ }
83
+ }
84
+ /** In-memory {@link LockoutStore} for dev / single-process use. */
85
+ export class MemoryLockoutStore {
86
+ #records = new Map();
87
+ /** The stored record for `userId`, if any. */
88
+ get(userId) {
89
+ return Promise.resolve(this.#records.get(userId));
90
+ }
91
+ /** Add one failure synchronously (atomic in a single-process store). */
92
+ increment(userId, now) {
93
+ const record = this.#records.get(userId);
94
+ const lockExpired = record?.lockedUntil !== undefined &&
95
+ record.lockedUntil <= now;
96
+ const next = record && !lockExpired
97
+ ? {
98
+ failures: record.failures + 1,
99
+ ...(record.lockedUntil !== undefined
100
+ ? { lockedUntil: record.lockedUntil }
101
+ : {}),
102
+ }
103
+ : { failures: 1 };
104
+ this.#records.set(userId, next);
105
+ return Promise.resolve(next);
106
+ }
107
+ /** Store `record` for `userId`. */
108
+ set(userId, record) {
109
+ this.#records.set(userId, record);
110
+ return Promise.resolve();
111
+ }
112
+ /** Drop `userId`'s record. */
113
+ clear(userId) {
114
+ this.#records.delete(userId);
115
+ return Promise.resolve();
116
+ }
117
+ }