@atproto/oauth-provider 0.3.1 → 0.5.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 (404) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +29 -0
  3. package/LICENSE.txt +1 -1
  4. package/dist/account/account-manager.d.ts +17 -3
  5. package/dist/account/account-manager.d.ts.map +1 -1
  6. package/dist/account/account-manager.js +102 -8
  7. package/dist/account/account-manager.js.map +1 -1
  8. package/dist/account/account-store.d.ts +81 -15
  9. package/dist/account/account-store.d.ts.map +1 -1
  10. package/dist/account/account-store.js +70 -19
  11. package/dist/account/account-store.js.map +1 -1
  12. package/dist/account/sign-in-data.d.ts +28 -0
  13. package/dist/account/sign-in-data.d.ts.map +1 -0
  14. package/dist/account/sign-in-data.js +16 -0
  15. package/dist/account/sign-in-data.js.map +1 -0
  16. package/dist/account/sign-up-data.d.ts +26 -0
  17. package/dist/account/sign-up-data.d.ts.map +1 -0
  18. package/dist/account/sign-up-data.js +11 -0
  19. package/dist/account/sign-up-data.js.map +1 -0
  20. package/dist/assets/app/bundle-manifest.json +598 -6
  21. package/dist/assets/app/index-ItwwtJ8r.js +36 -0
  22. package/dist/assets/app/index-ItwwtJ8r.js.map +1 -0
  23. package/dist/assets/app/main-B_dNxQo_.js +4 -0
  24. package/dist/assets/app/main-B_dNxQo_.js.map +1 -0
  25. package/dist/assets/app/main-CSatvmRR.css +3 -0
  26. package/dist/assets/app/main-CSatvmRR.js +306 -0
  27. package/dist/assets/app/main-CSatvmRR.js.map +1 -0
  28. package/dist/assets/app/messages-BQeltXSF.js +4 -0
  29. package/dist/assets/app/messages-BQeltXSF.js.map +1 -0
  30. package/dist/assets/app/messages-BQkEhfjg.js +4 -0
  31. package/dist/assets/app/messages-BQkEhfjg.js.map +1 -0
  32. package/dist/assets/app/messages-BUjKj_UJ.js +4 -0
  33. package/dist/assets/app/messages-BUjKj_UJ.js.map +1 -0
  34. package/dist/assets/app/messages-BWIQa8fO.js +4 -0
  35. package/dist/assets/app/messages-BWIQa8fO.js.map +1 -0
  36. package/dist/assets/app/messages-BaNVb0bp.js +4 -0
  37. package/dist/assets/app/messages-BaNVb0bp.js.map +1 -0
  38. package/dist/assets/app/messages-BaizVXcF.js +4 -0
  39. package/dist/assets/app/messages-BaizVXcF.js.map +1 -0
  40. package/dist/assets/app/messages-BfoClA1Y.js +4 -0
  41. package/dist/assets/app/messages-BfoClA1Y.js.map +1 -0
  42. package/dist/assets/app/messages-BsKGDZnC.js +4 -0
  43. package/dist/assets/app/messages-BsKGDZnC.js.map +1 -0
  44. package/dist/assets/app/messages-Bu-TJhml.js +4 -0
  45. package/dist/assets/app/messages-Bu-TJhml.js.map +1 -0
  46. package/dist/assets/app/messages-BvOKnBQk.js +4 -0
  47. package/dist/assets/app/messages-BvOKnBQk.js.map +1 -0
  48. package/dist/assets/app/messages-BxDzCiWz.js +4 -0
  49. package/dist/assets/app/messages-BxDzCiWz.js.map +1 -0
  50. package/dist/assets/app/messages-CDgFOy4S.js +4 -0
  51. package/dist/assets/app/messages-CDgFOy4S.js.map +1 -0
  52. package/dist/assets/app/messages-CLbTz0o9.js +4 -0
  53. package/dist/assets/app/messages-CLbTz0o9.js.map +1 -0
  54. package/dist/assets/app/messages-CNwSh0t7.js +4 -0
  55. package/dist/assets/app/messages-CNwSh0t7.js.map +1 -0
  56. package/dist/assets/app/messages-CSMNJ6P8.js +4 -0
  57. package/dist/assets/app/messages-CSMNJ6P8.js.map +1 -0
  58. package/dist/assets/app/messages-CZQUw3mp.js +4 -0
  59. package/dist/assets/app/messages-CZQUw3mp.js.map +1 -0
  60. package/dist/assets/app/messages-CZT41oVp.js +4 -0
  61. package/dist/assets/app/messages-CZT41oVp.js.map +1 -0
  62. package/dist/assets/app/messages-C_b-d3t8.js +4 -0
  63. package/dist/assets/app/messages-C_b-d3t8.js.map +1 -0
  64. package/dist/assets/app/messages-C_u3MTc2.js +4 -0
  65. package/dist/assets/app/messages-C_u3MTc2.js.map +1 -0
  66. package/dist/assets/app/messages-Cn8nHZic.js +4 -0
  67. package/dist/assets/app/messages-Cn8nHZic.js.map +1 -0
  68. package/dist/assets/app/messages-CtDywJUm.js +4 -0
  69. package/dist/assets/app/messages-CtDywJUm.js.map +1 -0
  70. package/dist/assets/app/messages-CurtIjBF.js +4 -0
  71. package/dist/assets/app/messages-CurtIjBF.js.map +1 -0
  72. package/dist/assets/app/messages-Cv6zIbaP.js +4 -0
  73. package/dist/assets/app/messages-Cv6zIbaP.js.map +1 -0
  74. package/dist/assets/app/messages-D1eLQuPE.js +4 -0
  75. package/dist/assets/app/messages-D1eLQuPE.js.map +1 -0
  76. package/dist/assets/app/messages-D8vHEaYW.js +4 -0
  77. package/dist/assets/app/messages-D8vHEaYW.js.map +1 -0
  78. package/dist/assets/app/messages-DJ1Q4GeC.js +4 -0
  79. package/dist/assets/app/messages-DJ1Q4GeC.js.map +1 -0
  80. package/dist/assets/app/messages-DRL3exqd.js +4 -0
  81. package/dist/assets/app/messages-DRL3exqd.js.map +1 -0
  82. package/dist/assets/app/messages-DWLPQRTp.js +4 -0
  83. package/dist/assets/app/messages-DWLPQRTp.js.map +1 -0
  84. package/dist/assets/app/messages-DjVaE9YE.js +4 -0
  85. package/dist/assets/app/messages-DjVaE9YE.js.map +1 -0
  86. package/dist/assets/app/messages-DqpMfFJR.js +4 -0
  87. package/dist/assets/app/messages-DqpMfFJR.js.map +1 -0
  88. package/dist/assets/app/messages-ETjhJBEN.js +4 -0
  89. package/dist/assets/app/messages-ETjhJBEN.js.map +1 -0
  90. package/dist/assets/app/messages-EUKrgrGn.js +4 -0
  91. package/dist/assets/app/messages-EUKrgrGn.js.map +1 -0
  92. package/dist/assets/app/messages-QQrOUcPW.js +4 -0
  93. package/dist/assets/app/messages-QQrOUcPW.js.map +1 -0
  94. package/dist/assets/app/messages-e2QGqFL6.js +4 -0
  95. package/dist/assets/app/messages-e2QGqFL6.js.map +1 -0
  96. package/dist/assets/app/messages-p61py7gD.js +4 -0
  97. package/dist/assets/app/messages-p61py7gD.js.map +1 -0
  98. package/dist/assets/asset.d.ts +1 -0
  99. package/dist/assets/asset.d.ts.map +1 -1
  100. package/dist/assets/assets-middleware.d.ts.map +1 -1
  101. package/dist/assets/assets-middleware.js +12 -7
  102. package/dist/assets/assets-middleware.js.map +1 -1
  103. package/dist/assets/index.d.ts +3 -2
  104. package/dist/assets/index.d.ts.map +1 -1
  105. package/dist/assets/index.js +13 -1
  106. package/dist/assets/index.js.map +1 -1
  107. package/dist/client/client-store.d.ts +3 -3
  108. package/dist/client/client-store.d.ts.map +1 -1
  109. package/dist/client/client-store.js +6 -5
  110. package/dist/client/client-store.js.map +1 -1
  111. package/dist/device/device-manager.d.ts +12 -13
  112. package/dist/device/device-manager.d.ts.map +1 -1
  113. package/dist/device/device-manager.js +5 -3
  114. package/dist/device/device-manager.js.map +1 -1
  115. package/dist/device/device-store.d.ts +3 -3
  116. package/dist/device/device-store.d.ts.map +1 -1
  117. package/dist/device/device-store.js +10 -9
  118. package/dist/device/device-store.js.map +1 -1
  119. package/dist/dpop/dpop-manager.d.ts +15 -7
  120. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  121. package/dist/dpop/dpop-manager.js +17 -3
  122. package/dist/dpop/dpop-manager.js.map +1 -1
  123. package/dist/dpop/dpop-nonce.d.ts +11 -5
  124. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  125. package/dist/dpop/dpop-nonce.js +47 -38
  126. package/dist/dpop/dpop-nonce.js.map +1 -1
  127. package/dist/errors/handle-unavailable-error.d.ts +11 -0
  128. package/dist/errors/handle-unavailable-error.d.ts.map +1 -0
  129. package/dist/errors/handle-unavailable-error.js +19 -0
  130. package/dist/errors/handle-unavailable-error.js.map +1 -0
  131. package/dist/errors/invalid-request-error.d.ts +6 -8
  132. package/dist/errors/invalid-request-error.d.ts.map +1 -1
  133. package/dist/errors/invalid-request-error.js +10 -8
  134. package/dist/errors/invalid-request-error.js.map +1 -1
  135. package/dist/lib/csp/index.d.ts +18 -0
  136. package/dist/lib/csp/index.d.ts.map +1 -0
  137. package/dist/lib/csp/index.js +72 -0
  138. package/dist/lib/csp/index.js.map +1 -0
  139. package/dist/lib/hcaptcha.d.ts +177 -0
  140. package/dist/lib/hcaptcha.d.ts.map +1 -0
  141. package/dist/lib/hcaptcha.js +155 -0
  142. package/dist/lib/hcaptcha.js.map +1 -0
  143. package/dist/lib/html/build-document.d.ts +11 -3
  144. package/dist/lib/html/build-document.d.ts.map +1 -1
  145. package/dist/lib/html/build-document.js +51 -15
  146. package/dist/lib/html/build-document.js.map +1 -1
  147. package/dist/lib/http/middleware.d.ts.map +1 -1
  148. package/dist/lib/http/middleware.js +4 -1
  149. package/dist/lib/http/middleware.js.map +1 -1
  150. package/dist/lib/http/request.d.ts +18 -3
  151. package/dist/lib/http/request.d.ts.map +1 -1
  152. package/dist/lib/http/request.js +56 -23
  153. package/dist/lib/http/request.js.map +1 -1
  154. package/dist/lib/http/response.d.ts +4 -2
  155. package/dist/lib/http/response.d.ts.map +1 -1
  156. package/dist/lib/http/response.js +23 -5
  157. package/dist/lib/http/response.js.map +1 -1
  158. package/dist/lib/locale.d.ts +15 -0
  159. package/dist/lib/locale.d.ts.map +1 -0
  160. package/dist/lib/locale.js +17 -0
  161. package/dist/lib/locale.js.map +1 -0
  162. package/dist/lib/util/function.d.ts +2 -2
  163. package/dist/lib/util/function.d.ts.map +1 -1
  164. package/dist/lib/util/function.js.map +1 -1
  165. package/dist/lib/util/type.d.ts +88 -1
  166. package/dist/lib/util/type.d.ts.map +1 -1
  167. package/dist/lib/util/type.js +41 -0
  168. package/dist/lib/util/type.js.map +1 -1
  169. package/dist/metadata/build-metadata.d.ts +2 -2
  170. package/dist/metadata/build-metadata.d.ts.map +1 -1
  171. package/dist/metadata/build-metadata.js.map +1 -1
  172. package/dist/oauth-errors.d.ts +1 -0
  173. package/dist/oauth-errors.d.ts.map +1 -1
  174. package/dist/oauth-errors.js +3 -1
  175. package/dist/oauth-errors.js.map +1 -1
  176. package/dist/oauth-hooks.d.ts +60 -3
  177. package/dist/oauth-hooks.d.ts.map +1 -1
  178. package/dist/oauth-hooks.js +3 -3
  179. package/dist/oauth-hooks.js.map +1 -1
  180. package/dist/oauth-provider.d.ts +28 -22
  181. package/dist/oauth-provider.d.ts.map +1 -1
  182. package/dist/oauth-provider.js +212 -211
  183. package/dist/oauth-provider.js.map +1 -1
  184. package/dist/oauth-verifier.d.ts +1 -1
  185. package/dist/oauth-verifier.d.ts.map +1 -1
  186. package/dist/oauth-verifier.js +2 -1
  187. package/dist/oauth-verifier.js.map +1 -1
  188. package/dist/output/build-authorize-data.d.ts +0 -1
  189. package/dist/output/build-authorize-data.d.ts.map +1 -1
  190. package/dist/output/build-authorize-data.js +0 -1
  191. package/dist/output/build-authorize-data.js.map +1 -1
  192. package/dist/output/build-customization-data.d.ts +232 -0
  193. package/dist/output/build-customization-data.d.ts.map +1 -0
  194. package/dist/output/build-customization-data.js +145 -0
  195. package/dist/output/build-customization-data.js.map +1 -0
  196. package/dist/output/output-manager.d.ts +16 -9
  197. package/dist/output/output-manager.d.ts.map +1 -1
  198. package/dist/output/output-manager.js +78 -42
  199. package/dist/output/output-manager.js.map +1 -1
  200. package/dist/output/send-authorize-redirect.d.ts +9 -6
  201. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  202. package/dist/output/send-authorize-redirect.js +20 -14
  203. package/dist/output/send-authorize-redirect.js.map +1 -1
  204. package/dist/output/send-web-page.d.ts +7 -2
  205. package/dist/output/send-web-page.d.ts.map +1 -1
  206. package/dist/output/send-web-page.js +37 -21
  207. package/dist/output/send-web-page.js.map +1 -1
  208. package/dist/request/request-manager.d.ts +1 -1
  209. package/dist/request/request-manager.d.ts.map +1 -1
  210. package/dist/request/request-manager.js +4 -4
  211. package/dist/request/request-manager.js.map +1 -1
  212. package/dist/request/request-store.d.ts +3 -3
  213. package/dist/request/request-store.d.ts.map +1 -1
  214. package/dist/request/request-store.js +11 -10
  215. package/dist/request/request-store.js.map +1 -1
  216. package/dist/token/token-store.d.ts +4 -4
  217. package/dist/token/token-store.d.ts.map +1 -1
  218. package/dist/token/token-store.js +13 -12
  219. package/dist/token/token-store.js.map +1 -1
  220. package/package.json +46 -21
  221. package/rollup.config.js +61 -17
  222. package/src/account/account-manager.ts +159 -8
  223. package/src/account/account-store.ts +127 -32
  224. package/src/account/sign-in-data.ts +15 -0
  225. package/src/account/sign-up-data.ts +11 -0
  226. package/src/assets/app/app.tsx +31 -16
  227. package/src/assets/app/backend-data.ts +15 -60
  228. package/src/assets/app/backend-types.ts +66 -0
  229. package/src/assets/app/components/forms/button-toggle-visibility.tsx +43 -0
  230. package/src/assets/app/components/forms/button.tsx +60 -0
  231. package/src/assets/app/components/forms/fieldset.tsx +55 -0
  232. package/src/assets/app/components/forms/form-card-async.tsx +103 -0
  233. package/src/assets/app/components/forms/form-card.tsx +49 -0
  234. package/src/assets/app/components/forms/input-checkbox.tsx +73 -0
  235. package/src/assets/app/components/forms/input-container.tsx +107 -0
  236. package/src/assets/app/components/forms/input-email-address.tsx +66 -0
  237. package/src/assets/app/components/forms/input-new-password.tsx +62 -0
  238. package/src/assets/app/components/forms/input-password.tsx +88 -0
  239. package/src/assets/app/components/forms/input-text.tsx +76 -0
  240. package/src/assets/app/components/forms/input-token.tsx +94 -0
  241. package/src/assets/app/components/forms/wizard-card.tsx +116 -0
  242. package/src/assets/app/components/layouts/layout-title-page.tsx +77 -0
  243. package/src/assets/app/components/layouts/layout-welcome.tsx +73 -0
  244. package/src/assets/app/components/utils/account-identifier.tsx +23 -0
  245. package/src/assets/app/components/utils/account-image.tsx +33 -0
  246. package/src/assets/app/components/utils/admonition.tsx +52 -0
  247. package/src/assets/app/components/utils/client-name.tsx +45 -0
  248. package/src/assets/app/components/utils/error-card.tsx +93 -0
  249. package/src/assets/app/components/utils/error-message.tsx +62 -0
  250. package/src/assets/app/components/utils/help-card.tsx +46 -0
  251. package/src/assets/app/components/utils/icons.tsx +88 -0
  252. package/src/assets/app/components/utils/link-anchor.tsx +28 -0
  253. package/src/assets/app/components/utils/link-title.tsx +26 -0
  254. package/src/assets/app/components/utils/multi-lang-string.tsx +56 -0
  255. package/src/assets/app/components/utils/password-strength-label.tsx +37 -0
  256. package/src/assets/app/components/utils/password-strength-meter.tsx +58 -0
  257. package/src/assets/app/components/{url-viewer.tsx → utils/url-viewer.tsx} +9 -6
  258. package/src/assets/app/hooks/use-api.ts +128 -55
  259. package/src/assets/app/hooks/use-async-action.ts +120 -0
  260. package/src/assets/app/hooks/use-browser-color-scheme.ts +31 -0
  261. package/src/assets/app/hooks/use-csrf-token.ts +1 -1
  262. package/src/assets/app/hooks/use-random-string.ts +37 -0
  263. package/src/assets/app/hooks/use-stepper.ts +87 -0
  264. package/src/assets/app/index.html +182 -0
  265. package/src/assets/app/lib/api.ts +248 -79
  266. package/src/assets/app/lib/clsx.ts +5 -8
  267. package/src/assets/app/lib/json-client.ts +94 -0
  268. package/src/assets/app/lib/password.ts +98 -0
  269. package/src/assets/app/lib/ref.ts +17 -0
  270. package/src/assets/app/locales/an/messages.po +492 -0
  271. package/src/assets/app/locales/ast/messages.po +492 -0
  272. package/src/assets/app/locales/ca/messages.po +492 -0
  273. package/src/assets/app/locales/da/messages.po +492 -0
  274. package/src/assets/app/locales/de/messages.po +492 -0
  275. package/src/assets/app/locales/el/messages.po +492 -0
  276. package/src/assets/app/locales/en/messages.po +492 -0
  277. package/src/assets/app/locales/en-GB/messages.po +492 -0
  278. package/src/assets/app/locales/es/messages.po +492 -0
  279. package/src/assets/app/locales/eu/messages.po +492 -0
  280. package/src/assets/app/locales/fi/messages.po +492 -0
  281. package/src/assets/app/locales/fr/messages.po +492 -0
  282. package/src/assets/app/locales/ga/messages.po +492 -0
  283. package/src/assets/app/locales/gl/messages.po +492 -0
  284. package/src/assets/app/locales/hi/messages.po +492 -0
  285. package/src/assets/app/locales/hu/messages.po +492 -0
  286. package/src/assets/app/locales/ia/messages.po +492 -0
  287. package/src/assets/app/locales/id/messages.po +492 -0
  288. package/src/assets/app/locales/it/messages.po +492 -0
  289. package/src/assets/app/locales/ja/messages.po +492 -0
  290. package/src/assets/app/locales/km/messages.po +492 -0
  291. package/src/assets/app/locales/ko/messages.po +492 -0
  292. package/src/assets/app/locales/load.ts +8 -0
  293. package/src/assets/app/locales/locale-context.ts +19 -0
  294. package/src/assets/app/locales/locale-provider.tsx +112 -0
  295. package/src/assets/app/locales/locale-selector.tsx +58 -0
  296. package/src/assets/app/locales/locales.ts +168 -0
  297. package/src/assets/app/locales/ne/messages.po +492 -0
  298. package/src/assets/app/locales/nl/messages.po +492 -0
  299. package/src/assets/app/locales/pl/messages.po +492 -0
  300. package/src/assets/app/locales/pt-BR/messages.po +492 -0
  301. package/src/assets/app/locales/ro/messages.po +492 -0
  302. package/src/assets/app/locales/ru/messages.po +492 -0
  303. package/src/assets/app/locales/sv/messages.po +492 -0
  304. package/src/assets/app/locales/th/messages.po +492 -0
  305. package/src/assets/app/locales/tr/messages.po +492 -0
  306. package/src/assets/app/locales/uk/messages.po +492 -0
  307. package/src/assets/app/locales/vi/messages.po +492 -0
  308. package/src/assets/app/locales/zh-CN/messages.po +492 -0
  309. package/src/assets/app/locales/zh-HK/messages.po +492 -0
  310. package/src/assets/app/locales/zh-TW/messages.po +492 -0
  311. package/src/assets/app/main.css +23 -2
  312. package/src/assets/app/main.tsx +24 -8
  313. package/src/assets/app/views/authorize/accept/accept-form.tsx +150 -0
  314. package/src/assets/app/views/authorize/accept/accept-view.tsx +70 -0
  315. package/src/assets/app/views/authorize/authorize-view.tsx +180 -0
  316. package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  317. package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  318. package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +127 -0
  319. package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +244 -0
  320. package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  321. package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +145 -0
  322. package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +140 -0
  323. package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  324. package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +289 -0
  325. package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  326. package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +158 -0
  327. package/src/assets/app/views/authorize/welcome/welcome-view.tsx +56 -0
  328. package/src/assets/app/views/error/error-view.tsx +31 -0
  329. package/src/assets/asset.ts +1 -0
  330. package/src/assets/assets-middleware.ts +13 -8
  331. package/src/assets/index.ts +15 -2
  332. package/src/client/client-store.ts +10 -12
  333. package/src/device/device-manager.ts +14 -15
  334. package/src/device/device-store.ts +9 -15
  335. package/src/dpop/dpop-manager.ts +20 -8
  336. package/src/dpop/dpop-nonce.ts +58 -40
  337. package/src/errors/handle-unavailable-error.ts +18 -0
  338. package/src/errors/invalid-request-error.ts +10 -8
  339. package/src/lib/csp/index.ts +98 -0
  340. package/src/lib/hcaptcha.ts +182 -0
  341. package/src/lib/html/build-document.ts +60 -16
  342. package/src/lib/http/middleware.ts +4 -3
  343. package/src/lib/http/request.ts +81 -28
  344. package/src/lib/http/response.ts +22 -9
  345. package/src/lib/locale.ts +21 -0
  346. package/src/lib/util/function.ts +0 -3
  347. package/src/lib/util/type.ts +130 -1
  348. package/src/metadata/build-metadata.ts +2 -1
  349. package/src/oauth-errors.ts +1 -0
  350. package/src/oauth-hooks.ts +69 -3
  351. package/src/oauth-provider.ts +410 -315
  352. package/src/oauth-verifier.ts +3 -1
  353. package/src/output/build-authorize-data.ts +1 -3
  354. package/src/output/build-customization-data.ts +189 -0
  355. package/src/output/output-manager.ts +111 -48
  356. package/src/output/send-authorize-redirect.ts +43 -36
  357. package/src/output/send-web-page.ts +40 -26
  358. package/src/request/request-manager.ts +4 -4
  359. package/src/request/request-store.ts +12 -16
  360. package/src/token/token-store.ts +14 -18
  361. package/tailwind.config.js +5 -0
  362. package/tsconfig.backend.tsbuildinfo +1 -1
  363. package/tsconfig.frontend.tsbuildinfo +1 -1
  364. package/tsconfig.tools.tsbuildinfo +1 -1
  365. package/vite.config.mjs +16 -0
  366. package/.postcssrc.yml +0 -3
  367. package/dist/assets/app/main.css +0 -3
  368. package/dist/assets/app/main.js +0 -20
  369. package/dist/assets/app/main.js.map +0 -1
  370. package/dist/output/customization.d.ts +0 -27
  371. package/dist/output/customization.d.ts.map +0 -1
  372. package/dist/output/customization.js +0 -88
  373. package/dist/output/customization.js.map +0 -1
  374. package/src/assets/app/components/accept-form.tsx +0 -137
  375. package/src/assets/app/components/account-identifier.tsx +0 -18
  376. package/src/assets/app/components/account-picker.tsx +0 -127
  377. package/src/assets/app/components/button.tsx +0 -34
  378. package/src/assets/app/components/client-name.tsx +0 -37
  379. package/src/assets/app/components/fieldset.tsx +0 -26
  380. package/src/assets/app/components/form-card.tsx +0 -47
  381. package/src/assets/app/components/help-card.tsx +0 -42
  382. package/src/assets/app/components/icons/alert-icon.tsx +0 -5
  383. package/src/assets/app/components/icons/at-symbol-icon.tsx +0 -5
  384. package/src/assets/app/components/icons/caret-right-icon.tsx +0 -5
  385. package/src/assets/app/components/icons/lock-icon.tsx +0 -5
  386. package/src/assets/app/components/icons/token-icon.tsx +0 -5
  387. package/src/assets/app/components/icons/util.tsx +0 -17
  388. package/src/assets/app/components/info-card.tsx +0 -45
  389. package/src/assets/app/components/input-checkbox.tsx +0 -47
  390. package/src/assets/app/components/input-container.tsx +0 -37
  391. package/src/assets/app/components/input-layout.tsx +0 -47
  392. package/src/assets/app/components/input-text.tsx +0 -69
  393. package/src/assets/app/components/layout-title-page.tsx +0 -60
  394. package/src/assets/app/components/layout-welcome.tsx +0 -74
  395. package/src/assets/app/components/sign-in-form.tsx +0 -337
  396. package/src/assets/app/components/sign-up-account-form.tsx +0 -194
  397. package/src/assets/app/components/sign-up-disclaimer.tsx +0 -44
  398. package/src/assets/app/views/accept-view.tsx +0 -55
  399. package/src/assets/app/views/authorize-view.tsx +0 -106
  400. package/src/assets/app/views/error-view.tsx +0 -36
  401. package/src/assets/app/views/sign-in-view.tsx +0 -111
  402. package/src/assets/app/views/sign-up-view.tsx +0 -86
  403. package/src/assets/app/views/welcome-view.tsx +0 -54
  404. package/src/output/customization.ts +0 -118
