@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,254 @@
1
+ /**
2
+ * Testing helpers for the Hono BFF adapter.
3
+ *
4
+ * Lets a consumer's tests drive protected endpoints without walking
5
+ * through the full login → authorize → callback flow, and ages the
6
+ * access token in the underlying {@link SessionStore} so a follow-up
7
+ * request through `attachToken()` exercises the refresh path
8
+ * deterministically (no `setTimeout` required).
9
+ *
10
+ * The runner-style helper {@link runSessionStoreContractTests} is
11
+ * available for verifying custom {@link SessionStore} implementations
12
+ * (Postgres, Redis, DynamoDB, …) satisfy the create / read / update /
13
+ * destroy contract.
14
+ *
15
+ * @module
16
+ */
17
+ import "../../../_dnt.polyfills.js";
18
+ import { assert, assertEquals, assertStrictEquals } from "../../../deps/jsr.io/@std/assert/1.0.19/mod.js";
19
+ import { delay } from "../../../deps/jsr.io/@std/async/1.5.0/delay.js";
20
+ import { beforeEach, describe, it } from "../../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
21
+ import { sessionStoreMaxAgeMs, } from "./session-store.js";
22
+ /**
23
+ * Seeds a session in `bff.sessionStore` and returns a `Cookie` header
24
+ * value (`<name>=<value>`) ready to attach to subsequent
25
+ * `app.request(...)` calls. Skips the authorize / callback round-trip
26
+ * entirely — useful for tests that only care about authenticated
27
+ * endpoint behavior.
28
+ *
29
+ * **Important — token recognition.** The seeded access token is random by
30
+ * default and is NOT registered with any resource server. That is enough for
31
+ * `GET /auth/session` (which reads the cached `user` claim), but a route
32
+ * guarded by {@link HonoBff.protect} validates the session's access token
33
+ * against the resource server's token service — so an unrecognized token
34
+ * yields 401. For protected API routes, either pre-register the token
35
+ * (`tokenService.save({ accessToken, ... })` then pass the same
36
+ * `tokens.accessToken` here), or use {@link createAuthenticatedTestSession},
37
+ * which does both in one call.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // Probe-only (GET /auth/session) — no resource server involved:
42
+ * const cookie = await createTestSession(bff, {
43
+ * user: { sub: "u1", name: "Alice" },
44
+ * });
45
+ * const res = await app.request("/auth/session", { headers: { cookie } });
46
+ * ```
47
+ */
48
+ export async function createTestSession(bff, options = {}) {
49
+ const now = Date.now();
50
+ const data = {
51
+ tokens: {
52
+ accessToken: options.tokens?.accessToken ?? crypto.randomUUID(),
53
+ accessTokenExpiresAt: options.tokens?.accessTokenExpiresAt ??
54
+ now + 60 * 60 * 1000,
55
+ tokenType: options.tokens?.tokenType ?? "Bearer",
56
+ scope: options.tokens?.scope,
57
+ idToken: options.tokens?.idToken,
58
+ },
59
+ refreshToken: options.refreshToken,
60
+ user: options.user ?? null,
61
+ createdAt: options.createdAt ?? now,
62
+ updatedAt: options.updatedAt ?? now,
63
+ };
64
+ const cookieValue = await bff.sessionStore.create(data);
65
+ return `${bff.cookieName}=${cookieValue}`;
66
+ }
67
+ /**
68
+ * Mints a real access token, registers it with the resource server's token
69
+ * service, AND seeds a matching BFF session — the one-call setup for testing a
70
+ * route guarded by {@link HonoBff.protect}. Returns a `Cookie` header ready to
71
+ * attach to `app.request(...)`.
72
+ *
73
+ * This is the combined form of the two-step pattern (save a token, then
74
+ * {@link createTestSession} with the same access token) that protected-route
75
+ * tests otherwise hand-roll. Works with any {@link SessionStore} the BFF can
76
+ * `create()` into (e.g. `MemorySessionStore`,
77
+ * `EncryptedCookieSessionStore`); apps with a DB-backed store that mints
78
+ * its own cookie/secret should seed via that store directly.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const cookie = await createAuthenticatedTestSession(bff, {
83
+ * tokenService,
84
+ * client: { id: "spa" },
85
+ * user: { id: "u1", username: "alice" },
86
+ * scope: new BasicScope("read write"),
87
+ * claims: { sub: "u1", name: "Alice" },
88
+ * });
89
+ * const res = await app.request("/api/me", { headers: { cookie } });
90
+ * ```
91
+ */
92
+ export async function createAuthenticatedTestSession(bff, options) {
93
+ const accessToken = crypto.randomUUID();
94
+ const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenLifetime ?? 60 * 60) * 1000);
95
+ await options.tokenService.save({
96
+ accessToken,
97
+ accessTokenExpiresAt,
98
+ client: options.client,
99
+ user: options.user,
100
+ scope: options.scope,
101
+ });
102
+ return createTestSession(bff, {
103
+ tokens: {
104
+ accessToken,
105
+ accessTokenExpiresAt: accessTokenExpiresAt.getTime(),
106
+ },
107
+ user: options.claims ?? null,
108
+ });
109
+ }
110
+ /**
111
+ * Ages the access token in the underlying session so the next request
112
+ * through {@link HonoBff.attachToken} sees it as expired and triggers
113
+ * the refresh path.
114
+ *
115
+ * Returns a fresh `Cookie` header. For stateful stores
116
+ * (`MemorySessionStore` and DB-backed variants) the cookie value
117
+ * is unchanged. For stateless stores
118
+ * (`EncryptedCookieSessionStore`) the value is re-encrypted and
119
+ * differs from the input — use the returned header for subsequent
120
+ * requests.
121
+ *
122
+ * @param expiredAt Optional time to set the access token's expiry to
123
+ * (epoch ms). Defaults to one second in the past.
124
+ */
125
+ export async function forceTokenExpiry(bff, cookieHeader, expiredAt) {
126
+ const value = parseCookieHeader(cookieHeader, bff.cookieName);
127
+ const data = await bff.sessionStore.read(value);
128
+ if (!data) {
129
+ throw new Error(`forceTokenExpiry: no session found for cookie value "${value}"`);
130
+ }
131
+ const aged = {
132
+ ...data,
133
+ tokens: {
134
+ ...data.tokens,
135
+ accessTokenExpiresAt: expiredAt ?? Date.now() - 1000,
136
+ },
137
+ updatedAt: Date.now(),
138
+ };
139
+ const newValue = await bff.sessionStore.update(value, aged);
140
+ return `${bff.cookieName}=${newValue}`;
141
+ }
142
+ /**
143
+ * Returns the session data currently stored under the given cookie
144
+ * header, or `null` if there is none. Convenience wrapper around
145
+ * `bff.sessionStore.read` that handles cookie parsing.
146
+ */
147
+ export function readTestSession(bff, cookieHeader) {
148
+ const value = parseCookieHeader(cookieHeader, bff.cookieName);
149
+ return Promise.resolve(bff.sessionStore.read(value));
150
+ }
151
+ function parseCookieHeader(header, name) {
152
+ if (!header.includes("="))
153
+ return header;
154
+ const cookies = header.split(/;\s*/);
155
+ for (const cookie of cookies) {
156
+ const idx = cookie.indexOf("=");
157
+ if (idx < 0)
158
+ continue;
159
+ const k = cookie.slice(0, idx).trim();
160
+ if (k === name)
161
+ return cookie.slice(idx + 1).trim();
162
+ }
163
+ throw new Error(`parseCookieHeader: cookie "${name}" not found in header "${header}"`);
164
+ }
165
+ const BOUND_MS = 1000;
166
+ /** Contract test suite for {@link SessionStore} implementations. */
167
+ export function runSessionStoreContractTests(options) {
168
+ const destroyClearsRead = options.destroyClearsRead ?? true;
169
+ const makeBoundedStore = options.makeBoundedStore;
170
+ describe(options.describeName ?? "SessionStore contract", () => {
171
+ let store;
172
+ beforeEach(async () => {
173
+ store = await options.makeStore();
174
+ });
175
+ function sampleSession() {
176
+ const now = Date.now();
177
+ return {
178
+ tokens: {
179
+ accessToken: "access-1",
180
+ accessTokenExpiresAt: now + 60_000,
181
+ tokenType: "Bearer",
182
+ },
183
+ refreshToken: "refresh-1",
184
+ user: { sub: "u1" },
185
+ createdAt: now,
186
+ updatedAt: now,
187
+ };
188
+ }
189
+ it("create returns a non-empty cookie value", async () => {
190
+ const value = await store.create(sampleSession());
191
+ assertEquals(typeof value, "string");
192
+ assert(value.length > 0);
193
+ });
194
+ it("read returns null for an unknown cookie value", async () => {
195
+ assertStrictEquals(await store.read("no-such-session"), null);
196
+ });
197
+ it("create + read round-trips the session data", async () => {
198
+ const original = sampleSession();
199
+ const value = await store.create(original);
200
+ const fetched = await store.read(value);
201
+ assert(fetched !== null, "read should return data for a created session");
202
+ assertEquals(fetched.tokens.accessToken, original.tokens.accessToken);
203
+ assertEquals(fetched.refreshToken, original.refreshToken);
204
+ assertEquals(fetched.user, original.user);
205
+ });
206
+ it("preserves createdAt, which carries the session's absolute age", async () => {
207
+ const original = sampleSession();
208
+ const value = await store.create(original);
209
+ const fetched = await store.read(value);
210
+ assert(fetched !== null, "read should return data for a created session");
211
+ assertEquals(fetched.createdAt, original.createdAt, "a store that drops createdAt silently opts its sessions out of " +
212
+ "HonoBffOptions.sessionMaxAgeMs, so they never age out");
213
+ });
214
+ it("update reflects changes on subsequent read", async () => {
215
+ const value = await store.create(sampleSession());
216
+ const updated = {
217
+ ...sampleSession(),
218
+ user: { sub: "u1", role: "admin" },
219
+ updatedAt: Date.now(),
220
+ };
221
+ const newValue = await store.update(value, updated);
222
+ const fetched = await store.read(newValue);
223
+ assertEquals(fetched?.user, { sub: "u1", role: "admin" });
224
+ });
225
+ if (destroyClearsRead) {
226
+ it("destroy removes the session", async () => {
227
+ const value = await store.create(sampleSession());
228
+ await store.destroy(value);
229
+ assertStrictEquals(await store.read(value), null);
230
+ });
231
+ }
232
+ it("never advertises a max age without proving it enforces one", () => {
233
+ const advertised = sessionStoreMaxAgeMs(store);
234
+ assertEquals(advertised !== undefined && makeBoundedStore === undefined, false, `this store advertises maxAgeMs=${advertised}, which HonoBff trusts ` +
235
+ "when it refuses a session cookie that would expire before the " +
236
+ "session does. Supply makeBoundedStore so the contract can prove " +
237
+ "the bound is real.");
238
+ });
239
+ if (makeBoundedStore) {
240
+ it("reports the max age it was built with, so a BFF can outlive it", async () => {
241
+ const bounded = await makeBoundedStore(BOUND_MS);
242
+ assertStrictEquals(sessionStoreMaxAgeMs(bounded), BOUND_MS, "a store that hides its bound gets no divergence check from HonoBff");
243
+ });
244
+ it("stops reading a session older than the max age it enforces", async () => {
245
+ const bounded = await makeBoundedStore(BOUND_MS);
246
+ const value = await bounded.create(sampleSession());
247
+ assert(await bounded.read(value) !== null, "a session created moments ago must still read");
248
+ await delay(BOUND_MS + 500);
249
+ assertStrictEquals(await bounded.read(value), null, "a session past the store's own bound must stop reading, or a " +
250
+ "captured cookie outlives the window the bound promises");
251
+ });
252
+ }
253
+ });
254
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Thin, optional Hono route factory over {@link IdentityService} for the
3
+ * **password-credential** flows.
4
+ *
5
+ * `honoIdentityRoutes(service, opts)` mounts exactly five POST endpoints —
6
+ * `/signup`, `/signin`, `/password/reset-request`, `/password/reset`, and
7
+ * `/email/verify` — by delegating to the service's methods. It is deliberately
8
+ * **not** an opinionated framework: the service methods are the real API, and
9
+ * this just wires the password path to routes for apps that want them mounted
10
+ * (the `IdentityService` stays usable directly from your own
11
+ * routes/loaders/actions).
12
+ *
13
+ * **Scope — password path only, by design.** The newer flows are intentionally
14
+ * *not* mounted here, because their safe wiring carries app-owned policy the
15
+ * factory can't guess. Drive them by calling the service methods from your own
16
+ * handlers, as each guide shows:
17
+ * - **MFA** (enroll / confirm / verify / disable / regenerate, via `MfaService`):
18
+ * *when* a challenge is required, where the pending challenge state lives, and
19
+ * the cookie shape are all yours — see `docs/guides/add-mfa.md`.
20
+ * - **Passwordless** (`requestSignInLink` / `consumeSignInLink`,
21
+ * `requestSignInCode` / `verifySignInCode`): the enumeration-safe response
22
+ * shape, whether `rate_limited` is swallowed, and IP throttling are yours —
23
+ * see `docs/guides/passwordless.md`.
24
+ * - **Social / OIDC** (start / callback, via `@udibo/oauth2/identity/external`):
25
+ * transient `state`/PKCE custody and the account-resolution/linking policy
26
+ * (where the account-takeover bugs live) are yours — see
27
+ * `docs/guides/social-sign-in.md`.
28
+ *
29
+ * Session creation stays in your app: the `signup`/`signin` routes call your
30
+ * `onAuthenticated(c, user)` hook on success, where you mint your session and
31
+ * return a `Response` (e.g. a redirect via `bff.loginContinuation`). Omit the
32
+ * hook to mount only the stateless reset/verify routes.
33
+ *
34
+ * Accepts JSON or form-encoded bodies. Responses are simple JSON
35
+ * (`{ ok: true }` / `{ error }`); render your own UI around them. Because a
36
+ * form-encoded POST is exactly what a cross-site page can forge, the routes
37
+ * refuse requests a browser reports as cross-site — see
38
+ * {@link HonoIdentityOptions.csrf}.
39
+ *
40
+ * @module
41
+ */
42
+ import "../../_dnt.polyfills.js";
43
+ import { Hono } from "hono";
44
+ import type { Context } from "hono";
45
+ import type { IdentityService, IdentityUser } from "../../identity/service.js";
46
+ /** Paths for the routes mounted by {@link honoIdentityRoutes}. */
47
+ export interface HonoIdentityPaths {
48
+ /** `POST` sign-up. Default `/signup`. */
49
+ signUp?: string;
50
+ /** `POST` sign-in. Default `/signin`. */
51
+ signIn?: string;
52
+ /** `POST` password-reset request (enumeration-safe). Default `/password/reset-request`. */
53
+ requestPasswordReset?: string;
54
+ /** `POST` password reset (consume token). Default `/password/reset`. */
55
+ resetPassword?: string;
56
+ /** `POST` email verification (consume token). Default `/email/verify`. */
57
+ verifyEmail?: string;
58
+ }
59
+ /**
60
+ * Tunes the same-origin guard {@link honoIdentityRoutes} mounts by default.
61
+ * Pass `csrf: false` instead to turn the guard off entirely — only when
62
+ * something in front of these routes already stops cross-site requests.
63
+ */
64
+ export interface HonoIdentityCsrfOptions {
65
+ /**
66
+ * Additional origins allowed to post to these routes, as exact
67
+ * `scheme://host[:port]` strings (e.g. `"https://app.example.com"`). Use it
68
+ * when the page holding your sign-in form is served from a different origin
69
+ * than the routes. The origin serving the routes is always allowed.
70
+ */
71
+ allowedOrigins?: string[];
72
+ }
73
+ /** Options for {@link honoIdentityRoutes}. */
74
+ export interface HonoIdentityOptions<User extends IdentityUser> {
75
+ /**
76
+ * Cross-site request protection, **on by default**. These routes carry no
77
+ * token of their own, so the guard reads what the browser states about the
78
+ * request: a `Sec-Fetch-Site` of `same-origin`/`none` passes, any other value
79
+ * is refused, and browsers too old to send it fall back to matching the
80
+ * `Origin` header's host against the request's. A request with neither header
81
+ * — curl, a server-side call, a native app — passes, since cross-site request
82
+ * forgery needs a browser.
83
+ *
84
+ * Refusals get `403` `{ error: "forbidden_origin" }` before any handler runs.
85
+ * Set {@link HonoIdentityCsrfOptions.allowedOrigins} for a legitimate
86
+ * cross-origin caller, or `false` to disable the guard when a proxy or an
87
+ * outer middleware already terminates CSRF.
88
+ */
89
+ csrf?: HonoIdentityCsrfOptions | false;
90
+ /**
91
+ * Called after `signup`/`signin` authenticates a user. Your app owns what
92
+ * happens next — create your session and return a `Response` (e.g. a redirect
93
+ * via `bff.loginContinuation`). **Required to mount the signup/signin routes**;
94
+ * omit it to mount only the stateless reset/verify routes.
95
+ */
96
+ onAuthenticated?: (c: Context, user: User, action: "signUp" | "signIn") => Response | Promise<Response>;
97
+ /** Override the default paths. */
98
+ paths?: HonoIdentityPaths;
99
+ }
100
+ /**
101
+ * Build a Hono app mounting the password-credential endpoints
102
+ * (`/signup`, `/signin`, `/password/reset-request`, `/password/reset`,
103
+ * `/email/verify`) over `service`. Mount it wherever you like, e.g.
104
+ * `app.route("/auth", honoIdentityRoutes(service, …))`.
105
+ *
106
+ * A JSON body keeps each field's type except `password`, `identifier`, `email`,
107
+ * and `token`, which are coerced to strings when present; on the sign-up route
108
+ * every other field is passed to `service.signUp` as `profile`. A form-encoded
109
+ * body is all strings.
110
+ *
111
+ * Every mounted route sits behind the same-origin guard described in
112
+ * {@link HonoIdentityOptions.csrf}, which is on unless you pass `csrf: false`.
113
+ *
114
+ * Passwordless, MFA, and social are intentionally not mounted — see the module
115
+ * doc for why and where to hand-wire each.
116
+ */
117
+ export declare function honoIdentityRoutes<User extends IdentityUser>(service: IdentityService<User>, options?: HonoIdentityOptions<User>): Hono;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Thin, optional Hono route factory over {@link IdentityService} for the
3
+ * **password-credential** flows.
4
+ *
5
+ * `honoIdentityRoutes(service, opts)` mounts exactly five POST endpoints —
6
+ * `/signup`, `/signin`, `/password/reset-request`, `/password/reset`, and
7
+ * `/email/verify` — by delegating to the service's methods. It is deliberately
8
+ * **not** an opinionated framework: the service methods are the real API, and
9
+ * this just wires the password path to routes for apps that want them mounted
10
+ * (the `IdentityService` stays usable directly from your own
11
+ * routes/loaders/actions).
12
+ *
13
+ * **Scope — password path only, by design.** The newer flows are intentionally
14
+ * *not* mounted here, because their safe wiring carries app-owned policy the
15
+ * factory can't guess. Drive them by calling the service methods from your own
16
+ * handlers, as each guide shows:
17
+ * - **MFA** (enroll / confirm / verify / disable / regenerate, via `MfaService`):
18
+ * *when* a challenge is required, where the pending challenge state lives, and
19
+ * the cookie shape are all yours — see `docs/guides/add-mfa.md`.
20
+ * - **Passwordless** (`requestSignInLink` / `consumeSignInLink`,
21
+ * `requestSignInCode` / `verifySignInCode`): the enumeration-safe response
22
+ * shape, whether `rate_limited` is swallowed, and IP throttling are yours —
23
+ * see `docs/guides/passwordless.md`.
24
+ * - **Social / OIDC** (start / callback, via `@udibo/oauth2/identity/external`):
25
+ * transient `state`/PKCE custody and the account-resolution/linking policy
26
+ * (where the account-takeover bugs live) are yours — see
27
+ * `docs/guides/social-sign-in.md`.
28
+ *
29
+ * Session creation stays in your app: the `signup`/`signin` routes call your
30
+ * `onAuthenticated(c, user)` hook on success, where you mint your session and
31
+ * return a `Response` (e.g. a redirect via `bff.loginContinuation`). Omit the
32
+ * hook to mount only the stateless reset/verify routes.
33
+ *
34
+ * Accepts JSON or form-encoded bodies. Responses are simple JSON
35
+ * (`{ ok: true }` / `{ error }`); render your own UI around them. Because a
36
+ * form-encoded POST is exactly what a cross-site page can forge, the routes
37
+ * refuse requests a browser reports as cross-site — see
38
+ * {@link HonoIdentityOptions.csrf}.
39
+ *
40
+ * @module
41
+ */
42
+ import "../../_dnt.polyfills.js";
43
+ import { Hono } from "hono";
44
+ import { identityErrorStatus, isIdentityError, } from "../../identity/errors.js";
45
+ /**
46
+ * Wrap a handler so a thrown {@link IdentityError} (e.g. `identifier_taken` from
47
+ * your user store, or `weak_password` from a policy check) becomes a consistent
48
+ * `{ error: code }` response with the conventional status; other errors
49
+ * propagate untouched.
50
+ */
51
+ function handle(fn) {
52
+ return async (c) => {
53
+ try {
54
+ return await fn(c);
55
+ }
56
+ catch (error) {
57
+ if (!isIdentityError(error))
58
+ throw error;
59
+ if (error.retryAfterMs !== undefined) {
60
+ c.header("Retry-After", String(Math.ceil(error.retryAfterMs / 1000)));
61
+ }
62
+ return c.json({ error: error.code }, identityErrorStatus(error.code));
63
+ }
64
+ };
65
+ }
66
+ const STRING_FIELDS = new Set(["password", "identifier", "email", "token"]);
67
+ /**
68
+ * Reads a JSON or form-encoded request body. A JSON body keeps each value's
69
+ * type except for {@link STRING_FIELDS}; a body that is not a JSON object
70
+ * (array, string, `null`) reads as empty. A form-encoded body is all strings by
71
+ * construction.
72
+ */
73
+ async function readBody(c) {
74
+ const contentType = c.req.header("content-type") ?? "";
75
+ if (contentType.includes("application/json")) {
76
+ try {
77
+ const body = await c.req.json();
78
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
79
+ return {};
80
+ }
81
+ const out = {};
82
+ for (const [key, value] of Object.entries(body)) {
83
+ out[key] = STRING_FIELDS.has(key) && value !== null &&
84
+ value !== undefined
85
+ ? String(value)
86
+ : value;
87
+ }
88
+ return out;
89
+ }
90
+ catch {
91
+ return {};
92
+ }
93
+ }
94
+ try {
95
+ const form = await c.req.formData();
96
+ const out = {};
97
+ for (const [key, value] of form)
98
+ out[key] = String(value);
99
+ return out;
100
+ }
101
+ catch {
102
+ return {};
103
+ }
104
+ }
105
+ const FORBIDDEN_ORIGIN = "forbidden_origin";
106
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
107
+ function hostOf(origin) {
108
+ try {
109
+ return new URL(origin).host;
110
+ }
111
+ catch {
112
+ return undefined;
113
+ }
114
+ }
115
+ function isTrustedOrigin(c, allowedOrigins) {
116
+ const origin = c.req.header("origin");
117
+ const site = c.req.header("sec-fetch-site");
118
+ if (site) {
119
+ return site === "same-origin" || site === "none" ||
120
+ (!!origin && allowedOrigins.has(origin));
121
+ }
122
+ if (!origin)
123
+ return true;
124
+ return allowedOrigins.has(origin) ||
125
+ hostOf(origin) === new URL(c.req.url).host;
126
+ }
127
+ /**
128
+ * Refuses an unsafe-method request a browser reports as coming from another
129
+ * site, per {@link HonoIdentityOptions.csrf}.
130
+ */
131
+ function sameOriginGuard(options) {
132
+ const allowedOrigins = new Set(options.allowedOrigins ?? []);
133
+ return async (c, next) => {
134
+ if (!SAFE_METHODS.has(c.req.method) && !isTrustedOrigin(c, allowedOrigins)) {
135
+ return c.json({ error: FORBIDDEN_ORIGIN }, identityErrorStatus(FORBIDDEN_ORIGIN));
136
+ }
137
+ await next();
138
+ };
139
+ }
140
+ /**
141
+ * Build a Hono app mounting the password-credential endpoints
142
+ * (`/signup`, `/signin`, `/password/reset-request`, `/password/reset`,
143
+ * `/email/verify`) over `service`. Mount it wherever you like, e.g.
144
+ * `app.route("/auth", honoIdentityRoutes(service, …))`.
145
+ *
146
+ * A JSON body keeps each field's type except `password`, `identifier`, `email`,
147
+ * and `token`, which are coerced to strings when present; on the sign-up route
148
+ * every other field is passed to `service.signUp` as `profile`. A form-encoded
149
+ * body is all strings.
150
+ *
151
+ * Every mounted route sits behind the same-origin guard described in
152
+ * {@link HonoIdentityOptions.csrf}, which is on unless you pass `csrf: false`.
153
+ *
154
+ * Passwordless, MFA, and social are intentionally not mounted — see the module
155
+ * doc for why and where to hand-wire each.
156
+ */
157
+ export function honoIdentityRoutes(service, options = {}) {
158
+ const app = new Hono();
159
+ const paths = {
160
+ signUp: "/signup",
161
+ signIn: "/signin",
162
+ requestPasswordReset: "/password/reset-request",
163
+ resetPassword: "/password/reset",
164
+ verifyEmail: "/email/verify",
165
+ ...options.paths,
166
+ };
167
+ const onAuthenticated = options.onAuthenticated;
168
+ if (options.csrf !== false) {
169
+ app.use("*", sameOriginGuard(options.csrf ?? {}));
170
+ }
171
+ if (onAuthenticated) {
172
+ app.post(paths.signUp, handle(async (c) => {
173
+ const { password, ...profile } = await readBody(c);
174
+ if (typeof password !== "string" || !password) {
175
+ return c.json({ error: "invalid_request" }, 400);
176
+ }
177
+ const user = await service.signUp({ password, profile });
178
+ return await onAuthenticated(c, user, "signUp");
179
+ }));
180
+ app.post(paths.signIn, handle(async (c) => {
181
+ const body = await readBody(c);
182
+ const user = await service.signIn({
183
+ identifier: String(body.identifier ?? ""),
184
+ password: String(body.password ?? ""),
185
+ });
186
+ if (!user)
187
+ return c.json({ error: "invalid_credentials" }, 401);
188
+ return await onAuthenticated(c, user, "signIn");
189
+ }));
190
+ }
191
+ app.post(paths.requestPasswordReset, handle(async (c) => {
192
+ const body = await readBody(c);
193
+ await service.requestPasswordReset(String(body.email ?? ""));
194
+ return c.json({ ok: true });
195
+ }));
196
+ app.post(paths.resetPassword, handle(async (c) => {
197
+ const body = await readBody(c);
198
+ const result = await service.resetPassword({
199
+ token: String(body.token ?? ""),
200
+ password: String(body.password ?? ""),
201
+ });
202
+ return result
203
+ ? c.json({ ok: true })
204
+ : c.json({ error: "invalid_token" }, 400);
205
+ }));
206
+ app.post(paths.verifyEmail, handle(async (c) => {
207
+ const body = await readBody(c);
208
+ const result = await service.verifyEmail(String(body.token ?? ""));
209
+ if (result.status === "success") {
210
+ return c.json({ ok: true, userId: result.userId });
211
+ }
212
+ return c.json({ error: `token_${result.status}` }, 400);
213
+ }));
214
+ return app;
215
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Request logging that removes query values, including OAuth callback credentials.
3
+ *
4
+ * @module hono/log
5
+ */
6
+ import "../../_dnt.polyfills.js";
7
+ import type { MiddlewareHandler } from "hono";
8
+ /**
9
+ * Produces a request target with every query value replaced by `[redacted]`.
10
+ * Keeps the pathname and percent-encoded parameter names, including repeats,
11
+ * and drops the origin and fragment. Use it before recording a request URL.
12
+ * Paths and parameter names must themselves contain no secrets.
13
+ *
14
+ * @param url An absolute request URL.
15
+ * @returns The pathname followed by redacted query parameters, if any.
16
+ * @throws TypeError if `url` is not a valid absolute URL.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { redactedRequestTarget } from "@udibo/oauth2/hono/log";
21
+ *
22
+ * const target = redactedRequestTarget(
23
+ * "https://app.example.com/auth/callback?code=secret&state=nonce",
24
+ * );
25
+ * // "/auth/callback?code=[redacted]&state=[redacted]"
26
+ * ```
27
+ */
28
+ export declare function redactedRequestTarget(url: string | URL): string;
29
+ /**
30
+ * Logs Hono requests and responses without any query values. Mount before
31
+ * authentication routes so OAuth callback codes and state stay out of logs.
32
+ * Logs method, redacted target, response status, and elapsed milliseconds;
33
+ * does not log headers or bodies. Paths and parameter names remain visible.
34
+ *
35
+ * @param print Receives each log line; defaults to `console.log`, resolved per line.
36
+ * @returns Middleware emitting a request line and, after downstream completion,
37
+ * a response line.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { Hono } from "hono";
42
+ * import { requestLogger } from "@udibo/oauth2/hono/log";
43
+ *
44
+ * const app = new Hono();
45
+ * app.use(requestLogger());
46
+ * app.get("/", (c) => c.text("Hello"));
47
+ * ```
48
+ */
49
+ export declare function requestLogger(print?: (line: string) => void): MiddlewareHandler;