@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,64 @@
1
+ /**
2
+ * Identity self-service primitives for apps that own their login.
3
+ *
4
+ * The OAuth2/BFF machinery handles tokens and sessions; this layer covers the
5
+ * "own-auth" flows an app otherwise hand-rolls. It is **optional** and
6
+ * composable — own the flows and the token lifecycle, keep storage as thin,
7
+ * app-owned seams (the `SessionStore` philosophy), never a DB-adapter zoo.
8
+ *
9
+ * - {@link IdentityService} — orchestrates the primitives below into sign-up,
10
+ * sign-in, password-reset, and email-verification flows you call from your
11
+ * own routes (storage stays an app-owned {@link IdentityUserStore}).
12
+ * - {@link PasswordIdentityService} — PBKDF2 password hashing + constant-time
13
+ * verify (no more hand-rolled crypto); implement {@link PasswordHasherLike}
14
+ * instead to plug in argon2 or scrypt.
15
+ * - {@link TokenFlowService} — single-use, hashed-at-rest verify/reset/sign-in
16
+ * tokens over an app-owned {@link TokenFlowStore} (magic links reuse it via
17
+ * {@link IdentityService.requestSignInLink}).
18
+ * - {@link EmailOtpService} — short numeric one-time codes over an app-owned
19
+ * {@link OtpStore}: hashed at rest, attempt-capped (guessable-online threat
20
+ * model), purpose-generic so it doubles as a step-up factor. Wire it into
21
+ * {@link IdentityService} via the `otp` option for "email me a code"
22
+ * sign-in.
23
+ * - {@link createIdentifierResolver} / {@link classifyIdentifier} — accept
24
+ * "email or username (or phone)" on a sign-in form without the bespoke branch.
25
+ * - {@link DeliveryHooks} + URL builders — the seam your mailer/SMS plugs into.
26
+ * - {@link RevocableSessionService} / {@link ListableSessionService} /
27
+ * {@link isRecentlyAuthenticated} — session revocation on credential change,
28
+ * the optional listing half ({@link SessionSummary} rows for a "where you're
29
+ * signed in" screen), and step-up checks.
30
+ * - {@link RateLimiter} — opt-in throttling (wire it into
31
+ * {@link IdentityService}); {@link checkPasswordPolicy} — the password rules
32
+ * {@link IdentityService} already applies to `signUp`/`resetPassword`, at
33
+ * their 8–256 character defaults unless you pass a tighter `passwordPolicy`.
34
+ * - {@link CaptchaProvider} + {@link verifyCaptcha} — the bring-your-own
35
+ * bot-challenge seam (Turnstile/hCaptcha/reCAPTCHA), with fail-open/closed
36
+ * handling; no provider means no challenge.
37
+ * - {@link AccountLockout} — failed-attempt lockout with a self-service unlock
38
+ * flow ({@link IdentityService.unlockAccount}) and a log-only rollout mode
39
+ * via `protectionMode`.
40
+ * - {@link IdentityEvent} + {@link IdentityEventHook} — auth-outcome events for
41
+ * audit capture (wire via {@link IdentityService}'s `onEvent`).
42
+ * - {@link IdentityError} — a stable error contract a front-end can switch on.
43
+ * - Importing users from another auth system without a password reset
44
+ * (upgrade-on-login) lives on its own subpath,
45
+ * `@udibo/oauth2/identity/migration`: the `LegacyPasswordVerifier` seam, the
46
+ * built-in `pbkdf2Verifier`, and `parsePhc`. Wire them through this module's
47
+ * {@link IdentityServiceOptions.legacyVerifiers}.
48
+ *
49
+ * @module
50
+ */
51
+ import "../_dnt.polyfills.js";
52
+ export { DEFAULT_PBKDF2_ITERATIONS, generateSalt, hashPassword, LEGACY_PBKDF2_ITERATIONS, PasswordIdentityService, PBKDF2_SHA256, verifyPassword, } from "./password.js";
53
+ export { classifyIdentifier, createIdentifierResolver, } from "./identifier.js";
54
+ export { MemoryTokenFlowStore, TokenFlowService, TokenPurpose, } from "./token-flow.js";
55
+ export { buildResetUrl, buildSignInUrl, buildTokenUrl, buildUnlockUrl, buildVerificationUrl, } from "./delivery.js";
56
+ export { isRecentlyAuthenticated, supportsSessionListing, } from "./session.js";
57
+ export { IdentityService, } from "./service.js";
58
+ export { EmailOtpService, MemoryOtpStore, } from "./otp.js";
59
+ export { AccountLockout, MemoryLockoutStore, } from "./lockout.js";
60
+ export { IdentityError, identityErrorStatus, isIdentityError, } from "./errors.js";
61
+ export { MemoryRateLimitStore, RateLimiter, } from "./rate-limit.js";
62
+ export { verifyCaptcha, } from "./captcha.js";
63
+ export { assertPasswordPolicy, checkPasswordPolicy, } from "./password-policy.js";
64
+ export { breachedPasswordValidator, } from "./hibp.js";
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Email one-time-code primitives — the "email me a code" half of passwordless
3
+ * sign-in, and a reusable step-up factor.
4
+ *
5
+ * A short numeric code has a different threat model than a URL token: it is
6
+ * **guessable online** (a 6-digit code is one in a million), so unlike
7
+ * {@link TokenFlowService} tokens every code carries an attempt budget — wrong
8
+ * tries are counted and the code is invalidated (`locked`) when the budget is
9
+ * spent. Codes are stored **hashed** (SHA-256, domain-bound to the email +
10
+ * purpose) and verified with a constant-time compare; the raw code exists only
11
+ * in the delivery callback.
12
+ *
13
+ * Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
14
+ * dev/tests) and delivery is a caller-supplied callback — the package owns the
15
+ * lifecycle, never your tables or your mailer. `purpose` is a free string, so
16
+ * the same service covers sign-in codes today and step-up / verification codes
17
+ * later.
18
+ *
19
+ * @module
20
+ */
21
+ import { type RateLimiterLike } from "./rate-limit.js";
22
+ /**
23
+ * The rate-limit key a one-time-code request is throttled under. Exported so
24
+ * `IdentityService` throttles its sign-in-code flow under the very key
25
+ * {@link EmailOtpService} would, instead of re-typing the format and drifting
26
+ * into two windows for one operation.
27
+ *
28
+ * The email is case-folded, matching how {@link EmailOtpService} folds at its
29
+ * entry points, so casing variants of one mailbox share a window rather than
30
+ * each earning a fresh send budget.
31
+ */
32
+ export declare function otpRateKey(purpose: string, email: string): string;
33
+ /**
34
+ * A stored one-time-code record. Holds the code's **hash**, never the raw
35
+ * code — a leaked record can't be replayed, and a 6-digit hash space is only
36
+ * brute-forceable while the record is live and has attempts left.
37
+ */
38
+ export interface OtpRecord {
39
+ /** Opaque unique id (the store's key for attempt/consume updates). */
40
+ id: string;
41
+ /** The email the code was issued to, case-folded by the service. */
42
+ email: string;
43
+ /** What the code authorizes (e.g. `"signin"`). */
44
+ purpose: string;
45
+ /** Hash of the raw code (opaque to the store; the service computes it). */
46
+ codeHash: string;
47
+ /** Expiry, epoch ms. */
48
+ expiresAt: number;
49
+ /** Wrong verification attempts so far. */
50
+ attempts: number;
51
+ /** Attempt budget; reaching it invalidates the code. */
52
+ maxAttempts: number;
53
+ /** Created, epoch ms. */
54
+ createdAt: number;
55
+ }
56
+ /**
57
+ * App-owned persistence for {@link EmailOtpService}. "Active" means neither
58
+ * consumed nor invalidated; expiry stays the service's concern so it can report
59
+ * `expired` distinctly. Back it with your database in production.
60
+ */
61
+ export interface OtpStore {
62
+ /** Persist a new active record. */
63
+ create(record: OtpRecord): Promise<void>;
64
+ /**
65
+ * The most recently created active record for `(email, purpose)`, or `null`.
66
+ * Must still return an expired record — the service reports expiry itself.
67
+ */
68
+ findActive(email: string, purpose: string): Promise<OtpRecord | null>;
69
+ /**
70
+ * Atomically increment a record's attempt count and return the new count
71
+ * (0 if unknown). Must be a single atomic operation (e.g.
72
+ * `UPDATE … SET attempts = attempts + 1 RETURNING attempts`), never a
73
+ * read-then-write: {@link EmailOtpService.verify} gates the guess budget on
74
+ * the returned value, so a non-atomic implementation lets concurrent
75
+ * verifies bypass `maxAttempts`.
76
+ */
77
+ recordAttempt(id: string): Promise<number>;
78
+ /**
79
+ * Atomically consume an active code. Return `true` only to the caller that
80
+ * removed it from the active set; concurrent or repeated claims return
81
+ * `false`. Use a conditional delete/update with RETURNING in persistent stores.
82
+ */
83
+ consume(id: string): Promise<boolean>;
84
+ /**
85
+ * Deactivate the one record with this id, for a code that was never usable —
86
+ * the send carrying it failed. Separate from {@link OtpStore.consume} so a
87
+ * store that records *why* a code left the active set can tell a redeemed
88
+ * code from a discarded one, and from {@link OtpStore.invalidate} so a
89
+ * concurrent request's newer code survives. A no-op for an unknown id.
90
+ */
91
+ invalidateById(id: string): Promise<void>;
92
+ /** Deactivate every active record for `(email, purpose)`. */
93
+ invalidate(email: string, purpose: string): Promise<void>;
94
+ }
95
+ /** Options for {@link EmailOtpService}. */
96
+ export interface EmailOtpServiceOptions {
97
+ /** App-owned code storage. */
98
+ store: OtpStore;
99
+ /** Code length in digits. Defaults to `6`. */
100
+ digits?: number;
101
+ /** Code lifetime in ms. Defaults to 10 minutes. */
102
+ ttlMs?: number;
103
+ /** Wrong-attempt budget before the code is invalidated. Defaults to `5`. */
104
+ maxAttempts?: number;
105
+ /**
106
+ * Throttles {@link EmailOtpService.request} per `(purpose, email)` (key
107
+ * {@link otpRateKey}, which case-folds the email so casing variants of one
108
+ * mailbox cannot each earn a fresh send budget). When exceeded and
109
+ * `protectionMode` is `"enforce"`, `request` throws
110
+ * {@link IdentityError} `rate_limited`. A limiter whose
111
+ * `check` itself throws is trapped the way every identity flow traps it:
112
+ * under `"enforce"` it fails closed as the same `rate_limited` error, and
113
+ * under `"log-only"` the attempt proceeds. For direct use of this service;
114
+ * `IdentityService` never sets it, because it throttles the sign-in-code flow
115
+ * itself and a second limiter reachable only on the known-email path would be
116
+ * an enumeration oracle.
117
+ */
118
+ rateLimiter?: RateLimiterLike;
119
+ /**
120
+ * `"enforce"` (default) blocks a rate-limited request; `"log-only"` lets it
121
+ * proceed so adopters can observe thresholds before turning enforcement on.
122
+ */
123
+ protectionMode?: "enforce" | "log-only";
124
+ }
125
+ /** Options for {@link EmailOtpService.request}. */
126
+ export interface RequestOtpOptions {
127
+ /** The email the code is issued to. Case-folded, so casing need not match later. */
128
+ email: string;
129
+ /** What the code authorizes (e.g. `"signin"`); free string. */
130
+ purpose: string;
131
+ /**
132
+ * Receives the raw code for delivery — the only place it ever exists. Send
133
+ * it via your transport; it is not stored or recoverable. `codeId` identifies
134
+ * the stored record, for {@link EmailOtpService.invalidateCode} when a send
135
+ * fails.
136
+ */
137
+ onDeliver: (code: string, expiresAt: number, codeId: string) => Promise<void> | void;
138
+ }
139
+ /** What {@link EmailOtpService.request} returns after storing + delivering. */
140
+ export interface RequestedOtp {
141
+ /** When the code expires, epoch ms. */
142
+ expiresAt: number;
143
+ /**
144
+ * The stored record's id — pass it to
145
+ * {@link EmailOtpService.invalidateCode} to drop exactly this code.
146
+ */
147
+ id: string;
148
+ }
149
+ /** Options for {@link EmailOtpService.verify}. */
150
+ export interface VerifyOtpOptions {
151
+ /** The email the code was issued to. Case-folded, so any casing matches. */
152
+ email: string;
153
+ /** The purpose the code was issued for. */
154
+ purpose: string;
155
+ /** The submitted code; whitespace is tolerated. */
156
+ code: string;
157
+ }
158
+ /**
159
+ * The outcome of {@link EmailOtpService.verify}. `invalid` covers both a wrong
160
+ * code and no active code — indistinguishable on purpose. `locked` is reported
161
+ * once, on the attempt that spends the budget; afterwards the code is gone and
162
+ * further tries report `invalid`.
163
+ */
164
+ export type VerifyOtpResult = {
165
+ status: "success";
166
+ } | {
167
+ status: "invalid";
168
+ } | {
169
+ status: "expired";
170
+ } | {
171
+ status: "locked";
172
+ };
173
+ /**
174
+ * Issues and verifies short-lived numeric one-time codes over an app-owned
175
+ * {@link OtpStore}. Purpose-generic: `"signin"` today, step-up or verification
176
+ * codes tomorrow — one instance can serve them all.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const otp = new EmailOtpService({ store: new MemoryOtpStore() });
181
+ * await otp.request({
182
+ * email: user.email,
183
+ * purpose: "signin",
184
+ * onDeliver: (code) => mailer.send(user.email, `Your code: ${code}`),
185
+ * });
186
+ * // …the user types the code back in…
187
+ * const result = await otp.verify({ email: user.email, purpose: "signin", code });
188
+ * if (result.status === "success") await establishSession(user);
189
+ * ```
190
+ */
191
+ export declare class EmailOtpService {
192
+ #private;
193
+ /** Wires the app-owned store and applies the digit/TTL/attempt defaults. */
194
+ constructor(options: EmailOtpServiceOptions);
195
+ /**
196
+ * Mint a code for `(email, purpose)`: invalidates any prior active code,
197
+ * stores the new code's hash, and hands the raw code to `onDeliver`. Throws
198
+ * {@link IdentityError} `rate_limited` when a configured `rateLimiter` is
199
+ * exceeded — or is itself broken — in enforce mode; nothing is stored or
200
+ * delivered on that path.
201
+ *
202
+ * A throw from `onDeliver` propagates and the stored code stays active — so a
203
+ * failed send would otherwise leave a live, unsent code behind. Trap it and
204
+ * call {@link EmailOtpService.invalidateCode} with the `codeId` you were
205
+ * handed if that matters (which is what `IdentityService` does for its
206
+ * `sendSignInCode` hook).
207
+ */
208
+ request(options: RequestOtpOptions): Promise<RequestedOtp>;
209
+ /**
210
+ * Drop **every** active code for `(email, purpose)` without verifying one —
211
+ * for an abandoned flow, or an email that no longer belongs to the account.
212
+ * A no-op when none is active.
213
+ *
214
+ * Not for a failed send: a slow failure would drop whatever code is active
215
+ * *now*, which may be a newer one a retry already delivered. Use
216
+ * {@link EmailOtpService.invalidateCode} there.
217
+ */
218
+ invalidate(email: string, purpose: string): Promise<void>;
219
+ /**
220
+ * Drop exactly the code with this id — the `codeId` handed to `onDeliver`, or
221
+ * {@link RequestedOtp.id}. Scoped to the one record, so a code another
222
+ * request has since minted and delivered survives. A no-op for an unknown id.
223
+ */
224
+ invalidateCode(id: string): Promise<void>;
225
+ /**
226
+ * Check a submitted code against the active record for `(email, purpose)`.
227
+ * The hash compare is constant-time and runs even when no active record
228
+ * exists, so a missing account or code is not distinguishable by timing. The
229
+ * attempt is reserved atomically — via the store's `recordAttempt` return
230
+ * value — **before** the compare, so concurrent verifies of one live code
231
+ * can't each test a guess against a stale pre-increment count: at most
232
+ * `maxAttempts` guesses ever reach the comparison. Spending the budget
233
+ * invalidates the code (`locked`); success consumes it (single-use).
234
+ */
235
+ verify(options: VerifyOtpOptions): Promise<VerifyOtpResult>;
236
+ }
237
+ /**
238
+ * In-memory {@link OtpStore} for development and tests. Records are lost on
239
+ * restart; back the contract with a database for production.
240
+ */
241
+ export declare class MemoryOtpStore implements OtpStore {
242
+ #private;
243
+ /** Persist a record and mark it active. */
244
+ create(record: OtpRecord): Promise<void>;
245
+ /** The newest active record for `(email, purpose)`, expired or not; else `null`. */
246
+ findActive(email: string, purpose: string): Promise<OtpRecord | null>;
247
+ /** Bump a record's attempt count; returns the new count, or 0 if unknown. */
248
+ recordAttempt(id: string): Promise<number>;
249
+ /** Atomically deactivate one active record; return whether it was claimed. */
250
+ consume(id: string): Promise<boolean>;
251
+ /** Deactivate one undelivered record; a no-op if the id is unknown. */
252
+ invalidateById(id: string): Promise<void>;
253
+ /** Deactivate every active record for `(email, purpose)`. */
254
+ invalidate(email: string, purpose: string): Promise<void>;
255
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Email one-time-code primitives — the "email me a code" half of passwordless
3
+ * sign-in, and a reusable step-up factor.
4
+ *
5
+ * A short numeric code has a different threat model than a URL token: it is
6
+ * **guessable online** (a 6-digit code is one in a million), so unlike
7
+ * {@link TokenFlowService} tokens every code carries an attempt budget — wrong
8
+ * tries are counted and the code is invalidated (`locked`) when the budget is
9
+ * spent. Codes are stored **hashed** (SHA-256, domain-bound to the email +
10
+ * purpose) and verified with a constant-time compare; the raw code exists only
11
+ * in the delivery callback.
12
+ *
13
+ * Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
14
+ * dev/tests) and delivery is a caller-supplied callback — the package owns the
15
+ * lifecycle, never your tables or your mailer. `purpose` is a free string, so
16
+ * the same service covers sign-in codes today and step-up / verification codes
17
+ * later.
18
+ *
19
+ * @module
20
+ */
21
+ import { sha256Hash } from "../server/utils/hash.js";
22
+ import { timingSafeEqualString } from "../utils/crypto.js";
23
+ import { enforceRateLimit } from "./rate-limit.js";
24
+ function foldEmail(email) {
25
+ return email.trim().toLowerCase();
26
+ }
27
+ /**
28
+ * The rate-limit key a one-time-code request is throttled under. Exported so
29
+ * `IdentityService` throttles its sign-in-code flow under the very key
30
+ * {@link EmailOtpService} would, instead of re-typing the format and drifting
31
+ * into two windows for one operation.
32
+ *
33
+ * The email is case-folded, matching how {@link EmailOtpService} folds at its
34
+ * entry points, so casing variants of one mailbox share a window rather than
35
+ * each earning a fresh send budget.
36
+ */
37
+ export function otpRateKey(purpose, email) {
38
+ return `otp:${purpose}:${foldEmail(email)}`;
39
+ }
40
+ function generateNumericCode(digits) {
41
+ if (!Number.isInteger(digits) || digits < 1) {
42
+ throw new RangeError("digits must be a positive integer");
43
+ }
44
+ let code = "";
45
+ while (code.length < digits) {
46
+ const bytes = crypto.getRandomValues(new Uint8Array(digits * 2));
47
+ for (const byte of bytes) {
48
+ if (byte < 250 && code.length < digits)
49
+ code += String(byte % 10);
50
+ }
51
+ }
52
+ return code;
53
+ }
54
+ function hashOtpCode(email, purpose, code) {
55
+ return sha256Hash(`${purpose}\n${email}\n${code}`);
56
+ }
57
+ const DEFAULT_DIGITS = 6;
58
+ const DEFAULT_TTL_MS = 10 * 60 * 1000;
59
+ const DEFAULT_MAX_ATTEMPTS = 5;
60
+ /**
61
+ * Issues and verifies short-lived numeric one-time codes over an app-owned
62
+ * {@link OtpStore}. Purpose-generic: `"signin"` today, step-up or verification
63
+ * codes tomorrow — one instance can serve them all.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * const otp = new EmailOtpService({ store: new MemoryOtpStore() });
68
+ * await otp.request({
69
+ * email: user.email,
70
+ * purpose: "signin",
71
+ * onDeliver: (code) => mailer.send(user.email, `Your code: ${code}`),
72
+ * });
73
+ * // …the user types the code back in…
74
+ * const result = await otp.verify({ email: user.email, purpose: "signin", code });
75
+ * if (result.status === "success") await establishSession(user);
76
+ * ```
77
+ */
78
+ export class EmailOtpService {
79
+ #store;
80
+ #digits;
81
+ #ttlMs;
82
+ #maxAttempts;
83
+ #rateLimiter;
84
+ #enforce;
85
+ /** Wires the app-owned store and applies the digit/TTL/attempt defaults. */
86
+ constructor(options) {
87
+ this.#store = options.store;
88
+ this.#digits = options.digits ?? DEFAULT_DIGITS;
89
+ this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
90
+ this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
91
+ this.#rateLimiter = options.rateLimiter;
92
+ this.#enforce = (options.protectionMode ?? "enforce") === "enforce";
93
+ }
94
+ /**
95
+ * Mint a code for `(email, purpose)`: invalidates any prior active code,
96
+ * stores the new code's hash, and hands the raw code to `onDeliver`. Throws
97
+ * {@link IdentityError} `rate_limited` when a configured `rateLimiter` is
98
+ * exceeded — or is itself broken — in enforce mode; nothing is stored or
99
+ * delivered on that path.
100
+ *
101
+ * A throw from `onDeliver` propagates and the stored code stays active — so a
102
+ * failed send would otherwise leave a live, unsent code behind. Trap it and
103
+ * call {@link EmailOtpService.invalidateCode} with the `codeId` you were
104
+ * handed if that matters (which is what `IdentityService` does for its
105
+ * `sendSignInCode` hook).
106
+ */
107
+ async request(options) {
108
+ const { purpose } = options;
109
+ const email = foldEmail(options.email);
110
+ await enforceRateLimit({
111
+ rateLimiter: this.#rateLimiter,
112
+ key: otpRateKey(purpose, email),
113
+ message: "too many code requests",
114
+ enforce: this.#enforce,
115
+ });
116
+ await this.#store.invalidate(email, purpose);
117
+ const code = generateNumericCode(this.#digits);
118
+ const now = Date.now();
119
+ const expiresAt = now + this.#ttlMs;
120
+ const id = crypto.randomUUID();
121
+ await this.#store.create({
122
+ id,
123
+ email,
124
+ purpose,
125
+ codeHash: await hashOtpCode(email, purpose, code),
126
+ expiresAt,
127
+ attempts: 0,
128
+ maxAttempts: this.#maxAttempts,
129
+ createdAt: now,
130
+ });
131
+ await options.onDeliver(code, expiresAt, id);
132
+ return { expiresAt, id };
133
+ }
134
+ /**
135
+ * Drop **every** active code for `(email, purpose)` without verifying one —
136
+ * for an abandoned flow, or an email that no longer belongs to the account.
137
+ * A no-op when none is active.
138
+ *
139
+ * Not for a failed send: a slow failure would drop whatever code is active
140
+ * *now*, which may be a newer one a retry already delivered. Use
141
+ * {@link EmailOtpService.invalidateCode} there.
142
+ */
143
+ invalidate(email, purpose) {
144
+ return this.#store.invalidate(foldEmail(email), purpose);
145
+ }
146
+ /**
147
+ * Drop exactly the code with this id — the `codeId` handed to `onDeliver`, or
148
+ * {@link RequestedOtp.id}. Scoped to the one record, so a code another
149
+ * request has since minted and delivered survives. A no-op for an unknown id.
150
+ */
151
+ invalidateCode(id) {
152
+ return this.#store.invalidateById(id);
153
+ }
154
+ /**
155
+ * Check a submitted code against the active record for `(email, purpose)`.
156
+ * The hash compare is constant-time and runs even when no active record
157
+ * exists, so a missing account or code is not distinguishable by timing. The
158
+ * attempt is reserved atomically — via the store's `recordAttempt` return
159
+ * value — **before** the compare, so concurrent verifies of one live code
160
+ * can't each test a guess against a stale pre-increment count: at most
161
+ * `maxAttempts` guesses ever reach the comparison. Spending the budget
162
+ * invalidates the code (`locked`); success consumes it (single-use).
163
+ */
164
+ async verify(options) {
165
+ const { purpose } = options;
166
+ const email = foldEmail(options.email);
167
+ const code = options.code.replaceAll(/\s+/g, "");
168
+ const submittedHash = await hashOtpCode(email, purpose, code);
169
+ const record = await this.#store.findActive(email, purpose);
170
+ if (!record) {
171
+ timingSafeEqualString(submittedHash, submittedHash);
172
+ return { status: "invalid" };
173
+ }
174
+ if (record.expiresAt <= Date.now())
175
+ return { status: "expired" };
176
+ const attempts = await this.#store.recordAttempt(record.id);
177
+ if (attempts > record.maxAttempts) {
178
+ await this.#store.invalidate(email, purpose);
179
+ return { status: "locked" };
180
+ }
181
+ if (timingSafeEqualString(record.codeHash, submittedHash)) {
182
+ return {
183
+ status: await this.#store.consume(record.id) ? "success" : "invalid",
184
+ };
185
+ }
186
+ if (attempts === record.maxAttempts) {
187
+ await this.#store.invalidate(email, purpose);
188
+ return { status: "locked" };
189
+ }
190
+ return { status: "invalid" };
191
+ }
192
+ }
193
+ /**
194
+ * In-memory {@link OtpStore} for development and tests. Records are lost on
195
+ * restart; back the contract with a database for production.
196
+ */
197
+ export class MemoryOtpStore {
198
+ #records = new Map();
199
+ #active = new Set();
200
+ /** Persist a record and mark it active. */
201
+ create(record) {
202
+ this.#records.set(record.id, record);
203
+ this.#active.add(record.id);
204
+ return Promise.resolve();
205
+ }
206
+ /** The newest active record for `(email, purpose)`, expired or not; else `null`. */
207
+ findActive(email, purpose) {
208
+ let latest = null;
209
+ for (const id of this.#active) {
210
+ const record = this.#records.get(id);
211
+ if (record && record.email === email && record.purpose === purpose &&
212
+ (!latest || record.createdAt >= latest.createdAt)) {
213
+ latest = record;
214
+ }
215
+ }
216
+ return Promise.resolve(latest);
217
+ }
218
+ /** Bump a record's attempt count; returns the new count, or 0 if unknown. */
219
+ recordAttempt(id) {
220
+ const record = this.#records.get(id);
221
+ if (!record)
222
+ return Promise.resolve(0);
223
+ record.attempts++;
224
+ return Promise.resolve(record.attempts);
225
+ }
226
+ /** Atomically deactivate one active record; return whether it was claimed. */
227
+ consume(id) {
228
+ return Promise.resolve(this.#active.delete(id));
229
+ }
230
+ /** Deactivate one undelivered record; a no-op if the id is unknown. */
231
+ invalidateById(id) {
232
+ this.#active.delete(id);
233
+ return Promise.resolve();
234
+ }
235
+ /** Deactivate every active record for `(email, purpose)`. */
236
+ invalidate(email, purpose) {
237
+ for (const id of [...this.#active]) {
238
+ const record = this.#records.get(id);
239
+ if (record && record.email === email && record.purpose === purpose) {
240
+ this.#active.delete(id);
241
+ }
242
+ }
243
+ return Promise.resolve();
244
+ }
245
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A small, configurable password policy. Defaults to a length floor (the NIST
3
+ * guidance is "length over composition rules"); add your own `validators` for
4
+ * extra checks (e.g. a breached-password list). Tenant-agnostic.
5
+ *
6
+ * @module
7
+ */
8
+ /** Password policy configuration. */
9
+ export interface PasswordPolicy {
10
+ /** Minimum length. Defaults to 8. */
11
+ minLength?: number;
12
+ /** Maximum length (a DoS guard on the hash input). Defaults to 256. */
13
+ maxLength?: number;
14
+ /**
15
+ * Extra checks. Each returns an issue string to reject, or `undefined` to
16
+ * pass, and may be async — e.g. {@link breachedPasswordValidator}, whose
17
+ * k-anonymity range lookup needs the network.
18
+ */
19
+ validators?: Array<(password: string) => string | undefined | Promise<string | undefined>>;
20
+ }
21
+ /** The result of {@link checkPasswordPolicy}. */
22
+ export interface PasswordPolicyResult {
23
+ /** Whether the password satisfied every policy check. */
24
+ ok: boolean;
25
+ /** Human-readable issues; empty when `ok`. */
26
+ issues: string[];
27
+ }
28
+ /**
29
+ * Check a password against a policy, collecting all issues.
30
+ *
31
+ * Fails closed on a non-string `password` — an untyped value off a JSON body
32
+ * has no `length`, so the length checks would silently pass it through to a
33
+ * hash function that then `String`-coerces it.
34
+ */
35
+ export declare function checkPasswordPolicy(password: string, policy?: PasswordPolicy): Promise<PasswordPolicyResult>;
36
+ /**
37
+ * Throw {@link IdentityError} `weak_password` if the policy fails (the issues
38
+ * are joined into the message). Convenient before {@link PasswordIdentityService.hash}.
39
+ *
40
+ * A custom `validators` entry that **throws** (rather than returning an issue
41
+ * string) is trapped and surfaced as the same `weak_password` error, so a
42
+ * misbehaving validator fails closed instead of leaking a raw error to the
43
+ * caller. A validator that itself throws an {@link IdentityError} is preserved.
44
+ */
45
+ export declare function assertPasswordPolicy(password: string, policy?: PasswordPolicy): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A small, configurable password policy. Defaults to a length floor (the NIST
3
+ * guidance is "length over composition rules"); add your own `validators` for
4
+ * extra checks (e.g. a breached-password list). Tenant-agnostic.
5
+ *
6
+ * @module
7
+ */
8
+ import { IdentityError } from "./errors.js";
9
+ /**
10
+ * Check a password against a policy, collecting all issues.
11
+ *
12
+ * Fails closed on a non-string `password` — an untyped value off a JSON body
13
+ * has no `length`, so the length checks would silently pass it through to a
14
+ * hash function that then `String`-coerces it.
15
+ */
16
+ export async function checkPasswordPolicy(password, policy = {}) {
17
+ if (typeof password !== "string") {
18
+ return { ok: false, issues: ["Password must be a string."] };
19
+ }
20
+ const minLength = policy.minLength ?? 8;
21
+ const maxLength = policy.maxLength ?? 256;
22
+ const issues = [];
23
+ if (password.length < minLength) {
24
+ issues.push(`Password must be at least ${minLength} characters.`);
25
+ }
26
+ if (password.length > maxLength) {
27
+ issues.push(`Password must be at most ${maxLength} characters.`);
28
+ }
29
+ for (const validate of policy.validators ?? []) {
30
+ const issue = await validate(password);
31
+ if (issue)
32
+ issues.push(issue);
33
+ }
34
+ return { ok: issues.length === 0, issues };
35
+ }
36
+ /**
37
+ * Throw {@link IdentityError} `weak_password` if the policy fails (the issues
38
+ * are joined into the message). Convenient before {@link PasswordIdentityService.hash}.
39
+ *
40
+ * A custom `validators` entry that **throws** (rather than returning an issue
41
+ * string) is trapped and surfaced as the same `weak_password` error, so a
42
+ * misbehaving validator fails closed instead of leaking a raw error to the
43
+ * caller. A validator that itself throws an {@link IdentityError} is preserved.
44
+ */
45
+ export async function assertPasswordPolicy(password, policy) {
46
+ let result;
47
+ try {
48
+ result = await checkPasswordPolicy(password, policy);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof IdentityError)
52
+ throw error;
53
+ throw new IdentityError("weak_password", "Password could not be validated.", {
54
+ cause: error,
55
+ });
56
+ }
57
+ if (!result.ok) {
58
+ throw new IdentityError("weak_password", result.issues.join(" "));
59
+ }
60
+ }