@@ -0,0 +1,182 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { z } from 'zod'
3
+ import {
4
+ Fetch,
5
+ FetchBound,
6
+ bindFetch,
7
+ fetchJsonProcessor,
8
+ fetchJsonZodProcessor,
9
+ fetchOkProcessor,
10
+ } from '@atproto-labs/fetch'
11
+ import { pipe } from '@atproto-labs/pipe'
12
+
13
+ export const hcaptchaTokenSchema = z.string().min(1)
14
+ export type HcaptchaToken = z.infer<typeof hcaptchaTokenSchema>
15
+
16
+ export const hcaptchaConfigSchema = z.object({
17
+ /**
18
+ * The hCaptcha site key to use for the sign-up form.
19
+ */
20
+ siteKey: z.string().min(1),
21
+ /**
22
+ * The hCaptcha secret key to use for the sign-up form.
23
+ */
24
+ secretKey: z.string().min(1),
25
+ /**
26
+ * A salt to use when hashing client tokens.
27
+ */
28
+ tokenSalt: z.string().min(1),
29
+ /**
30
+ * The risk score over which the user is considered a threat and will be
31
+ * denied access. This will be ignored if the enterprise features are not
32
+ * available.
33
+ */
34
+ scoreThreshold: z.number().optional(),
35
+ })
36
+ export type HcaptchaConfig = z.infer<typeof hcaptchaConfigSchema>
37
+
38
+ /**
39
+ * @see {@link https://docs.hcaptcha.com/#verify-the-user-response-server-side hCaptcha API}
40
+ */
41
+ export const hcaptchaVerifyResultSchema = z.object({
42
+ /**
43
+ * is the passcode valid, and does it meet security criteria you specified, e.g. sitekey?
44
+ */
45
+ success: z.boolean(),
46
+ /**
47
+ * timestamp of the challenge (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
48
+ */
49
+ challenge_ts: z.string(),
50
+ /**
51
+ * the hostname of the site where the challenge was passed
52
+ */
53
+ hostname: z.string(),
54
+ /**
55
+ * optional: any error codes
56
+ */
57
+ 'error-codes': z.array(z.string()),
58
+ /**
59
+ * ENTERPRISE feature: a score denoting malicious activity. Value ranges from
60
+ * 0.0 (no risk) to 1.0 (confirmed threat).
61
+ */
62
+ score: z.number().optional(),
63
+ /**
64
+ * ENTERPRISE feature: reason(s) for score.
65
+ */
66
+ score_reason: z.array(z.string()).optional(),
67
+ /**
68
+ * sitekey of the request
69
+ */
70
+ sitekey: z.string().optional(),
71
+ /**
72
+ * obj of form: {'ip_device': 1, .. etc}
73
+ */
74
+ behavior_counts: z.record(z.unknown()).optional(),
75
+ /**
76
+ * how similar is this? (0.0 - 1.0, -1 on err)
77
+ */
78
+ similarity: z.number().optional(),
79
+ /**
80
+ * count of similar_tokens not processed
81
+ */
82
+ similarity_failures: z.number().optional(),
83
+ /**
84
+ * array of strings for any similarity errors
85
+ */
86
+ similarity_error_details: z.array(z.string()).optional(),
87
+ /**
88
+ * encoded clientID
89
+ */
90
+ scoped_uid_0: z.string().optional(),
91
+ /**
92
+ * encoded IP
93
+ */
94
+ scoped_uid_1: z.string().optional(),
95
+ /**
96
+ * encoded IP (APT)
97
+ */
98
+ scoped_uid_2: z.string().optional(),
99
+ /**
100
+ * Risk Insights (APT + RI)
101
+ */
102
+ risk_insights: z.record(z.unknown()).optional(),
103
+ /**
104
+ * Advanced Threat Signatures (APT)
105
+ */
106
+ sigs: z.record(z.unknown()).optional(),
107
+ /**
108
+ * tags added via Rules
109
+ */
110
+ tags: z.array(z.string()).optional(),
111
+ })
112
+
113
+ export type HcaptchaVerifyResult = z.infer<typeof hcaptchaVerifyResultSchema>
114
+
115
+ const fetchSuccessHandler = pipe(
116
+ fetchOkProcessor(),
117
+ fetchJsonProcessor(),
118
+ fetchJsonZodProcessor(hcaptchaVerifyResultSchema),
119
+ )
120
+
121
+ export class HCaptchaClient {
122
+ protected readonly fetch: FetchBound
123
+ constructor(
124
+ private readonly hostname: string,
125
+ private readonly config: HcaptchaConfig,
126
+ fetch: Fetch = globalThis.fetch,
127
+ ) {
128
+ this.fetch = bindFetch(fetch)
129
+ }
130
+
131
+ async verify(
132
+ behaviorType: 'login' | 'signup',
133
+ response: string,
134
+ remoteip: string,
135
+ handle: string,
136
+ userAgent?: string,
137
+ ) {
138
+ const result = await this.fetch('https://api.hcaptcha.com/siteverify', {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/x-www-form-urlencoded',
142
+ },
143
+ body: new URLSearchParams({
144
+ secret: this.config.secretKey,
145
+ sitekey: this.config.siteKey,
146
+ behavior_type: behaviorType,
147
+ response,
148
+ remoteip,
149
+ client_tokens: JSON.stringify({
150
+ hashedIp: this.hashToken(remoteip),
151
+ hashedHandle: this.hashToken(handle),
152
+ hashedUserAgent: userAgent ? this.hashToken(userAgent) : undefined,
153
+ }),
154
+ }).toString(),
155
+ }).then(fetchSuccessHandler)
156
+
157
+ return {
158
+ allowed: this.isAllowed(result),
159
+ result,
160
+ }
161
+ }
162
+
163
+ isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
164
+ return (
165
+ success &&
166
+ // Fool-proofing: If this is false, the user is trying to use a token
167
+ // generated for the same siteKey, but on another domain.
168
+ hostname === this.hostname &&
169
+ // Ignore if enterprise feature is not enabled
170
+ score != null &&
171
+ this.config.scoreThreshold != null &&
172
+ score < this.config.scoreThreshold
173
+ )
174
+ }
175
+
176
+ hashToken(value: string) {
177
+ const hash = createHash('sha256')
178
+ hash.update(this.config.tokenSalt)
179
+ hash.update(value)
180
+ return hash.digest().toString('base64')
181
+ }
182
+ }
@@ -8,7 +8,43 @@ export type AssetRef = {
8
8
  }
