@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,171 @@
1
+ /**
2
+ * Password hashing primitive for the identity self-service layer.
3
+ *
4
+ * Promotes the package's internal PBKDF2 helpers (previously reachable only via
5
+ * a relative import) to a public, documented service so apps stop hand-rolling
6
+ * password crypto — the single riskiest thing to reinvent. The built-in
7
+ * algorithm is PBKDF2-SHA-256 at {@link DEFAULT_PBKDF2_ITERATIONS} iterations
8
+ * with a constant-time verify. Want argon2 or scrypt instead? Implement
9
+ * {@link PasswordHasherLike} with your own dependency and pass it wherever a
10
+ * hasher is accepted — nothing in the package requires the concrete class.
11
+ *
12
+ * **Storage is yours.** Persist the whole {@link PasswordCredential} — including
13
+ * its `params` — on your user record and pass it back to
14
+ * {@link PasswordIdentityService.verify}. The package owns the crypto, not the
15
+ * database (the same thin-seam philosophy as `SessionStore`).
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * Generates a cryptographically random salt for password hashing.
21
+ *
22
+ * @param length The number of bytes of randomness
23
+ * @returns The hex-encoded salt, twice `length` characters long
24
+ */
25
+ export declare function generateSalt(length?: number): string;
26
+ /**
27
+ * The PBKDF2-SHA-256 iteration count new credentials are minted at, matching
28
+ * current OWASP guidance. Raise it with `new PasswordIdentityService({
29
+ * iterations })` as hardware improves; stored credentials record what they were
30
+ * hashed with, so raising it only triggers a rehash on the owner's next sign-in.
31
+ */
32
+ export declare const DEFAULT_PBKDF2_ITERATIONS = 600000;
33
+ /**
34
+ * The iteration count a {@link PasswordCredential} with no `params` is assumed
35
+ * to have been hashed at — the package's original default. Credentials minted
36
+ * before `params` existed keep verifying against this, and
37
+ * {@link PasswordIdentityService.needsRehash} reports them as due for an
38
+ * upgrade.
39
+ */
40
+ export declare const LEGACY_PBKDF2_ITERATIONS = 100000;
41
+ /** The algorithm identifier {@link PasswordIdentityService} records and accepts. */
42
+ export declare const PBKDF2_SHA256 = "pbkdf2-sha256";
43
+ /**
44
+ * The work factor a stored credential was produced with, so a credential is
45
+ * self-describing and a later default change can tell old hashes from new.
46
+ */
47
+ export interface PasswordHashParams {
48
+ /** Algorithm identifier; {@link PasswordIdentityService} only mints {@link PBKDF2_SHA256}. */
49
+ algorithm: string;
50
+ /** Iteration count the hash was derived with. */
51
+ iterations: number;
52
+ }
53
+ /**
54
+ * A stored password credential: the hash, the salt it used, and (for
55
+ * credentials minted since `params` existed) the work factor behind it.
56
+ *
57
+ * Persist all three fields. A credential with no `params` is interpreted as
58
+ * PBKDF2-SHA-256 at {@link LEGACY_PBKDF2_ITERATIONS}.
59
+ */
60
+ export interface PasswordCredential {
61
+ /** Hex-encoded PBKDF2-SHA-256 digest. */
62
+ hash: string;
63
+ /** Hex-encoded random salt. */
64
+ salt: string;
65
+ /** Algorithm and work factor used; absent means the legacy default. */
66
+ params?: PasswordHashParams;
67
+ }
68
+ /**
69
+ * The password-hasher seam. {@link PasswordIdentityService} is the built-in
70
+ * PBKDF2 implementation; implement this yourself to plug in argon2, scrypt, or
71
+ * a hardware-backed hasher, and pass it as `IdentityService`'s `passwords`
72
+ * option.
73
+ *
74
+ * Implement the optional `needsRehash` to get rehash-on-successful-sign-in: the
75
+ * identity flows call it after a password verifies and, when it returns `true`,
76
+ * re-hash the just-verified password and persist the result.
77
+ */
78
+ export interface PasswordHasherLike {
79
+ /** Hashes a new password into a credential to store. */
80
+ hash(password: string): Promise<PasswordCredential>;
81
+ /** Constant-time verify of a password against a stored credential. */
82
+ verify(password: string, credential: PasswordCredential): Promise<boolean>;
83
+ /**
84
+ * Whether a stored credential is weaker than what this hasher mints today.
85
+ * Only consulted for a credential that has just verified. Omit to opt out of
86
+ * rehash-on-sign-in.
87
+ */
88
+ needsRehash?(credential: PasswordCredential): boolean;
89
+ }
90
+ /**
91
+ * Hashes a password with salt using PBKDF2-SHA-256.
92
+ *
93
+ * Pass the same `iterations` to {@link verifyPassword} that you hashed with —
94
+ * a mismatch simply fails to verify. Prefer {@link PasswordIdentityService},
95
+ * which records the work factor on the credential so you never have to track it.
96
+ *
97
+ * @param password The password to hash
98
+ * @param salt The salt to use, from {@link generateSalt}
99
+ * @param iterations PBKDF2 iterations; defaults to {@link DEFAULT_PBKDF2_ITERATIONS}
100
+ * @returns The hex-encoded 256-bit digest
101
+ */
102
+ export declare function hashPassword(password: string, salt: string, iterations?: number): Promise<string>;
103
+ /**
104
+ * Verifies a password against a stored hash in constant time.
105
+ *
106
+ * @param password The password to verify
107
+ * @param salt The salt used for the hash
108
+ * @param hash The stored hash to compare against
109
+ * @param iterations The iterations the stored hash was derived with; defaults to {@link DEFAULT_PBKDF2_ITERATIONS}
110
+ * @returns Whether the password reproduces the stored hash
111
+ */
112
+ export declare function verifyPassword(password: string, salt: string, hash: string, iterations?: number): Promise<boolean>;
113
+ /** Options for {@link PasswordIdentityService}. */
114
+ export interface PasswordIdentityServiceOptions {
115
+ /**
116
+ * PBKDF2 iterations new credentials are minted at. Defaults to
117
+ * {@link DEFAULT_PBKDF2_ITERATIONS}. Raising it costs login latency (roughly
118
+ * linear) and makes every older credential report `needsRehash`.
119
+ */
120
+ iterations?: number;
121
+ }
122
+ /**
123
+ * Hashes and verifies passwords (PBKDF2-SHA-256, constant-time verify).
124
+ * Construct once and reuse.
125
+ *
126
+ * Verification reads the work factor off the credential, so credentials minted
127
+ * under an older default keep verifying; {@link needsRehash} tells you when one
128
+ * is due for an upgrade (`IdentityService` does this for you on sign-in).
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * import {
133
+ * type IdentityUserStore,
134
+ * PasswordIdentityService,
135
+ * } from "@udibo/oauth2/identity";
136
+ *
137
+ * interface User { id: string }
138
+ *
139
+ * declare const store: IdentityUserStore<User>;
140
+ * declare const userId: string;
141
+ *
142
+ * const passwords = new PasswordIdentityService();
143
+ * const credential = await passwords.hash("hunter2"); // { hash, salt, params }
144
+ * // …store credential on the user…
145
+ * const ok = await passwords.verify("hunter2", credential);
146
+ * if (ok && passwords.needsRehash(credential)) {
147
+ * await store.setCredential(userId, await passwords.hash("hunter2"));
148
+ * }
149
+ * ```
150
+ */
151
+ export declare class PasswordIdentityService implements PasswordHasherLike {
152
+ #private;
153
+ /** Applies the iteration default; construct with no arguments for it. */
154
+ constructor(options?: PasswordIdentityServiceOptions);
155
+ /** The PBKDF2 iteration count this instance mints new credentials at. */
156
+ get iterations(): number;
157
+ /** Hashes a new password, generating a fresh random salt. */
158
+ hash(password: string): Promise<PasswordCredential>;
159
+ /**
160
+ * Constant-time verify of a password against a stored credential, using the
161
+ * credential's own recorded work factor. Resolves `false` for a credential
162
+ * recorded under an algorithm this service does not implement.
163
+ */
164
+ verify(password: string, credential: PasswordCredential): Promise<boolean>;
165
+ /**
166
+ * Whether the credential was produced with a weaker configuration than this
167
+ * service mints today — call it after a successful {@link verify} and, when
168
+ * `true`, re-hash the verified password and persist the new credential.
169
+ */
170
+ needsRehash(credential: PasswordCredential): boolean;
171
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Password hashing primitive for the identity self-service layer.
3
+ *
4
+ * Promotes the package's internal PBKDF2 helpers (previously reachable only via
5
+ * a relative import) to a public, documented service so apps stop hand-rolling
6
+ * password crypto — the single riskiest thing to reinvent. The built-in
7
+ * algorithm is PBKDF2-SHA-256 at {@link DEFAULT_PBKDF2_ITERATIONS} iterations
8
+ * with a constant-time verify. Want argon2 or scrypt instead? Implement
9
+ * {@link PasswordHasherLike} with your own dependency and pass it wherever a
10
+ * hasher is accepted — nothing in the package requires the concrete class.
11
+ *
12
+ * **Storage is yours.** Persist the whole {@link PasswordCredential} — including
13
+ * its `params` — on your user record and pass it back to
14
+ * {@link PasswordIdentityService.verify}. The package owns the crypto, not the
15
+ * database (the same thin-seam philosophy as `SessionStore`).
16
+ *
17
+ * @module
18
+ */
19
+ import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.11/mod.js";
20
+ import { timingSafeEqual } from "../deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js";
21
+ /**
22
+ * Generates a cryptographically random salt for password hashing.
23
+ *
24
+ * @param length The number of bytes of randomness
25
+ * @returns The hex-encoded salt, twice `length` characters long
26
+ */
27
+ export function generateSalt(length = 16) {
28
+ const salt = new Uint8Array(length);
29
+ crypto.getRandomValues(salt);
30
+ return encodeHex(salt);
31
+ }
32
+ const encoder = new TextEncoder();
33
+ /**
34
+ * The PBKDF2-SHA-256 iteration count new credentials are minted at, matching
35
+ * current OWASP guidance. Raise it with `new PasswordIdentityService({
36
+ * iterations })` as hardware improves; stored credentials record what they were
37
+ * hashed with, so raising it only triggers a rehash on the owner's next sign-in.
38
+ */
39
+ export const DEFAULT_PBKDF2_ITERATIONS = 600_000;
40
+ /**
41
+ * The iteration count a {@link PasswordCredential} with no `params` is assumed
42
+ * to have been hashed at — the package's original default. Credentials minted
43
+ * before `params` existed keep verifying against this, and
44
+ * {@link PasswordIdentityService.needsRehash} reports them as due for an
45
+ * upgrade.
46
+ */
47
+ export const LEGACY_PBKDF2_ITERATIONS = 100_000;
48
+ /** The algorithm identifier {@link PasswordIdentityService} records and accepts. */
49
+ export const PBKDF2_SHA256 = "pbkdf2-sha256";
50
+ /**
51
+ * Hashes a password with salt using PBKDF2-SHA-256.
52
+ *
53
+ * Pass the same `iterations` to {@link verifyPassword} that you hashed with —
54
+ * a mismatch simply fails to verify. Prefer {@link PasswordIdentityService},
55
+ * which records the work factor on the credential so you never have to track it.
56
+ *
57
+ * @param password The password to hash
58
+ * @param salt The salt to use, from {@link generateSalt}
59
+ * @param iterations PBKDF2 iterations; defaults to {@link DEFAULT_PBKDF2_ITERATIONS}
60
+ * @returns The hex-encoded 256-bit digest
61
+ */
62
+ export async function hashPassword(password, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
63
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"]);
64
+ const derivedBits = await crypto.subtle.deriveBits({
65
+ name: "PBKDF2",
66
+ salt: encoder.encode(salt),
67
+ iterations,
68
+ hash: "SHA-256",
69
+ }, keyMaterial, 256);
70
+ return encodeHex(new Uint8Array(derivedBits, 0, 32));
71
+ }
72
+ /**
73
+ * Verifies a password against a stored hash in constant time.
74
+ *
75
+ * @param password The password to verify
76
+ * @param salt The salt used for the hash
77
+ * @param hash The stored hash to compare against
78
+ * @param iterations The iterations the stored hash was derived with; defaults to {@link DEFAULT_PBKDF2_ITERATIONS}
79
+ * @returns Whether the password reproduces the stored hash
80
+ */
81
+ export async function verifyPassword(password, salt, hash, iterations = DEFAULT_PBKDF2_ITERATIONS) {
82
+ const computedHash = await hashPassword(password, salt, iterations);
83
+ // Both are fixed-width hex digests, so length carries no secret.
84
+ if (computedHash.length !== hash.length)
85
+ return false;
86
+ return timingSafeEqual(encoder.encode(computedHash), encoder.encode(hash));
87
+ }
88
+ /**
89
+ * Hashes and verifies passwords (PBKDF2-SHA-256, constant-time verify).
90
+ * Construct once and reuse.
91
+ *
92
+ * Verification reads the work factor off the credential, so credentials minted
93
+ * under an older default keep verifying; {@link needsRehash} tells you when one
94
+ * is due for an upgrade (`IdentityService` does this for you on sign-in).
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import {
99
+ * type IdentityUserStore,
100
+ * PasswordIdentityService,
101
+ * } from "@udibo/oauth2/identity";
102
+ *
103
+ * interface User { id: string }
104
+ *
105
+ * declare const store: IdentityUserStore<User>;
106
+ * declare const userId: string;
107
+ *
108
+ * const passwords = new PasswordIdentityService();
109
+ * const credential = await passwords.hash("hunter2"); // { hash, salt, params }
110
+ * // …store credential on the user…
111
+ * const ok = await passwords.verify("hunter2", credential);
112
+ * if (ok && passwords.needsRehash(credential)) {
113
+ * await store.setCredential(userId, await passwords.hash("hunter2"));
114
+ * }
115
+ * ```
116
+ */
117
+ export class PasswordIdentityService {
118
+ #iterations;
119
+ /** Applies the iteration default; construct with no arguments for it. */
120
+ constructor(options = {}) {
121
+ this.#iterations = options.iterations ?? DEFAULT_PBKDF2_ITERATIONS;
122
+ }
123
+ /** The PBKDF2 iteration count this instance mints new credentials at. */
124
+ get iterations() {
125
+ return this.#iterations;
126
+ }
127
+ /** Hashes a new password, generating a fresh random salt. */
128
+ async hash(password) {
129
+ const salt = generateSalt();
130
+ return {
131
+ salt,
132
+ hash: await hashPassword(password, salt, this.#iterations),
133
+ params: { algorithm: PBKDF2_SHA256, iterations: this.#iterations },
134
+ };
135
+ }
136
+ /**
137
+ * Constant-time verify of a password against a stored credential, using the
138
+ * credential's own recorded work factor. Resolves `false` for a credential
139
+ * recorded under an algorithm this service does not implement.
140
+ */
141
+ verify(password, credential) {
142
+ const params = credential.params;
143
+ if (params && params.algorithm !== PBKDF2_SHA256) {
144
+ return Promise.resolve(false);
145
+ }
146
+ return verifyPassword(password, credential.salt, credential.hash, params?.iterations ?? LEGACY_PBKDF2_ITERATIONS);
147
+ }
148
+ /**
149
+ * Whether the credential was produced with a weaker configuration than this
150
+ * service mints today — call it after a successful {@link verify} and, when
151
+ * `true`, re-hash the verified password and persist the new credential.
152
+ */
153
+ needsRehash(credential) {
154
+ const params = credential.params;
155
+ if (!params)
156
+ return LEGACY_PBKDF2_ITERATIONS < this.#iterations;
157
+ return params.algorithm !== PBKDF2_SHA256 ||
158
+ params.iterations < this.#iterations;
159
+ }
160
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Fixed-window rate limiting for auth endpoints — throttle sign-in attempts,
3
+ * forgot-password requests, etc. Two seams sit here: {@link RateLimitStore}
4
+ * swaps the counter storage (a {@link MemoryRateLimitStore} ships for
5
+ * dev/single-process; back it with Redis/DB to share limits across instances),
6
+ * and {@link RateLimiterLike} swaps the limiter itself when a fixed window is
7
+ * not the algorithm you want. Tenant-agnostic: scope by keying on whatever you
8
+ * like (IP, identifier, `tenant:ip`).
9
+ *
10
+ * @module
11
+ */
12
+ import type { IdentityEvent, RateLimitedEventInit } from "./events.js";
13
+ /** Pluggable counter store for {@link RateLimiter}. */
14
+ export interface RateLimitStore {
15
+ /**
16
+ * Increment `key`'s counter within its current window, returning the new
17
+ * count and when the window resets. Implementations start a fresh window
18
+ * (count 1, `resetAt = now + windowMs`) when none is active, and report the
19
+ * same `resetAt` for every hit inside one window — a window that slides
20
+ * forward on each hit never resets under sustained traffic. Must be atomic
21
+ * (a single statement / compare-and-set in shared stores) — a
22
+ * read-modify-write here lets concurrent attempts lose counts and slip past
23
+ * the threshold, which is exactly the burst the limiter exists to catch.
24
+ *
25
+ * Key cardinality is caller-controlled and often derived from
26
+ * unauthenticated request input, so bound what you keep: a TTL per key on
27
+ * Redis, a scheduled delete past `resetAt` on SQL.
28
+ */
29
+ increment(key: string, windowMs: number, now: number): Promise<{
30
+ count: number;
31
+ resetAt: number;
32
+ }>;
33
+ /** Clear `key`'s counter (e.g. on a successful sign-in). */
34
+ reset(key: string): Promise<void>;
35
+ }
36
+ /** The outcome of a {@link RateLimiter.check}. */
37
+ export interface RateLimitResult {
38
+ /** Whether this hit is within the limit. */
39
+ allowed: boolean;
40
+ /** Hits remaining in the current window (0 when blocked). */
41
+ remaining: number;
42
+ /** When the window resets, epoch ms. */
43
+ resetAt: number;
44
+ /** ms until the caller may retry (0 when allowed). */
45
+ retryAfterMs: number;
46
+ }
47
+ /**
48
+ * The limiter contract every identity flow accepts — the bring-your-own-limiter
49
+ * seam. {@link RateLimiter} is the shipped fixed-window implementation and
50
+ * satisfies it; implement this directly to run a sliding window, a token
51
+ * bucket, IP reputation, or a limiter you already operate. Everything but the
52
+ * {@link RateLimitResult} is yours: the window arithmetic and the allow/deny
53
+ * decision never leave your implementation.
54
+ *
55
+ * Prefer {@link RateLimitStore} when you only need the built-in fixed window to
56
+ * be shared across instances — that seam swaps storage, not the algorithm.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import type { RateLimiterLike } from "@udibo/oauth2/identity";
61
+ *
62
+ * declare const myLimiter: {
63
+ * consume(key: string): Promise<{ ok: boolean; msBeforeNext: number }>;
64
+ * delete(key: string): Promise<void>;
65
+ * };
66
+ *
67
+ * const rateLimiter: RateLimiterLike = {
68
+ * async check(key, now = Date.now()) {
69
+ * const { ok, msBeforeNext } = await myLimiter.consume(key);
70
+ * return {
71
+ * allowed: ok,
72
+ * remaining: 0,
73
+ * resetAt: now + msBeforeNext,
74
+ * retryAfterMs: ok ? 0 : msBeforeNext,
75
+ * };
76
+ * },
77
+ * reset: (key) => myLimiter.delete(key),
78
+ * };
79
+ * ```
80
+ */
81
+ export interface RateLimiterLike {
82
+ /** Record a hit for `key` and report whether it is within the limit. */
83
+ check(key: string, now?: number): Promise<RateLimitResult>;
84
+ /** Clear `key`'s counter (e.g. on a successful sign-in). */
85
+ reset(key: string): Promise<void>;
86
+ }
87
+ /** Options for {@link RateLimiter}. */
88
+ export interface RateLimiterOptions {
89
+ /** Max hits per window. Defaults to 10 — a sane sign-in floor. */
90
+ limit?: number;
91
+ /** Window length in ms. Defaults to 15 minutes. */
92
+ windowMs?: number;
93
+ /** Counter storage. Defaults to {@link MemoryRateLimitStore}. */
94
+ store?: RateLimitStore;
95
+ }
96
+ /**
97
+ * Fixed-window limiter — the default {@link RateLimiterLike}, deliberately
98
+ * feature-frozen as a floor rather than a product. Swap in your own
99
+ * `RateLimiterLike` for anything the fixed window cannot express.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const limiter = new RateLimiter({ limit: 5, windowMs: 15 * 60_000 });
104
+ * const r = await limiter.check(`signin:${ip}`);
105
+ * if (!r.allowed) throw new IdentityError("rate_limited", undefined, { retryAfterMs: r.retryAfterMs });
106
+ * // …on success: await limiter.reset(`signin:${ip}`);
107
+ * ```
108
+ */
109
+ export declare class RateLimiter implements RateLimiterLike {
110
+ #private;
111
+ /** Fixes the `limit`/`windowMs` and the backing store (defaults to {@linkcode MemoryRateLimitStore}). */
112
+ constructor(options?: RateLimiterOptions);
113
+ /** Record a hit for `key` and report whether it is within the limit. */
114
+ check(key: string, now?: number): Promise<RateLimitResult>;
115
+ /** Clear `key`'s counter. */
116
+ reset(key: string): Promise<void>;
117
+ }
118
+ /**
119
+ * How a throttled flow reports hitting its limit. Both halves travel together
120
+ * or neither does — supplying one without the other would type-check into a
121
+ * silently dropped audit event.
122
+ */
123
+ type RateLimitReport = {
124
+ /** The flow's `*.rate_limited` event, minus the fields filled in here. */
125
+ event: RateLimitedEventInit;
126
+ /** The service's event emitter (already hook-safe, never throws). */
127
+ emit: (event: IdentityEvent) => Promise<void>;
128
+ } | {
129
+ /** Omitted in a flow whose service exposes no event seam. */
130
+ event?: never;
131
+ emit?: never;
132
+ };
133
+ /** Options for {@link enforceRateLimit}. */
134
+ export type EnforceRateLimitOptions = {
135
+ /** The limiter to check; `undefined` disables throttling entirely. */
136
+ rateLimiter: RateLimiterLike | undefined;
137
+ /** The limiter key (e.g. `signin:${identifier}`, `mfa:verify:${userId}`). */
138
+ key: string;
139
+ /** The `IdentityError` message when the limit is hit under enforcement. */
140
+ message: string;
141
+ /** `false` (log-only mode) emits the event but lets the attempt through. */
142
+ enforce: boolean;
143
+ } & RateLimitReport;
144
+ /**
145
+ * The shared throttle sequence every identity-layer flow uses: check the
146
+ * limiter, and when the limit is hit emit the flow's `*.rate_limited` event
147
+ * (when the caller supplies `event` + `emit`) and — under enforcement — throw.
148
+ * No-op without a limiter.
149
+ *
150
+ * A BYO {@link RateLimiterLike.check} that **throws** is trapped: under
151
+ * enforcement it surfaces as the same `rate_limited` error (fail closed, so a
152
+ * broken limiter denies rather than leaking a raw error), and in log-only mode
153
+ * it is logged and lets the attempt through (the mode's promise never to
154
+ * block). A limiter that itself throws an {@link IdentityError} is preserved.
155
+ *
156
+ * @throws {IdentityError} `rate_limited` (with `retryAfterMs`) when the limit
157
+ * is hit and `enforce` is `true`.
158
+ */
159
+ export declare function enforceRateLimit(options: EnforceRateLimitOptions): Promise<void>;
160
+ /**
161
+ * In-memory {@link RateLimitStore} for dev / single-process use. Counters are
162
+ * per-process, so replicas do not share limits — back {@link RateLimiter} with
163
+ * Redis or a database in production.
164
+ *
165
+ * Bounded on purpose: limiter keys are built from unauthenticated request
166
+ * input, so the store is capped at 10,000 buckets and evicts to stay under it.
167
+ * Eviction takes lapsed buckets first, then the buckets with the fewest hits,
168
+ * so a unique-key flood discards its own counters rather than one that is
169
+ * close to blocking. The residual is real: an attacker able to push ~10,000
170
+ * keys above a live counter's hit count can still displace it.
171
+ */
172
+ export declare class MemoryRateLimitStore implements RateLimitStore {
173
+ #private;
174
+ /**
175
+ * How many buckets are currently retained — never more than the 10,000-bucket
176
+ * cap. Exposed so a test or a dev-time probe can see the guardrail working;
177
+ * it is not part of the {@link RateLimitStore} contract.
178
+ */
179
+ get size(): number;
180
+ /**
181
+ * Bump `key`'s counter, opening a fresh window when the current one has
182
+ * lapsed. Atomic by virtue of being synchronous in a single process. May
183
+ * evict other keys' buckets to stay under the store's capacity.
184
+ */
185
+ increment(key: string, windowMs: number, now: number): Promise<{
186
+ count: number;
187
+ resetAt: number;
188
+ }>;
189
+ /** Forget `key`'s bucket so its next hit starts a new window. */
190
+ reset(key: string): Promise<void>;
191
+ }
192
+ export {};
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Fixed-window rate limiting for auth endpoints — throttle sign-in attempts,
3
+ * forgot-password requests, etc. Two seams sit here: {@link RateLimitStore}
4
+ * swaps the counter storage (a {@link MemoryRateLimitStore} ships for
5
+ * dev/single-process; back it with Redis/DB to share limits across instances),
6
+ * and {@link RateLimiterLike} swaps the limiter itself when a fixed window is
7
+ * not the algorithm you want. Tenant-agnostic: scope by keying on whatever you
8
+ * like (IP, identifier, `tenant:ip`).
9
+ *
10
+ * @module
11
+ */
12
+ import { IdentityError } from "./errors.js";
13
+ const DEFAULT_LIMIT = 10;
14
+ const DEFAULT_WINDOW_MS = 15 * 60 * 1000;
15
+ /**
16
+ * Fixed-window limiter — the default {@link RateLimiterLike}, deliberately
17
+ * feature-frozen as a floor rather than a product. Swap in your own
18
+ * `RateLimiterLike` for anything the fixed window cannot express.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const limiter = new RateLimiter({ limit: 5, windowMs: 15 * 60_000 });
23
+ * const r = await limiter.check(`signin:${ip}`);
24
+ * if (!r.allowed) throw new IdentityError("rate_limited", undefined, { retryAfterMs: r.retryAfterMs });
25
+ * // …on success: await limiter.reset(`signin:${ip}`);
26
+ * ```
27
+ */
28
+ export class RateLimiter {
29
+ #limit;
30
+ #windowMs;
31
+ #store;
32
+ /** Fixes the `limit`/`windowMs` and the backing store (defaults to {@linkcode MemoryRateLimitStore}). */
33
+ constructor(options = {}) {
34
+ this.#limit = options.limit ?? DEFAULT_LIMIT;
35
+ this.#windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
36
+ this.#store = options.store ?? new MemoryRateLimitStore();
37
+ }
38
+ /** Record a hit for `key` and report whether it is within the limit. */
39
+ async check(key, now = Date.now()) {
40
+ const { count, resetAt } = await this.#store.increment(key, this.#windowMs, now);
41
+ const allowed = count <= this.#limit;
42
+ return {
43
+ allowed,
44
+ remaining: Math.max(0, this.#limit - count),
45
+ resetAt,
46
+ retryAfterMs: allowed ? 0 : Math.max(0, resetAt - now),
47
+ };
48
+ }
49
+ /** Clear `key`'s counter. */
50
+ reset(key) {
51
+ return this.#store.reset(key);
52
+ }
53
+ }
54
+ /**
55
+ * The shared throttle sequence every identity-layer flow uses: check the
56
+ * limiter, and when the limit is hit emit the flow's `*.rate_limited` event
57
+ * (when the caller supplies `event` + `emit`) and — under enforcement — throw.
58
+ * No-op without a limiter.
59
+ *
60
+ * A BYO {@link RateLimiterLike.check} that **throws** is trapped: under
61
+ * enforcement it surfaces as the same `rate_limited` error (fail closed, so a
62
+ * broken limiter denies rather than leaking a raw error), and in log-only mode
63
+ * it is logged and lets the attempt through (the mode's promise never to
64
+ * block). A limiter that itself throws an {@link IdentityError} is preserved.
65
+ *
66
+ * @throws {IdentityError} `rate_limited` (with `retryAfterMs`) when the limit
67
+ * is hit and `enforce` is `true`.
68
+ */
69
+ export async function enforceRateLimit(options) {
70
+ if (!options.rateLimiter)
71
+ return;
72
+ let result;
73
+ try {
74
+ result = await options.rateLimiter.check(options.key);
75
+ }
76
+ catch (error) {
77
+ if (error instanceof IdentityError)
78
+ throw error;
79
+ if (!options.enforce) {
80
+ console.error("[@udibo/oauth2] rate limiter check failed (log-only, allowing):", error instanceof Error ? error.message : error);
81
+ return;
82
+ }
83
+ throw new IdentityError("rate_limited", options.message, { cause: error });
84
+ }
85
+ if (result.allowed)
86
+ return;
87
+ if (options.event && options.emit) {
88
+ await options.emit({
89
+ ...options.event,
90
+ retryAfterMs: result.retryAfterMs,
91
+ enforced: options.enforce,
92
+ });
93
+ }
94
+ if (options.enforce) {
95
+ throw new IdentityError("rate_limited", options.message, {
96
+ retryAfterMs: result.retryAfterMs,
97
+ });
98
+ }
99
+ }
100
+ const MAX_BUCKETS = 10_000;
101
+ const SWEEP_INTERVAL = 1_000;
102
+ const EVICTION_TARGET = 9_000;
103
+ /**
104
+ * In-memory {@link RateLimitStore} for dev / single-process use. Counters are
105
+ * per-process, so replicas do not share limits — back {@link RateLimiter} with
106
+ * Redis or a database in production.
107
+ *
108
+ * Bounded on purpose: limiter keys are built from unauthenticated request
109
+ * input, so the store is capped at 10,000 buckets and evicts to stay under it.
110
+ * Eviction takes lapsed buckets first, then the buckets with the fewest hits,
111
+ * so a unique-key flood discards its own counters rather than one that is
112
+ * close to blocking. The residual is real: an attacker able to push ~10,000
113
+ * keys above a live counter's hit count can still displace it.
114
+ */
115
+ export class MemoryRateLimitStore {
116
+ #buckets = new Map();
117
+ #writesSinceSweep = 0;
118
+ /**
119
+ * How many buckets are currently retained — never more than the 10,000-bucket
120
+ * cap. Exposed so a test or a dev-time probe can see the guardrail working;
121
+ * it is not part of the {@link RateLimitStore} contract.
122
+ */
123
+ get size() {
124
+ return this.#buckets.size;
125
+ }
126
+ /**
127
+ * Bump `key`'s counter, opening a fresh window when the current one has
128
+ * lapsed. Atomic by virtue of being synchronous in a single process. May
129
+ * evict other keys' buckets to stay under the store's capacity.
130
+ */
131
+ increment(key, windowMs, now) {
132
+ let bucket = this.#buckets.get(key);
133
+ if (!bucket || bucket.resetAt <= now) {
134
+ bucket = { count: 0, resetAt: now + windowMs };
135
+ this.#buckets.set(key, bucket);
136
+ }
137
+ bucket.count++;
138
+ this.#collect(now);
139
+ return Promise.resolve({ count: bucket.count, resetAt: bucket.resetAt });
140
+ }
141
+ /** Forget `key`'s bucket so its next hit starts a new window. */
142
+ reset(key) {
143
+ this.#buckets.delete(key);
144
+ return Promise.resolve();
145
+ }
146
+ #collect(now) {
147
+ if (++this.#writesSinceSweep >= SWEEP_INTERVAL)
148
+ this.#dropLapsed(now);
149
+ if (this.#buckets.size <= MAX_BUCKETS)
150
+ return;
151
+ this.#dropLapsed(now);
152
+ if (this.#buckets.size <= MAX_BUCKETS)
153
+ return;
154
+ this.#dropColdest(this.#buckets.size - EVICTION_TARGET);
155
+ }
156
+ #dropLapsed(now) {
157
+ this.#writesSinceSweep = 0;
158
+ for (const [key, bucket] of this.#buckets) {
159
+ if (bucket.resetAt <= now)
160
+ this.#buckets.delete(key);
161
+ }
162
+ }
163
+ #dropColdest(count) {
164
+ const ordered = [...this.#buckets].sort((a, b) => a[1].count - b[1].count || a[1].resetAt - b[1].resetAt);
165
+ for (const [key] of ordered.slice(0, count))
166
+ this.#buckets.delete(key);
167
+ }
168
+ }