@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,122 @@
1
+ /**
2
+ * Session revocation + step-up contracts for the identity layer.
3
+ *
4
+ * Three security primitives the own-auth flows need, kept as **contracts + a
5
+ * pure helper** — storage stays app-owned (the `SessionStore` philosophy, and
6
+ * the Lucia-deprecation lesson: don't ship a DB-adapter zoo):
7
+ *
8
+ * - {@link RevocableSessionService} — revoke a user's sessions on a credential
9
+ * change. After a password reset the app must end the *other* sessions, or a
10
+ * compromised account's other devices stay logged in. The reset flow calls
11
+ * this; your app implements it over its session store.
12
+ * - {@link ListableSessionService} — the revocation seam's optional listing
13
+ * half: a user's live sessions as {@link SessionSummary} display rows, so a
14
+ * "where you're signed in" screen has one typed shape to render and wire the
15
+ * revoke buttons against. Only a stateful store can honestly implement it.
16
+ * - {@link isRecentlyAuthenticated} — a recent-auth / step-up gate for sensitive
17
+ * changes (change password/email, delete account, add MFA).
18
+ *
19
+ * @module
20
+ */
21
+ /**
22
+ * Revocation operations a session store exposes so credential-change flows can
23
+ * end sessions. Implement over your own session storage (DB / Redis / KV).
24
+ */
25
+ export interface RevocableSessionService {
26
+ /**
27
+ * Revoke **every** session belonging to `userId`. Call after a password reset
28
+ * or other full-account credential change. Returns the number revoked.
29
+ */
30
+ revokeAllByUser(userId: string): Promise<number>;
31
+ /**
32
+ * Revoke a user's sessions **except** `keepSessionId` — "sign out everywhere
33
+ * else" after a password change in the current session. Returns the number
34
+ * revoked.
35
+ */
36
+ revokeOthers(userId: string, keepSessionId: string): Promise<number>;
37
+ }
38
+ /**
39
+ * One live session as a "where you're signed in" screen renders it — a display
40
+ * projection, never the session's secret/hash or token material. The
41
+ * current-session marker is the caller's concern: compare
42
+ * {@link SessionSummary.id} against the id of the session backing the request
43
+ * being rendered, and label / disable "revoke" on the match ("revoke" on the
44
+ * current row is "sign out").
45
+ *
46
+ * Keep the optional client fields **coarse** — the contract omits raw address
47
+ * fields on purpose. `userAgent` is the raw header for a component to parse into
48
+ * a device label; `location` is a human-readable place (city / region /
49
+ * country) resolved from the IP at creation, not the raw address. Anything more
50
+ * precise turns a security screen into a tracking log; a store with nothing
51
+ * coarse to report leaves the field unset rather than reaching for the raw value.
52
+ */
53
+ export interface SessionSummary {
54
+ /**
55
+ * Stable session id — the revoke action's target and the value a caller
56
+ * compares against the current session to mark "this device". Use a separate
57
+ * non-secret record identifier, never the cookie value or its hash.
58
+ */
59
+ id: string;
60
+ /** When the session was created. */
61
+ createdAt: Date;
62
+ /**
63
+ * Last-activity timestamp. Bump it on a throttle (once a minute, say), not on
64
+ * every request, or the session table becomes the hottest write path.
65
+ */
66
+ lastSeenAt: Date;
67
+ /** Raw user-agent header, for the UI to summarize into a device label. */
68
+ userAgent?: string;
69
+ /**
70
+ * Coarse, human-readable location resolved from the IP at creation (e.g.
71
+ * `"San Francisco, US"`) — never the raw address.
72
+ */
73
+ location?: string;
74
+ }
75
+ /**
76
+ * Optional {@link RevocableSessionService} capability: list a user's live
77
+ * sessions as {@link SessionSummary} rows so the "where you're signed in"
78
+ * screen you build renders one typed shape and wires its revoke buttons against
79
+ * it, instead of every app inventing a different list shape. The screen is
80
+ * app-owned — the package ships this seam and the row type, not a list
81
+ * component. A store opts in by implementing this method — it is duck-typed at
82
+ * runtime via
83
+ * {@link supportsSessionListing}, mirroring how the BFF's `BackchannelLogoutStore`
84
+ * is an optional capability a stateful `SessionStore` opts into.
85
+ *
86
+ * Only a store that keeps a server-side record can implement it: a listing seam
87
+ * implies the rows are authoritative, and the paired revoke buttons promise
88
+ * **immediate, not advisory** revocation. A stateless self-contained-token store
89
+ * the server never looks up can neither enumerate its sessions nor honor a
90
+ * revoke, so it omits this and the screen simply isn't offered.
91
+ */
92
+ export interface ListableSessionService {
93
+ /**
94
+ * A user's live sessions as display summaries, most-recently-active first.
95
+ * Return only what a session screen renders — never the session secret/hash
96
+ * or token material. Excludes ended sessions (revoked, expired, idle-timed-out).
97
+ */
98
+ listByUser(userId: string): Promise<SessionSummary[]>;
99
+ }
100
+ /**
101
+ * Whether a session service also implements the optional
102
+ * {@link ListableSessionService} listing capability. Use it to decide whether to
103
+ * offer the "where you're signed in" screen: a stateful store returns `true`, a
104
+ * stateless one that only revokes returns `false`.
105
+ */
106
+ export declare function supportsSessionListing(service: RevocableSessionService): service is RevocableSessionService & ListableSessionService;
107
+ /**
108
+ * Whether a session authenticated within `maxAgeMs` — a recent-auth ("step-up")
109
+ * check to gate sensitive actions. Re-prompt for credentials when this is false.
110
+ *
111
+ * @param authenticatedAt When the session last actively authenticated (epoch ms).
112
+ * @param maxAgeMs How recent counts as "fresh" (e.g. 5 minutes).
113
+ * @param now Override the clock (for tests). Defaults to `Date.now()`.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * if (!isRecentlyAuthenticated(session.authenticatedAt, 5 * 60_000)) {
118
+ * throw redirect("/reauthenticate?return_to=/settings/password");
119
+ * }
120
+ * ```
121
+ */
122
+ export declare function isRecentlyAuthenticated(authenticatedAt: number, maxAgeMs: number, now?: number): boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Session revocation + step-up contracts for the identity layer.
3
+ *
4
+ * Three security primitives the own-auth flows need, kept as **contracts + a
5
+ * pure helper** — storage stays app-owned (the `SessionStore` philosophy, and
6
+ * the Lucia-deprecation lesson: don't ship a DB-adapter zoo):
7
+ *
8
+ * - {@link RevocableSessionService} — revoke a user's sessions on a credential
9
+ * change. After a password reset the app must end the *other* sessions, or a
10
+ * compromised account's other devices stay logged in. The reset flow calls
11
+ * this; your app implements it over its session store.
12
+ * - {@link ListableSessionService} — the revocation seam's optional listing
13
+ * half: a user's live sessions as {@link SessionSummary} display rows, so a
14
+ * "where you're signed in" screen has one typed shape to render and wire the
15
+ * revoke buttons against. Only a stateful store can honestly implement it.
16
+ * - {@link isRecentlyAuthenticated} — a recent-auth / step-up gate for sensitive
17
+ * changes (change password/email, delete account, add MFA).
18
+ *
19
+ * @module
20
+ */
21
+ /**
22
+ * Whether a session service also implements the optional
23
+ * {@link ListableSessionService} listing capability. Use it to decide whether to
24
+ * offer the "where you're signed in" screen: a stateful store returns `true`, a
25
+ * stateless one that only revokes returns `false`.
26
+ */
27
+ export function supportsSessionListing(service) {
28
+ return typeof service.listByUser ===
29
+ "function";
30
+ }
31
+ /**
32
+ * Whether a session authenticated within `maxAgeMs` — a recent-auth ("step-up")
33
+ * check to gate sensitive actions. Re-prompt for credentials when this is false.
34
+ *
35
+ * @param authenticatedAt When the session last actively authenticated (epoch ms).
36
+ * @param maxAgeMs How recent counts as "fresh" (e.g. 5 minutes).
37
+ * @param now Override the clock (for tests). Defaults to `Date.now()`.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * if (!isRecentlyAuthenticated(session.authenticatedAt, 5 * 60_000)) {
42
+ * throw redirect("/reauthenticate?return_to=/settings/password");
43
+ * }
44
+ * ```
45
+ */
46
+ export function isRecentlyAuthenticated(authenticatedAt, maxAgeMs, now = Date.now()) {
47
+ return authenticatedAt > 0 && now - authenticatedAt <= maxAgeMs;
48
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * One token-flow primitive for the single-use, expiring links an identity layer
3
+ * needs — **email verification** and **password reset** are the same mechanics:
4
+ * mint a high-entropy token, store only its hash, hand the raw token to the app
5
+ * to deliver, then validate / single-use-consume it later.
6
+ *
7
+ * The app reimplements this once per flow today (random bytes → sha256 → store
8
+ * with expiry → consume). This collapses it into one tested, enumeration-safe
9
+ * building block. Storage is an app-owned {@link TokenFlowStore} (a
10
+ * {@link MemoryTokenFlowStore} ships for dev/tests); the package owns the
11
+ * lifecycle, not the database.
12
+ *
13
+ * @module
14
+ */
15
+ /** Common token purposes. `purpose` is a free string; these are conventions. */
16
+ export declare const TokenPurpose: {
17
+ readonly EmailVerification: "email-verification";
18
+ readonly PasswordReset: "password-reset";
19
+ readonly AccountUnlock: "account-unlock";
20
+ readonly SignIn: "signin";
21
+ };
22
+ /**
23
+ * A stored token record. Holds the token's **hash**, never the raw token — a
24
+ * leaked record (or log line) can't be replayed as a working link.
25
+ */
26
+ export interface TokenFlowRecord {
27
+ /** sha256-hex of the raw token; the storage key and at-rest form. */
28
+ tokenHash: string;
29
+ /** What the token authorizes (e.g. {@link TokenPurpose.PasswordReset}). */
30
+ purpose: string;
31
+ /** The subject the token acts on (typically a user id). */
32
+ subject: string;
33
+ /** Optional extra payload (e.g. the email being verified). */
34
+ data?: Record<string, unknown>;
35
+ /** Expiry, epoch ms. */
36
+ expiresAt: number;
37
+ /** When consumed (epoch ms), or `undefined` while still usable. */
38
+ consumedAt?: number;
39
+ /** Created, epoch ms. */
40
+ createdAt: number;
41
+ }
42
+ /** App-owned persistence for {@link TokenFlowService}, keyed by `tokenHash`. */
43
+ export interface TokenFlowStore {
44
+ /** Persist a new record. */
45
+ save(record: TokenFlowRecord): Promise<void>;
46
+ /**
47
+ * Look up a record by token hash, or `null` when no record was ever stored
48
+ * under it.
49
+ *
50
+ * Return the record **whatever its state** — expired and already-consumed
51
+ * records included. {@link TokenFlowService.inspect} classifies them
52
+ * (`expired` vs `consumed` vs `invalid`) after this read, so a store that
53
+ * filters them out in SQL (`... AND consumed_at IS NULL AND expires_at >
54
+ * now()`) collapses both into `invalid` and costs the caller the message it
55
+ * would otherwise show.
56
+ */
57
+ get(tokenHash: string): Promise<TokenFlowRecord | null>;
58
+ /**
59
+ * Atomically claim a record as consumed (single-use): stamp `consumedAt` and
60
+ * return `true` only when the record existed and was still unconsumed
61
+ * (`UPDATE … SET consumed_at = $now WHERE token_hash = $hash AND consumed_at
62
+ * IS NULL`, checking the row count). Return `false` without changing anything
63
+ * otherwise.
64
+ *
65
+ * This **must** be a conditional write, not a read-then-write.
66
+ * {@link TokenFlowService.consume} reads the record before calling this and
67
+ * treats a `false` as "someone else got it", which is the only thing making a
68
+ * link single-use under concurrent redemption — an unconditional overwrite
69
+ * that always reports success lets two requests racing one link both proceed.
70
+ */
71
+ markConsumed(tokenHash: string, consumedAt: number): Promise<boolean>;
72
+ /**
73
+ * Optional: delete every still-pending token for a `(purpose, subject)` — e.g.
74
+ * invalidate outstanding reset links once one succeeds, or when a new one is
75
+ * issued. Implement it to harden the flow; the service calls it only if present.
76
+ *
77
+ * Delete the **pending** records only. Leaving the consumed ones keeps
78
+ * {@link TokenFlowService.inspect} able to answer `consumed` for a token that
79
+ * really was used, instead of the generic `invalid`.
80
+ */
81
+ deleteBySubject?(purpose: string, subject: string): Promise<void>;
82
+ }
83
+ /** Options for {@link TokenFlowService.create}. */
84
+ export interface CreateTokenOptions {
85
+ /** What the token authorizes (e.g. {@link TokenPurpose.PasswordReset}). */
86
+ purpose: string;
87
+ /** The subject the token acts on (typically a user id). */
88
+ subject: string;
89
+ /** Optional extra payload to carry on the record (e.g. the email being verified). */
90
+ data?: Record<string, unknown>;
91
+ /** Lifetime in milliseconds. */
92
+ ttlMs: number;
93
+ /**
94
+ * Invalidate any existing pending tokens for this `(purpose, subject)` first
95
+ * (requires {@link TokenFlowStore.deleteBySubject}). Defaults to `false`.
96
+ */
97
+ invalidateExisting?: boolean;
98
+ }
99
+ /** The raw token to deliver, available only at creation time. */
100
+ export interface CreatedToken {
101
+ /** The raw token — send it to the user; it is never stored or recoverable. */
102
+ token: string;
103
+ /** When the token expires, epoch ms. */
104
+ expiresAt: number;
105
+ }
106
+ /** The result of validating/consuming a token. */
107
+ export interface ResolvedToken {
108
+ /** The subject the token acts on (typically a user id). */
109
+ subject: string;
110
+ /** The extra payload supplied at creation, if any. */
111
+ data?: Record<string, unknown>;
112
+ }
113
+ /**
114
+ * The detailed outcome of {@link TokenFlowService.inspect}. Unlike the
115
+ * `null`-returning {@link TokenFlowService.validate}/{@link TokenFlowService.consume},
116
+ * this distinguishes **why** a token failed — so a verify/reset UI can say
117
+ * "this link has expired, request a new one" vs. a generic error. (A 32-byte
118
+ * random token isn't enumerable, so reporting `expired` vs `invalid` for the
119
+ * token is not a meaningful leak — distinct from email enumeration.)
120
+ */
121
+ export type TokenStatus = {
122
+ status: "valid";
123
+ subject: string;
124
+ data?: Record<string, unknown>;
125
+ } | {
126
+ status: "invalid";
127
+ } | {
128
+ status: "expired";
129
+ } | {
130
+ status: "consumed";
131
+ };
132
+ /**
133
+ * Issues, validates, and single-use-consumes purpose-scoped tokens.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const tokens = new TokenFlowService(store);
138
+ * const { token } = await tokens.create({
139
+ * purpose: TokenPurpose.PasswordReset, subject: user.id, ttlMs: 3_600_000,
140
+ * });
141
+ * // …email a link containing `token`…
142
+ * const resolved = await tokens.consume(TokenPurpose.PasswordReset, token);
143
+ * if (resolved) await resetPassword(resolved.subject, newPassword);
144
+ * ```
145
+ */
146
+ export declare class TokenFlowService {
147
+ #private;
148
+ /** Wraps an app-owned {@linkcode TokenFlowStore} that persists the token records. */
149
+ constructor(store: TokenFlowStore);
150
+ /**
151
+ * Drop every pending token for a `(purpose, subject)` without redeeming one —
152
+ * e.g. voiding outstanding sign-in links when the user resets their password,
153
+ * so a link an attacker already has stops working.
154
+ *
155
+ * Best-effort by design: {@link TokenFlowStore.deleteBySubject} is optional.
156
+ * Returns `true` when the store implements it and the deletion ran, and
157
+ * `false` when the store does not, in which case those tokens stay redeemable
158
+ * until they expire — check the result if that gap matters to you. A store
159
+ * that throws propagates the error.
160
+ */
161
+ invalidate(purpose: string, subject: string): Promise<boolean>;
162
+ /** Mint a token; stores its hash and returns the raw token (once). */
163
+ create(opts: CreateTokenOptions): Promise<CreatedToken>;
164
+ /**
165
+ * Validate a token **without** consuming it (e.g. to render a reset form
166
+ * before the user submits). Returns the subject/data, or `null` if the token
167
+ * is unknown, wrong-purpose, expired, or already consumed — never reveals which.
168
+ */
169
+ validate(purpose: string, token: string): Promise<ResolvedToken | null>;
170
+ /**
171
+ * Validate **and** consume a token (single-use). Returns the subject/data, or
172
+ * `null` (see {@link validate} for the failure cases). A second `consume` of
173
+ * the same token returns `null` — including when the second one races the
174
+ * first, since only the caller whose {@link TokenFlowStore.markConsumed}
175
+ * claimed the record is handed the token.
176
+ */
177
+ consume(purpose: string, token: string): Promise<ResolvedToken | null>;
178
+ /**
179
+ * Inspect a token **without** consuming it, reporting the detailed
180
+ * {@link TokenStatus} — `valid` (with subject/data), `invalid` (unknown or
181
+ * wrong-purpose), `expired`, or `consumed`. Use this when the UI needs to tell
182
+ * "expired" apart from "invalid" (e.g. an email-verification page that offers
183
+ * to resend). {@link validate} is the `null`-returning shorthand.
184
+ */
185
+ inspect(purpose: string, token: string): Promise<TokenStatus>;
186
+ }
187
+ /**
188
+ * In-memory {@link TokenFlowStore} for development and tests. Records are lost
189
+ * on restart; back the contract with a database for production.
190
+ *
191
+ * Nothing is reclaimed: a consumed record is kept so
192
+ * {@link TokenFlowService.inspect} can still answer `consumed`, and no sweep
193
+ * ever removes it, so the map grows by one entry per token minted. That is
194
+ * fine for a fixture and wrong in a database-backed port — give the rows a TTL
195
+ * or a purge job that drops records already past `expiresAt`. Purge only
196
+ * records that are *also* consumed: dropping an expired-but-unconsumed record
197
+ * collapses `expired` into `invalid` and loses the distinction `inspect`
198
+ * reports.
199
+ */
200
+ export declare class MemoryTokenFlowStore implements TokenFlowStore {
201
+ #private;
202
+ /** Persist a record, keyed by its token hash. */
203
+ save(record: TokenFlowRecord): Promise<void>;
204
+ /** Return the record for a token hash, or `null` if none is stored. */
205
+ get(tokenHash: string): Promise<TokenFlowRecord | null>;
206
+ /** Claim the record if still unconsumed; `false` when unknown or already spent. */
207
+ markConsumed(tokenHash: string, consumedAt: number): Promise<boolean>;
208
+ /**
209
+ * Drop every still-pending record matching the given `(purpose, subject)`.
210
+ * Already-consumed records are kept so {@link TokenFlowService.inspect} can
211
+ * still report `consumed` rather than the generic `invalid`.
212
+ */
213
+ deleteBySubject(purpose: string, subject: string): Promise<void>;
214
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * One token-flow primitive for the single-use, expiring links an identity layer
3
+ * needs — **email verification** and **password reset** are the same mechanics:
4
+ * mint a high-entropy token, store only its hash, hand the raw token to the app
5
+ * to deliver, then validate / single-use-consume it later.
6
+ *
7
+ * The app reimplements this once per flow today (random bytes → sha256 → store
8
+ * with expiry → consume). This collapses it into one tested, enumeration-safe
9
+ * building block. Storage is an app-owned {@link TokenFlowStore} (a
10
+ * {@link MemoryTokenFlowStore} ships for dev/tests); the package owns the
11
+ * lifecycle, not the database.
12
+ *
13
+ * @module
14
+ */
15
+ import { sha256Hash } from "../server/utils/hash.js";
16
+ import { randomToken } from "../utils/crypto.js";
17
+ /** Common token purposes. `purpose` is a free string; these are conventions. */
18
+ export const TokenPurpose = {
19
+ EmailVerification: "email-verification",
20
+ PasswordReset: "password-reset",
21
+ AccountUnlock: "account-unlock",
22
+ SignIn: "signin",
23
+ };
24
+ /**
25
+ * Issues, validates, and single-use-consumes purpose-scoped tokens.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const tokens = new TokenFlowService(store);
30
+ * const { token } = await tokens.create({
31
+ * purpose: TokenPurpose.PasswordReset, subject: user.id, ttlMs: 3_600_000,
32
+ * });
33
+ * // …email a link containing `token`…
34
+ * const resolved = await tokens.consume(TokenPurpose.PasswordReset, token);
35
+ * if (resolved) await resetPassword(resolved.subject, newPassword);
36
+ * ```
37
+ */
38
+ export class TokenFlowService {
39
+ #store;
40
+ /** Wraps an app-owned {@linkcode TokenFlowStore} that persists the token records. */
41
+ constructor(store) {
42
+ this.#store = store;
43
+ }
44
+ /**
45
+ * Drop every pending token for a `(purpose, subject)` without redeeming one —
46
+ * e.g. voiding outstanding sign-in links when the user resets their password,
47
+ * so a link an attacker already has stops working.
48
+ *
49
+ * Best-effort by design: {@link TokenFlowStore.deleteBySubject} is optional.
50
+ * Returns `true` when the store implements it and the deletion ran, and
51
+ * `false` when the store does not, in which case those tokens stay redeemable
52
+ * until they expire — check the result if that gap matters to you. A store
53
+ * that throws propagates the error.
54
+ */
55
+ async invalidate(purpose, subject) {
56
+ if (!this.#store.deleteBySubject)
57
+ return false;
58
+ await this.#store.deleteBySubject(purpose, subject);
59
+ return true;
60
+ }
61
+ /** Mint a token; stores its hash and returns the raw token (once). */
62
+ async create(opts) {
63
+ if (opts.invalidateExisting) {
64
+ await this.invalidate(opts.purpose, opts.subject);
65
+ }
66
+ const token = randomToken();
67
+ const now = Date.now();
68
+ const expiresAt = now + opts.ttlMs;
69
+ await this.#store.save({
70
+ tokenHash: await sha256Hash(token),
71
+ purpose: opts.purpose,
72
+ subject: opts.subject,
73
+ data: opts.data,
74
+ expiresAt,
75
+ createdAt: now,
76
+ });
77
+ return { token, expiresAt };
78
+ }
79
+ /**
80
+ * Validate a token **without** consuming it (e.g. to render a reset form
81
+ * before the user submits). Returns the subject/data, or `null` if the token
82
+ * is unknown, wrong-purpose, expired, or already consumed — never reveals which.
83
+ */
84
+ async validate(purpose, token) {
85
+ const status = await this.inspect(purpose, token);
86
+ return status.status === "valid"
87
+ ? { subject: status.subject, data: status.data }
88
+ : null;
89
+ }
90
+ /**
91
+ * Validate **and** consume a token (single-use). Returns the subject/data, or
92
+ * `null` (see {@link validate} for the failure cases). A second `consume` of
93
+ * the same token returns `null` — including when the second one races the
94
+ * first, since only the caller whose {@link TokenFlowStore.markConsumed}
95
+ * claimed the record is handed the token.
96
+ */
97
+ async consume(purpose, token) {
98
+ const tokenHash = await sha256Hash(token);
99
+ const status = this.#statusOf(await this.#store.get(tokenHash), purpose);
100
+ if (status.status !== "valid")
101
+ return null;
102
+ if (!await this.#store.markConsumed(tokenHash, Date.now()))
103
+ return null;
104
+ return { subject: status.subject, data: status.data };
105
+ }
106
+ /**
107
+ * Inspect a token **without** consuming it, reporting the detailed
108
+ * {@link TokenStatus} — `valid` (with subject/data), `invalid` (unknown or
109
+ * wrong-purpose), `expired`, or `consumed`. Use this when the UI needs to tell
110
+ * "expired" apart from "invalid" (e.g. an email-verification page that offers
111
+ * to resend). {@link validate} is the `null`-returning shorthand.
112
+ */
113
+ async inspect(purpose, token) {
114
+ return this.#statusOf(await this.#store.get(await sha256Hash(token)), purpose);
115
+ }
116
+ #statusOf(record, purpose) {
117
+ if (!record || record.purpose !== purpose)
118
+ return { status: "invalid" };
119
+ if (record.consumedAt !== undefined)
120
+ return { status: "consumed" };
121
+ if (record.expiresAt <= Date.now())
122
+ return { status: "expired" };
123
+ return { status: "valid", subject: record.subject, data: record.data };
124
+ }
125
+ }
126
+ /**
127
+ * In-memory {@link TokenFlowStore} for development and tests. Records are lost
128
+ * on restart; back the contract with a database for production.
129
+ *
130
+ * Nothing is reclaimed: a consumed record is kept so
131
+ * {@link TokenFlowService.inspect} can still answer `consumed`, and no sweep
132
+ * ever removes it, so the map grows by one entry per token minted. That is
133
+ * fine for a fixture and wrong in a database-backed port — give the rows a TTL
134
+ * or a purge job that drops records already past `expiresAt`. Purge only
135
+ * records that are *also* consumed: dropping an expired-but-unconsumed record
136
+ * collapses `expired` into `invalid` and loses the distinction `inspect`
137
+ * reports.
138
+ */
139
+ export class MemoryTokenFlowStore {
140
+ #byHash = new Map();
141
+ /** Persist a record, keyed by its token hash. */
142
+ save(record) {
143
+ this.#byHash.set(record.tokenHash, record);
144
+ return Promise.resolve();
145
+ }
146
+ /** Return the record for a token hash, or `null` if none is stored. */
147
+ get(tokenHash) {
148
+ return Promise.resolve(this.#byHash.get(tokenHash) ?? null);
149
+ }
150
+ /** Claim the record if still unconsumed; `false` when unknown or already spent. */
151
+ markConsumed(tokenHash, consumedAt) {
152
+ const record = this.#byHash.get(tokenHash);
153
+ if (!record || record.consumedAt !== undefined) {
154
+ return Promise.resolve(false);
155
+ }
156
+ record.consumedAt = consumedAt;
157
+ return Promise.resolve(true);
158
+ }
159
+ /**
160
+ * Drop every still-pending record matching the given `(purpose, subject)`.
161
+ * Already-consumed records are kept so {@link TokenFlowService.inspect} can
162
+ * still report `consumed` rather than the generic `invalid`.
163
+ */
164
+ deleteBySubject(purpose, subject) {
165
+ for (const [hash, record] of this.#byHash) {
166
+ if (record.purpose === purpose && record.subject === subject &&
167
+ record.consumedAt === undefined) {
168
+ this.#byHash.delete(hash);
169
+ }
170
+ }
171
+ return Promise.resolve();
172
+ }
173
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Verified authentication claims for one event, supplied by application policy.
3
+ *
4
+ * Capture this context at authorization-code issuance and persist it on codes
5
+ * and tokens. Refresh rotations retain the original event. Omit unknown claims;
6
+ * neither a requested assurance level nor token issuance time is evidence.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const event: AuthenticationContext = {
11
+ * auth_time: 1_700_000_000,
12
+ * acr: "urn:example:password",
13
+ * amr: ["pwd"],
14
+ * };
15
+ * ```
16
+ */
17
+ export interface AuthenticationContext {
18
+ /** Verified authentication time in Unix seconds, not token issuance time. */
19
+ readonly auth_time?: number;
20
+ /** Achieved authentication context, never the requested preference. */
21
+ readonly acr?: string;
22
+ /** Truthful authentication methods; omitted when the complete set is unknown. */
23
+ readonly amr?: readonly string[];
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import type { AbstractScope, BasicScope } from "./scope.js";
2
+ import type { ClientInterface } from "./client.js";
3
+ import type { AuthenticationContext } from "./authentication.js";
4
+ /**
5
+ * Authorization code model.
6
+ *
7
+ * Represents an authorization code issued by the authorization server
8
+ * after the resource owner grants authorization. The code is exchanged
9
+ * for tokens at the token endpoint.
10
+ */
11
+ export interface AuthorizationCode<Client extends ClientInterface, User, Scope extends AbstractScope = BasicScope> {
12
+ /** The authorization code string. */
13
+ code: string;
14
+ /** When the code expires. */
15
+ expiresAt: Date;
16
+ /** The client that requested authorization. */
17
+ client: Client;
18
+ /** The user who authorized the request. */
19
+ user: User;
20
+ /** The scope that was authorized. */
21
+ scope?: Scope | null;
22
+ /** The redirect URI used in the authorization request. */
23
+ redirectUri?: string;
24
+ /** PKCE code challenge. */
25
+ challenge?: string;
26
+ /** PKCE challenge method (S256 or plain). */
27
+ challengeMethod?: string;
28
+ /** OIDC nonce from the authorization request, echoed into the id_token. */
29
+ nonce?: string;
30
+ /** Verified event captured at issuance; code stores must persist and restore it. */
31
+ authenticationContext?: AuthenticationContext;
32
+ }
33
+ /**
34
+ * Parameters from an authorization request.
35
+ *
36
+ * Parsed from the query string of an authorization endpoint request.
37
+ *
38
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
39
+ */
40
+ export interface AuthorizeParameters {
41
+ /** Raw space-delimited ACR preferences; application policy must validate and honor them. */
42
+ acrValues?: string;
43
+ /** Raw maximum authentication age in seconds; application policy must validate and enforce it. */
44
+ maxAge?: string;
45
+ /** Raw prompt values; application policy must honor or refuse every supported control. */
46
+ prompt?: string;
47
+ /** Must be "code" for authorization code flow. */
48
+ responseType?: string;
49
+ /** Client identifier issued to the client. */
50
+ clientId?: string;
51
+ /** URI to redirect to after authorization. */
52
+ redirectUri?: string;
53
+ /** Opaque state value for CSRF protection. */
54
+ state?: string;
55
+ /** Space-delimited list of requested scopes. */
56
+ scope?: string;
57
+ /** PKCE code challenge (RFC 7636). */
58
+ challenge?: string;
59
+ /** PKCE code challenge method ("S256" or "plain"). */
60
+ challengeMethod?: string;
61
+ /** OIDC nonce (echoed into the id_token when `openid` is granted). */
62
+ nonce?: string;
63
+ }
@@ -0,0 +1 @@
1
+ export {};