9
9
 
10
10
  export type Attrs = Record<string, boolean | string | undefined>
11
- export type LinkAttrs = { href: string } & Attrs
11
+
12
+ /**
13
+ * @see {@link https://developer.mozilla.org/fr/docs/Web/HTML/Attributes/rel}
14
+ */
15
+ const ALLOWED_LINK_REL_VALUES = Object.freeze([
16
+ 'alternate',
17
+ 'author',
18
+ 'canonical',
19
+ 'dns-prefetch',
20
+ 'external',
21
+ 'expect',
22
+ 'help',
23
+ 'icon',
24
+ 'license',
25
+ 'manifest',
26
+ 'me',
27
+ 'modulepreload',
28
+ 'next',
29
+ 'pingback',
30
+ 'preconnect',
31
+ 'prefetch',
32
+ 'preload',
33
+ 'prerender',
34
+ 'prev',
35
+ 'privacy-policy',
36
+ 'search',
37
+ 'stylesheet',
38
+ 'terms-of-service',
39
+ ] as const)
40
+ export type LinkRel = (typeof ALLOWED_LINK_REL_VALUES)[number]
41
+ export const isLinkRel = (rel: unknown): rel is LinkRel =>
42
+ (ALLOWED_LINK_REL_VALUES as readonly unknown[]).includes(rel)
43
+
44
+ export type LinkAttrs = Attrs & {
45
+ href: string
46
+ rel: LinkRel
47
+ }
12
48
  export type MetaAttrs =
13
49
  | { name: string; content: string }
14
50
  | { 'http-equiv': string; content: string }
@@ -27,7 +63,7 @@ export type BuildDocumentOptions = {
27
63
  title?: HtmlValue
28
64
  scripts?: readonly (Html | AssetRef)[]
29
65
  styles?: readonly (Html | AssetRef)[]
30
- body: HtmlValue
66
+ body?: HtmlValue
31
67
  bodyAttrs?: Attrs
32
68
  }
33
69
 
@@ -50,12 +86,13 @@ export const buildDocument = ({
50
86
  ${base && html`<base href="${base.href}" />`}
51
87
  ${meta?.some(isViewportMeta) ? null : defaultViewport}
52
88
  ${meta?.map(metaToHtml)}
89
+ ${styles?.map(linkPreload('style'))}
90
+ ${scripts?.map(linkPreload('script'))}
53
91
  ${links?.map(linkToHtml)}
54
- ${head} ${styles?.map(styleToHtml)}
92
+ ${head}
93
+ ${styles?.map(styleToHtml)}
55
94
  </head>
56
- <body${attrsToHtml(bodyAttrs)}>
57
- ${body} ${scripts?.map(scriptToHtml)}
58
- </body>
95
+ <body${attrsToHtml(bodyAttrs)}>${body}${scripts?.map(scriptToHtml)}</body>
59
96
  </html>`
60
97
 
61
98
  function isViewportMeta<T extends MetaAttrs>(
@@ -64,12 +101,12 @@ function isViewportMeta<T extends MetaAttrs>(
64
101
  return 'name' in attrs && attrs.name === 'viewport'
65
102
  }
66
103
 
67
- function* linkToHtml(attrs: LinkAttrs) {
68
- yield html`<link${attrsToHtml(attrs)} />`
104
+ function linkToHtml(attrs: LinkAttrs) {
105
+ return html`<link${attrsToHtml(attrs)} />`
69
106
  }
70
107
 
71
- function* metaToHtml(attrs: MetaAttrs) {
72
- yield html`<meta${attrsToHtml(attrs)} />`
108
+ function metaToHtml(attrs: MetaAttrs) {
109
+ return html`<meta${attrsToHtml(attrs)} />`
73
110
  }
74
111
 
75
112
  function* attrsToHtml(attrs?: Attrs) {
@@ -83,16 +120,23 @@ function* attrsToHtml(attrs?: Attrs) {
83
120
  }
84
121
  }
85
122
 
86
- function* scriptToHtml(script: Html | AssetRef) {
87
- yield script instanceof Html
123
+ function linkPreload(as: 'script' | 'style') {
124
+ return (style: Html | AssetRef) =>
125
+ style instanceof Html
126
+ ? undefined
127
+ : html`<link rel="preload" href="${style.url}" as="${as}" />`
128
+ }
129
+
130
+ function scriptToHtml(script: Html | AssetRef) {
131
+ return script instanceof Html
88
132
  ? // prettier-ignore
89
133
  html`<script>${script}</script>` // hash validity requires no space around the content
90
- : html`<script type="module" src="${script.url}?${script.sha256}"></script>`
134
+ : html`<script type="module" src="${script.url}"></script>`
91
135
  }
92
136
 
93
- function* styleToHtml(style: Html | AssetRef) {
94
- yield style instanceof Html
137
+ function styleToHtml(style: Html | AssetRef) {
138
+ return style instanceof Html
95
139
  ? // prettier-ignore
96
140
  html`<style>${style}</style>` // hash validity requires no space around the content
97
- : html`<link rel="stylesheet" href="${style.url}?${style.sha256}" />`
141
+ : html`<link rel="stylesheet" href="${style.url}" />`
98
142
  }
@@ -2,6 +2,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
2
2
  import { writeJson } from './response.js'
3
3
  import { Handler, Middleware, NextFunction } from './types.js'
4
4
 
5
+ const isNonNullable = <X>(x: X): x is NonNullable<X> => x != null
6
+
5
7
  export function combineMiddlewares<M extends Middleware<any, any, any>>(
6
8
  middlewares: Iterable<null | undefined | M>,
7
9
  options?: { skipKeyword?: string },
@@ -15,12 +17,11 @@ export function combineMiddlewares(
15
17
  middlewares: Iterable<null | undefined | Middleware<unknown>>,
16
18
  { skipKeyword }: { skipKeyword?: string } = {},
17
19
  ): Middleware<unknown> {
18
- const middlewaresArray = Array.from(middlewares).filter(
19
- (x): x is NonNullable<typeof x> => x != null,
20
- )
20
+ const middlewaresArray = Array.from(middlewares).filter(isNonNullable)
21
21
 
22
22
  // Optimization: if there are no middlewares, return a noop middleware.
23
23
  if (middlewaresArray.length === 0) return (req, res, next) => void next()
24
+ if (middlewaresArray.length === 1) return middlewaresArray[0]
24
25
 
25
26
  return function (req, res, next) {
26
27
  let i = 0
@@ -1,6 +1,8 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import { languages, mediaType } from '@hapi/accept'
3
4
  import { parse as parseCookie, serialize as serializeCookie } from 'cookie'
5
+ import forwarded from 'forwarded'
4
6
  import createHttpError from 'http-errors'
5
7
  import { appendHeader } from './response.js'
6
8
  import { UrlReference, urlMatch } from './url.js'
@@ -78,6 +80,18 @@ export function validateFetchSite(
78
80
  validateHeaderValue(req, 'sec-fetch-site', expectedSite)
79
81
  }
80
82
 
83
+ export function validateReferer(
84
+ req: IncomingMessage,
85
+ res: ServerResponse,
86
+ reference: UrlReference,
87
+ allowNull: true,
88
+ ): URL | null
89
+ export function validateReferer(
90
+ req: IncomingMessage,
91
+ res: ServerResponse,
92
+ reference: UrlReference,
93
+ allowNull?: false,
94
+ ): URL
81
95
  export function validateReferer(
82
96
  req: IncomingMessage,
83
97
  res: ServerResponse,
@@ -89,6 +103,7 @@ export function validateReferer(
89
103
  if (refererUrl ? !urlMatch(refererUrl, reference) : !allowNull) {
90
104
  throw createHttpError(400, `Invalid referer ${referer}`)
91
105
  }
106
+ return refererUrl
92
107
  }
93
108
 
94
109
  export async function setupCsrfToken(
@@ -125,12 +140,13 @@ export function validateSameOrigin(
125
140
  export function validateCsrfToken(
126
141
  req: IncomingMessage,
127
142
  res: ServerResponse,
128
- csrfToken: string,
143
+ csrfToken: unknown,
129
144
  cookieName = 'csrf_token',
130
145
  clearCookie = false,
131
146
  ) {
132
147
  const cookies = parseHttpCookies(req)
133
148
  if (
149
+ typeof csrfToken !== 'string' ||
134
150
  !csrfToken ||
135
151
  !cookies ||
136
152
  !cookieName ||
@@ -164,7 +180,19 @@ export function parseHttpCookies(
164
180
  }
165
181
 
166
182
  export type ExtractRequestMetadataOptions = {
167
- trustProxy?: boolean
183
+ /**
184
+ * A function that determines whether a given IP address is trusted. The
185
+ * function is called with the IP addresses and its index in the list of
186
+ * forwarded addresses (starting from 0, 0 corresponding to the ip of the
187
+ * incoming HTTP connection, and the last item being the first proxied IP
188
+ * address in the proxy chain, deduced from the `X-Forwarded-For` header). The
189
+ * function should return `true` if the IP address is trusted, and `false`
190
+ * otherwise.
191
+ *
192
+ * @see {@link https://www.npmjs.com/package/proxy-addr} for a utility that
193
+ * allows you to create a trust function.
194
+ */
195
+ trustProxy?: (addr: string, i: number) => boolean
168
196
  }
169
197
 
170
198
  export type RequestMetadata = {
@@ -177,42 +205,49 @@ export function extractRequestMetadata(
177
205
  req: IncomingMessage,
178
206
  options?: ExtractRequestMetadataOptions,
179
207
  ): RequestMetadata {
180
- const userAgent = req.headers['user-agent'] || undefined
181
- const ipAddress = extractIpAddress(req, options) || null
182
- const port = extractPort(req, options)
183
-
184
- if (ipAddress == null || port == null) {
185
- throw new Error('Could not determine IP address')
208
+ const ip = extractIp(req, options)
209
+ return {
210
+ userAgent: req.headers['user-agent'],
211
+ ipAddress: ip,
212
+ port: extractPort(req, ip),
186
213
  }
187
-
188
- return { userAgent, ipAddress, port }
189
214
  }
190
215
 
191
- function extractIpAddress(
216
+ function extractIp(
192
217
  req: IncomingMessage,
193
218
  options?: ExtractRequestMetadataOptions,
194
- ): string | undefined {
195
- // Express app compatibility
196
- if ('ip' in req && typeof req.ip === 'string') {
197
- return req.ip
219
+ ): string {
220
+ const trust = options?.trustProxy
221
+ if (trust) {
222
+ const ips = forwarded(req)
223
+ for (let i = 0; i < ips.length; i++) {
224
+ const isTrusted = trust(ips[i], i)
225
+ if (!isTrusted) return ips[i]
226
+ }
227
+ // Let's return the last ("furthest") IP address in the chain if all of them
228
+ // are trusted. Note that this may indicate an issue with either the trust
229
+ // function (too permissive), or the proxy configuration (one of them not
230
+ // setting the X-Forwarded-For header).
231
+ const ip = ips[ips.length - 1]
232
+ if (ip) return ip
198
233
  }
199
234
 
200
- if (options?.trustProxy) {
201
- const forwardedFor = req.headers['x-forwarded-for']
202
- if (typeof forwardedFor === 'string') {
203
- const firstForward = forwardedFor.split(',')[0]!.trim()
204
- if (firstForward) return firstForward
205
- }
235
+ // Express app compatibility (see "trust proxy" setting)
236
+ if ('ip' in req) {
237
+ const ip = req.ip
238
+ if (typeof ip === 'string') return ip
206
239
  }
207
240
 
208
- return req.socket.remoteAddress
241
+ const ip = req.socket.remoteAddress
242
+ if (ip) return ip
243
+
244
+ throw new Error('Could not determine IP address')
209
245
  }
210
246
 
211
- function extractPort(
212
- req: IncomingMessage,
213
- options?: ExtractRequestMetadataOptions,
214
- ): number | undefined {
215
- if (options?.trustProxy) {
247
+ function extractPort(req: IncomingMessage, ip: string): number {
248
+ if (ip !== req.socket.remoteAddress) {
249
+ // Trust the X-Forwarded-Port header only if the IP address was a trusted
250
+ // proxied IP.
216
251
  const forwardedPort = req.headers['x-forwarded-port']
217
252
  if (typeof forwardedPort === 'string') {
218
253
  const port = Number(forwardedPort.trim())
@@ -223,5 +258,23 @@ function extractPort(
223
258
  }
224
259
  }
225
260
 
226
- return req.socket.remotePort
261
+ const port = req.socket.remotePort
262
+ if (port != null) return port
263
+
264
+ throw new Error('Could not determine port')
265
+ }
266
+
267
+ export function extractLocales(req: IncomingMessage) {
268
+ const acceptLanguage = req.headers['accept-language']
269
+ return acceptLanguage ? languages(acceptLanguage) : []
270
+ }
271
+
272
+ export function negotiateResponseContent<T extends string>(
273
+ req: IncomingMessage,
274
+ types: readonly T[],
275
+ ): T | undefined {
276
+ const type = mediaType(req.headers['accept'], types)
277
+ if (type) return type as T
278
+
279
+ return undefined
227
280
  }
@@ -1,6 +1,6 @@
1
1
  import type { ServerResponse } from 'node:http'
2
2
  import { type Readable, pipeline } from 'node:stream'
3
- import { Handler } from './types.js'
3
+ import type { Handler, Middleware } from './types.js'
4
4
 
5
5
  export function appendHeader(
6
6
  res: ServerResponse,
@@ -53,22 +53,27 @@ export function writeStream(
53
53
  export function writeBuffer(
54
54
  res: ServerResponse,
55
55
  chunk: string | Buffer,
56
- {
57
- status = 200,
58
- contentType = 'application/octet-stream',
59
- }: WriteResponseOptions = {},
56
+ opts: WriteResponseOptions,
60
57
  ): void {
61
- res.statusCode = status
62
- res.setHeader('content-type', contentType)
58
+ if (opts?.status != null) res.statusCode = opts.status
59
+ res.setHeader('content-type', opts?.contentType || 'application/octet-stream')
63
60
  res.end(chunk)
64
61
  }
65
62
 
63
+ export function toJsonBuffer(value: unknown): Buffer {
64
+ try {
65
+ return Buffer.from(JSON.stringify(value))
66
+ } catch (cause) {
67
+ throw new Error(`Failed to serialize as JSON`, { cause })
68
+ }
69
+ }
70
+
66
71
  export function writeJson(
67
72
  res: ServerResponse,
68
73
  payload: unknown,
69
74
  { contentType = 'application/json', ...options }: WriteResponseOptions = {},
70
75
  ): void {
71
- const buffer = Buffer.from(JSON.stringify(payload))
76
+ const buffer = toJsonBuffer(payload)
72
77
  writeBuffer(res, buffer, { ...options, contentType })
73
78
  }
74
79
 
@@ -76,7 +81,7 @@ export function staticJsonMiddleware(
76
81
  value: unknown,
77
82
  { contentType = 'application/json', ...options }: WriteResponseOptions = {},
78
83
  ): Handler<unknown> {
79
- const buffer = Buffer.from(JSON.stringify(value))
84
+ const buffer = toJsonBuffer(value)
80
85
  const staticOptions: WriteResponseOptions = { ...options, contentType }
81
86
  return function (req, res) {
82
87
  writeBuffer(res, buffer, staticOptions)
@@ -90,3 +95,11 @@ export function writeHtml(
90
95
  ): void {
91
96
  writeBuffer(res, html, { ...options, contentType })
92
97
  }
98
+
99
+ export function cacheControlMiddleware(maxAge: number): Middleware<void> {
100
+ const header = `max-age=${maxAge}`
101
+ return function (req, res, next) {
102
+ res.setHeader('Cache-Control', header)
103
+ next()
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod'
2
+
3
+ export const localeSchema = z
4
+ .string()
5
+ .regex(/^[a-z]{2,3}(-[A-Z]{2})?$/, 'Invalid locale')
6
+ export type Locale = z.infer<typeof localeSchema>
7
+
8
+ export const multiLangStringSchema = z.intersection(
9
+ z.object({ en: z.string() }), // en is required
10
+ z.record(localeSchema, z.union([z.string(), z.undefined()])),
11
+ )
12
+ export type MultiLangString = z.infer<typeof multiLangStringSchema>
13
+
14
+ export const AVAILABLE_LOCALES = [
15
+ // TODO: Add more in this list as translations are added in the PO files
16
+ 'en',
17
+ 'fr',
18
+ ] as const satisfies readonly Locale[]
19
+ export type AvailableLocale = (typeof AVAILABLE_LOCALES)[number]
20
+ export const isAvailableLocale = (v: unknown): v is AvailableLocale =>
21
+ (AVAILABLE_LOCALES as readonly unknown[]).includes(v)
@@ -6,17 +6,14 @@
6
6
  * particularly useful when the function is a member of a "private" object.
7
7
  */
8
8
  export async function callAsync<F extends (...args: any[]) => unknown>(
9
- this: ThisParameterType<F>,
10
9
  fn: F,
11
10
  ...args: Parameters<F>
12
11
  ): Promise<Awaited<ReturnType<F>>>
13
12
  export async function callAsync<F extends (...args: any[]) => unknown>(
14
- this: ThisParameterType<F>,
15
13
  fn?: F,
16
14
  ...args: Parameters<F>
17
15
  ): Promise<Awaited<ReturnType<F>> | undefined>
18
16
  export async function callAsync<F extends (...args: any[]) => unknown>(
19
- this: ThisParameterType<F>,
20
17
  fn?: F,
21
18
  ...args: Parameters<F>
22
19
  ): Promise<Awaited<ReturnType<F>> | undefined> {