@atproto/oauth-provider 0.5.1 → 0.6.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 (325) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/account/account-manager.d.ts +7 -5
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js +34 -25
  5. package/dist/account/account-manager.js.map +1 -1
  6. package/dist/account/account-store.d.ts +13 -5
  7. package/dist/account/account-store.d.ts.map +1 -1
  8. package/dist/account/account-store.js +24 -8
  9. package/dist/account/account-store.js.map +1 -1
  10. package/dist/account/account.d.ts +1 -11
  11. package/dist/account/account.d.ts.map +1 -1
  12. package/dist/account/{sign-up-data.d.ts → sign-up-input.d.ts} +5 -5
  13. package/dist/account/sign-up-input.d.ts.map +1 -0
  14. package/dist/account/{sign-up-data.js → sign-up-input.js} +3 -3
  15. package/dist/account/sign-up-input.js.map +1 -0
  16. package/dist/assets/assets-middleware.d.ts +2 -0
  17. package/dist/assets/assets-middleware.d.ts.map +1 -1
  18. package/dist/assets/assets-middleware.js +12 -14
  19. package/dist/assets/assets-middleware.js.map +1 -1
  20. package/dist/errors/invalid-invite-code-error.d.ts +5 -0
  21. package/dist/errors/invalid-invite-code-error.d.ts.map +1 -0
  22. package/dist/errors/invalid-invite-code-error.js +11 -0
  23. package/dist/errors/invalid-invite-code-error.js.map +1 -0
  24. package/dist/errors/oauth-error.d.ts +2 -2
  25. package/dist/errors/oauth-error.js.map +1 -1
  26. package/dist/lib/csp/index.d.ts +5 -6
  27. package/dist/lib/csp/index.d.ts.map +1 -1
  28. package/dist/lib/csp/index.js +14 -11
  29. package/dist/lib/csp/index.js.map +1 -1
  30. package/dist/lib/hcaptcha.d.ts +5 -3
  31. package/dist/lib/hcaptcha.d.ts.map +1 -1
  32. package/dist/lib/hcaptcha.js +7 -4
  33. package/dist/lib/hcaptcha.js.map +1 -1
  34. package/dist/lib/html/build-document.d.ts +2 -2
  35. package/dist/lib/html/build-document.d.ts.map +1 -1
  36. package/dist/lib/html/build-document.js +11 -7
  37. package/dist/lib/html/build-document.js.map +1 -1
  38. package/dist/lib/html/html.d.ts.map +1 -1
  39. package/dist/lib/html/html.js +10 -13
  40. package/dist/lib/html/html.js.map +1 -1
  41. package/dist/lib/html/util.d.ts +0 -1
  42. package/dist/lib/html/util.d.ts.map +1 -1
  43. package/dist/lib/html/util.js +0 -4
  44. package/dist/lib/html/util.js.map +1 -1
  45. package/dist/lib/http/response.d.ts +3 -1
  46. package/dist/lib/http/response.d.ts.map +1 -1
  47. package/dist/lib/http/response.js +3 -0
  48. package/dist/lib/http/response.js.map +1 -1
  49. package/dist/lib/http/security-headers.d.ts +48 -0
  50. package/dist/lib/http/security-headers.d.ts.map +1 -0
  51. package/dist/lib/http/security-headers.js +62 -0
  52. package/dist/lib/http/security-headers.js.map +1 -0
  53. package/dist/lib/util/type.d.ts +8 -0
  54. package/dist/lib/util/type.d.ts.map +1 -1
  55. package/dist/lib/util/type.js.map +1 -1
  56. package/dist/oauth-errors.d.ts +1 -0
  57. package/dist/oauth-errors.d.ts.map +1 -1
  58. package/dist/oauth-errors.js +3 -1
  59. package/dist/oauth-errors.js.map +1 -1
  60. package/dist/oauth-hooks.d.ts +4 -25
  61. package/dist/oauth-hooks.d.ts.map +1 -1
  62. package/dist/oauth-provider.d.ts.map +1 -1
  63. package/dist/oauth-provider.js +26 -25
  64. package/dist/oauth-provider.js.map +1 -1
  65. package/dist/output/backend-data.d.ts +4 -0
  66. package/dist/output/backend-data.d.ts.map +1 -0
  67. package/dist/output/backend-data.js +19 -0
  68. package/dist/output/backend-data.js.map +1 -0
  69. package/dist/output/build-authorize-data.d.ts +3 -19
  70. package/dist/output/build-authorize-data.d.ts.map +1 -1
  71. package/dist/output/build-authorize-data.js.map +1 -1
  72. package/dist/output/build-customization-data.d.ts +11 -18
  73. package/dist/output/build-customization-data.d.ts.map +1 -1
  74. package/dist/output/build-customization-data.js +1 -1
  75. package/dist/output/build-customization-data.js.map +1 -1
  76. package/dist/output/build-error-data.d.ts +3 -0
  77. package/dist/output/build-error-data.d.ts.map +1 -0
  78. package/dist/output/build-error-data.js +10 -0
  79. package/dist/output/build-error-data.js.map +1 -0
  80. package/dist/output/build-error-payload.d.ts +2 -1
  81. package/dist/output/build-error-payload.d.ts.map +1 -1
  82. package/dist/output/build-error-payload.js.map +1 -1
  83. package/dist/output/output-manager.d.ts +10 -4
  84. package/dist/output/output-manager.d.ts.map +1 -1
  85. package/dist/output/output-manager.js +68 -39
  86. package/dist/output/output-manager.js.map +1 -1
  87. package/dist/output/send-web-page.d.ts +6 -10
  88. package/dist/output/send-web-page.d.ts.map +1 -1
  89. package/dist/output/send-web-page.js +27 -47
  90. package/dist/output/send-web-page.js.map +1 -1
  91. package/dist/signer/signed-token-payload.d.ts +3 -3
  92. package/dist/signer/signer.d.ts +2 -2
  93. package/package.json +7 -39
  94. package/src/account/account-manager.ts +55 -34
  95. package/src/account/account-store.ts +29 -6
  96. package/src/account/account.ts +1 -14
  97. package/src/account/{sign-up-data.ts → sign-up-input.ts} +2 -2
  98. package/src/assets/assets-middleware.ts +11 -17
  99. package/src/errors/invalid-invite-code-error.ts +10 -0
  100. package/src/errors/oauth-error.ts +1 -1
  101. package/src/lib/csp/index.ts +16 -13
  102. package/src/lib/hcaptcha.ts +10 -7
  103. package/src/lib/html/build-document.ts +15 -8
  104. package/src/lib/html/html.ts +11 -18
  105. package/src/lib/html/util.ts +0 -4
  106. package/src/lib/http/response.ts +9 -1
  107. package/src/lib/http/security-headers.ts +91 -0
  108. package/src/lib/util/type.ts +18 -0
  109. package/src/oauth-errors.ts +1 -0
  110. package/src/oauth-hooks.ts +4 -25
  111. package/src/oauth-provider.ts +40 -34
  112. package/src/output/backend-data.ts +18 -0
  113. package/src/output/build-authorize-data.ts +3 -26
  114. package/src/output/build-customization-data.ts +2 -13
  115. package/src/output/build-error-data.ts +8 -0
  116. package/src/output/build-error-payload.ts +4 -2
  117. package/src/output/output-manager.ts +86 -47
  118. package/src/output/send-web-page.ts +29 -58
  119. package/tsconfig.backend.json +1 -2
  120. package/tsconfig.backend.tsbuildinfo +1 -1
  121. package/tsconfig.json +1 -5
  122. package/.linguirc +0 -57
  123. package/dist/account/sign-up-data.d.ts.map +0 -1
  124. package/dist/account/sign-up-data.js.map +0 -1
  125. package/dist/assets/app/bundle-manifest.json +0 -614
  126. package/dist/assets/app/index-ItwwtJ8r.js +0 -36
  127. package/dist/assets/app/index-ItwwtJ8r.js.map +0 -1
  128. package/dist/assets/app/main-B_dNxQo_.js +0 -4
  129. package/dist/assets/app/main-B_dNxQo_.js.map +0 -1
  130. package/dist/assets/app/main-CSatvmRR.css +0 -3
  131. package/dist/assets/app/main-CSatvmRR.js +0 -306
  132. package/dist/assets/app/main-CSatvmRR.js.map +0 -1
  133. package/dist/assets/app/messages-BQeltXSF.js +0 -4
  134. package/dist/assets/app/messages-BQeltXSF.js.map +0 -1
  135. package/dist/assets/app/messages-BQkEhfjg.js +0 -4
  136. package/dist/assets/app/messages-BQkEhfjg.js.map +0 -1
  137. package/dist/assets/app/messages-BUjKj_UJ.js +0 -4
  138. package/dist/assets/app/messages-BUjKj_UJ.js.map +0 -1
  139. package/dist/assets/app/messages-BWIQa8fO.js +0 -4
  140. package/dist/assets/app/messages-BWIQa8fO.js.map +0 -1
  141. package/dist/assets/app/messages-BaNVb0bp.js +0 -4
  142. package/dist/assets/app/messages-BaNVb0bp.js.map +0 -1
  143. package/dist/assets/app/messages-BaizVXcF.js +0 -4
  144. package/dist/assets/app/messages-BaizVXcF.js.map +0 -1
  145. package/dist/assets/app/messages-BfoClA1Y.js +0 -4
  146. package/dist/assets/app/messages-BfoClA1Y.js.map +0 -1
  147. package/dist/assets/app/messages-BsKGDZnC.js +0 -4
  148. package/dist/assets/app/messages-BsKGDZnC.js.map +0 -1
  149. package/dist/assets/app/messages-Bu-TJhml.js +0 -4
  150. package/dist/assets/app/messages-Bu-TJhml.js.map +0 -1
  151. package/dist/assets/app/messages-BvOKnBQk.js +0 -4
  152. package/dist/assets/app/messages-BvOKnBQk.js.map +0 -1
  153. package/dist/assets/app/messages-BxDzCiWz.js +0 -4
  154. package/dist/assets/app/messages-BxDzCiWz.js.map +0 -1
  155. package/dist/assets/app/messages-CDgFOy4S.js +0 -4
  156. package/dist/assets/app/messages-CDgFOy4S.js.map +0 -1
  157. package/dist/assets/app/messages-CLbTz0o9.js +0 -4
  158. package/dist/assets/app/messages-CLbTz0o9.js.map +0 -1
  159. package/dist/assets/app/messages-CNwSh0t7.js +0 -4
  160. package/dist/assets/app/messages-CNwSh0t7.js.map +0 -1
  161. package/dist/assets/app/messages-CSMNJ6P8.js +0 -4
  162. package/dist/assets/app/messages-CSMNJ6P8.js.map +0 -1
  163. package/dist/assets/app/messages-CZQUw3mp.js +0 -4
  164. package/dist/assets/app/messages-CZQUw3mp.js.map +0 -1
  165. package/dist/assets/app/messages-CZT41oVp.js +0 -4
  166. package/dist/assets/app/messages-CZT41oVp.js.map +0 -1
  167. package/dist/assets/app/messages-C_b-d3t8.js +0 -4
  168. package/dist/assets/app/messages-C_b-d3t8.js.map +0 -1
  169. package/dist/assets/app/messages-C_u3MTc2.js +0 -4
  170. package/dist/assets/app/messages-C_u3MTc2.js.map +0 -1
  171. package/dist/assets/app/messages-Cn8nHZic.js +0 -4
  172. package/dist/assets/app/messages-Cn8nHZic.js.map +0 -1
  173. package/dist/assets/app/messages-CtDywJUm.js +0 -4
  174. package/dist/assets/app/messages-CtDywJUm.js.map +0 -1
  175. package/dist/assets/app/messages-CurtIjBF.js +0 -4
  176. package/dist/assets/app/messages-CurtIjBF.js.map +0 -1
  177. package/dist/assets/app/messages-Cv6zIbaP.js +0 -4
  178. package/dist/assets/app/messages-Cv6zIbaP.js.map +0 -1
  179. package/dist/assets/app/messages-D1eLQuPE.js +0 -4
  180. package/dist/assets/app/messages-D1eLQuPE.js.map +0 -1
  181. package/dist/assets/app/messages-D8vHEaYW.js +0 -4
  182. package/dist/assets/app/messages-D8vHEaYW.js.map +0 -1
  183. package/dist/assets/app/messages-DJ1Q4GeC.js +0 -4
  184. package/dist/assets/app/messages-DJ1Q4GeC.js.map +0 -1
  185. package/dist/assets/app/messages-DRL3exqd.js +0 -4
  186. package/dist/assets/app/messages-DRL3exqd.js.map +0 -1
  187. package/dist/assets/app/messages-DWLPQRTp.js +0 -4
  188. package/dist/assets/app/messages-DWLPQRTp.js.map +0 -1
  189. package/dist/assets/app/messages-DjVaE9YE.js +0 -4
  190. package/dist/assets/app/messages-DjVaE9YE.js.map +0 -1
  191. package/dist/assets/app/messages-DqpMfFJR.js +0 -4
  192. package/dist/assets/app/messages-DqpMfFJR.js.map +0 -1
  193. package/dist/assets/app/messages-ETjhJBEN.js +0 -4
  194. package/dist/assets/app/messages-ETjhJBEN.js.map +0 -1
  195. package/dist/assets/app/messages-EUKrgrGn.js +0 -4
  196. package/dist/assets/app/messages-EUKrgrGn.js.map +0 -1
  197. package/dist/assets/app/messages-QQrOUcPW.js +0 -4
  198. package/dist/assets/app/messages-QQrOUcPW.js.map +0 -1
  199. package/dist/assets/app/messages-e2QGqFL6.js +0 -4
  200. package/dist/assets/app/messages-e2QGqFL6.js.map +0 -1
  201. package/dist/assets/app/messages-p61py7gD.js +0 -4
  202. package/dist/assets/app/messages-p61py7gD.js.map +0 -1
  203. package/dist/assets/asset.d.ts +0 -9
  204. package/dist/assets/asset.d.ts.map +0 -1
  205. package/dist/assets/asset.js +0 -3
  206. package/dist/assets/asset.js.map +0 -1
  207. package/dist/assets/index.d.ts +0 -5
  208. package/dist/assets/index.d.ts.map +0 -1
  209. package/dist/assets/index.js +0 -78
  210. package/dist/assets/index.js.map +0 -1
  211. package/rollup.config.js +0 -98
  212. package/src/assets/app/app.tsx +0 -43
  213. package/src/assets/app/backend-data.ts +0 -27
  214. package/src/assets/app/backend-types.ts +0 -66
  215. package/src/assets/app/components/forms/button-toggle-visibility.tsx +0 -43
  216. package/src/assets/app/components/forms/button.tsx +0 -60
  217. package/src/assets/app/components/forms/fieldset.tsx +0 -55
  218. package/src/assets/app/components/forms/form-card-async.tsx +0 -103
  219. package/src/assets/app/components/forms/form-card.tsx +0 -49
  220. package/src/assets/app/components/forms/input-checkbox.tsx +0 -73
  221. package/src/assets/app/components/forms/input-container.tsx +0 -107
  222. package/src/assets/app/components/forms/input-email-address.tsx +0 -66
  223. package/src/assets/app/components/forms/input-new-password.tsx +0 -62
  224. package/src/assets/app/components/forms/input-password.tsx +0 -88
  225. package/src/assets/app/components/forms/input-text.tsx +0 -76
  226. package/src/assets/app/components/forms/input-token.tsx +0 -94
  227. package/src/assets/app/components/forms/wizard-card.tsx +0 -116
  228. package/src/assets/app/components/layouts/layout-title-page.tsx +0 -77
  229. package/src/assets/app/components/layouts/layout-welcome.tsx +0 -73
  230. package/src/assets/app/components/utils/account-identifier.tsx +0 -23
  231. package/src/assets/app/components/utils/account-image.tsx +0 -33
  232. package/src/assets/app/components/utils/admonition.tsx +0 -52
  233. package/src/assets/app/components/utils/client-name.tsx +0 -45
  234. package/src/assets/app/components/utils/error-card.tsx +0 -93
  235. package/src/assets/app/components/utils/error-message.tsx +0 -62
  236. package/src/assets/app/components/utils/help-card.tsx +0 -46
  237. package/src/assets/app/components/utils/icons.tsx +0 -88
  238. package/src/assets/app/components/utils/link-anchor.tsx +0 -28
  239. package/src/assets/app/components/utils/link-title.tsx +0 -26
  240. package/src/assets/app/components/utils/multi-lang-string.tsx +0 -56
  241. package/src/assets/app/components/utils/password-strength-label.tsx +0 -37
  242. package/src/assets/app/components/utils/password-strength-meter.tsx +0 -58
  243. package/src/assets/app/components/utils/url-viewer.tsx +0 -73
  244. package/src/assets/app/cookies.ts +0 -11
  245. package/src/assets/app/hooks/use-api.ts +0 -178
  246. package/src/assets/app/hooks/use-async-action.ts +0 -120
  247. package/src/assets/app/hooks/use-bound-dispatch.ts +0 -5
  248. package/src/assets/app/hooks/use-browser-color-scheme.ts +0 -31
  249. package/src/assets/app/hooks/use-csrf-token.ts +0 -5
  250. package/src/assets/app/hooks/use-random-string.ts +0 -37
  251. package/src/assets/app/hooks/use-stepper.ts +0 -87
  252. package/src/assets/app/index.html +0 -182
  253. package/src/assets/app/lib/api.ts +0 -267
  254. package/src/assets/app/lib/clsx.ts +0 -6
  255. package/src/assets/app/lib/json-client.ts +0 -94
  256. package/src/assets/app/lib/password.ts +0 -98
  257. package/src/assets/app/lib/ref.ts +0 -17
  258. package/src/assets/app/lib/util.ts +0 -13
  259. package/src/assets/app/locales/an/messages.po +0 -492
  260. package/src/assets/app/locales/ast/messages.po +0 -492
  261. package/src/assets/app/locales/ca/messages.po +0 -492
  262. package/src/assets/app/locales/da/messages.po +0 -492
  263. package/src/assets/app/locales/de/messages.po +0 -492
  264. package/src/assets/app/locales/el/messages.po +0 -492
  265. package/src/assets/app/locales/en/messages.po +0 -492
  266. package/src/assets/app/locales/en-GB/messages.po +0 -492
  267. package/src/assets/app/locales/es/messages.po +0 -492
  268. package/src/assets/app/locales/eu/messages.po +0 -492
  269. package/src/assets/app/locales/fi/messages.po +0 -492
  270. package/src/assets/app/locales/fr/messages.po +0 -492
  271. package/src/assets/app/locales/ga/messages.po +0 -492
  272. package/src/assets/app/locales/gl/messages.po +0 -492
  273. package/src/assets/app/locales/hi/messages.po +0 -492
  274. package/src/assets/app/locales/hu/messages.po +0 -492
  275. package/src/assets/app/locales/ia/messages.po +0 -492
  276. package/src/assets/app/locales/id/messages.po +0 -492
  277. package/src/assets/app/locales/it/messages.po +0 -492
  278. package/src/assets/app/locales/ja/messages.po +0 -492
  279. package/src/assets/app/locales/km/messages.po +0 -492
  280. package/src/assets/app/locales/ko/messages.po +0 -492
  281. package/src/assets/app/locales/load.ts +0 -8
  282. package/src/assets/app/locales/locale-context.ts +0 -19
  283. package/src/assets/app/locales/locale-provider.tsx +0 -112
  284. package/src/assets/app/locales/locale-selector.tsx +0 -58
  285. package/src/assets/app/locales/locales.ts +0 -168
  286. package/src/assets/app/locales/ne/messages.po +0 -492
  287. package/src/assets/app/locales/nl/messages.po +0 -492
  288. package/src/assets/app/locales/pl/messages.po +0 -492
  289. package/src/assets/app/locales/pt-BR/messages.po +0 -492
  290. package/src/assets/app/locales/ro/messages.po +0 -492
  291. package/src/assets/app/locales/ru/messages.po +0 -492
  292. package/src/assets/app/locales/sv/messages.po +0 -492
  293. package/src/assets/app/locales/th/messages.po +0 -492
  294. package/src/assets/app/locales/tr/messages.po +0 -492
  295. package/src/assets/app/locales/uk/messages.po +0 -492
  296. package/src/assets/app/locales/vi/messages.po +0 -492
  297. package/src/assets/app/locales/zh-CN/messages.po +0 -492
  298. package/src/assets/app/locales/zh-HK/messages.po +0 -492
  299. package/src/assets/app/locales/zh-TW/messages.po +0 -492
  300. package/src/assets/app/main.css +0 -33
  301. package/src/assets/app/main.tsx +0 -44
  302. package/src/assets/app/views/authorize/accept/accept-form.tsx +0 -150
  303. package/src/assets/app/views/authorize/accept/accept-view.tsx +0 -70
  304. package/src/assets/app/views/authorize/authorize-view.tsx +0 -180
  305. package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +0 -88
  306. package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +0 -80
  307. package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +0 -127
  308. package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +0 -244
  309. package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +0 -116
  310. package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +0 -145
  311. package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +0 -140
  312. package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +0 -51
  313. package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +0 -289
  314. package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +0 -108
  315. package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +0 -158
  316. package/src/assets/app/views/authorize/welcome/welcome-view.tsx +0 -56
  317. package/src/assets/app/views/error/error-view.tsx +0 -31
  318. package/src/assets/asset.ts +0 -9
  319. package/src/assets/index.ts +0 -86
  320. package/tailwind.config.js +0 -31
  321. package/tsconfig.frontend.json +0 -11
  322. package/tsconfig.frontend.tsbuildinfo +0 -1
  323. package/tsconfig.tools.json +0 -8
  324. package/tsconfig.tools.tsbuildinfo +0 -1
  325. package/vite.config.mjs +0 -16
@@ -18,9 +18,10 @@ import {
18
18
  AccountStore,
19
19
  ResetPasswordConfirmData,
20
20
  ResetPasswordRequestData,
21
+ SignUpData,
21
22
  } from './account-store.js'
22
23
  import { SignInData } from './sign-in-data.js'
23
- import { SignUpData } from './sign-up-data.js'
24
+ import { SignUpInput } from './sign-up-input.js'
24
25
 
25
26
  const TIMING_ATTACK_MITIGATION_DELAY = 400
26
27
  const BRUTE_FORCE_MITIGATION_DELAY = 300
@@ -41,59 +42,79 @@ export class AccountManager {
41
42
  : undefined
42
43
  }
43
44
 
44
- protected async verifySignupData(
45
- data: SignUpData,
45
+ protected async processHcaptchaToken(
46
+ input: SignUpInput,
46
47
  deviceId: DeviceId,
47
48
  deviceMetadata: RequestMetadata,
48
- ): Promise<void> {
49
- let hcaptchaResult: undefined | HcaptchaVerifyResult
50
-
51
- if (this.inviteCodeRequired && !data.inviteCode) {
52
- throw new InvalidRequestError('Invite code is required')
49
+ ): Promise<HcaptchaVerifyResult | undefined> {
50
+ if (!this.hcaptchaClient) {
51
+ return undefined
53
52
  }
54
53
 
55
- if (this.hcaptchaClient) {
56
- if (!data.hcaptchaToken) {
57
- throw new InvalidRequestError('hCaptcha token is required')
58
- }
54
+ if (!input.hcaptchaToken) {
55
+ throw new InvalidRequestError('hCaptcha token is required')
56
+ }
59
57
 
60
- const { allowed, result } = await this.hcaptchaClient.verify(
58
+ const { allowed, result } = await this.hcaptchaClient
59
+ .verify(
61
60
  'signup',
62
- data.hcaptchaToken,
61
+ input.hcaptchaToken,
63
62
  deviceMetadata.ipAddress,
64
- data.handle,
63
+ input.handle,
65
64
  deviceMetadata.userAgent,
66
65
  )
67
-
68
- await callAsync(this.hooks.onSignupHcaptchaResult, {
69
- data,
70
- allowed,
71
- result,
72
- deviceId,
73
- deviceMetadata,
66
+ .catch((err) => {
67
+ throw InvalidRequestError.from(err, 'hCaptcha verification failed')
74
68
  })
75
69
 
76
- if (!allowed) {
77
- throw new InvalidRequestError('hCaptcha verification failed')
78
- }
70
+ if (!allowed) {
71
+ throw new InvalidRequestError('hCaptcha verification failed')
72
+ }
73
+
74
+ return result
75
+ }
79
76
 
80
- hcaptchaResult = result
77
+ protected async enforceInviteCode(
78
+ input: SignUpInput,
79
+ _deviceId: DeviceId,
80
+ _deviceMetadata: RequestMetadata,
81
+ ): Promise<string | undefined> {
82
+ if (!this.inviteCodeRequired) {
83
+ return undefined
81
84
  }
82
85
 
83
- await callAsync(this.hooks.onSignupAttempt, {
84
- data,
85
- deviceId,
86
- deviceMetadata,
87
- hcaptchaResult,
88
- })
86
+ if (!input.inviteCode) {
87
+ throw new InvalidRequestError('Invite code is required')
88
+ }
89
+
90
+ return input.inviteCode
91
+ }
92
+
93
+ protected async buildSignupData(
94
+ input: SignUpInput,
95
+ deviceId: DeviceId,
96
+ deviceMetadata: RequestMetadata,
97
+ ): Promise<SignUpData> {
98
+ const [hcaptchaResult, inviteCode] = await Promise.all([
99
+ this.processHcaptchaToken(input, deviceId, deviceMetadata),
100
+ this.enforceInviteCode(input, deviceId, deviceMetadata),
101
+ ])
102
+
103
+ return { ...input, hcaptchaResult, inviteCode }
89
104
  }
90
105
 
91
106
  public async signUp(
92
- data: SignUpData,
107
+ input: SignUpInput,
93
108
  deviceId: DeviceId,
94
109
  deviceMetadata: RequestMetadata,
95
110
  ): Promise<AccountInfo> {
96
- await this.verifySignupData(data, deviceId, deviceMetadata)
111
+ await callAsync(this.hooks.onSignupAttempt, {
112
+ input,
113
+ deviceId,
114
+ deviceMetadata,
115
+ })
116
+
117
+ const data = await this.buildSignupData(input, deviceId, deviceMetadata)
97
118
 
98
119
  // Mitigation against brute forcing email of users.
99
120
  // @TODO Add rate limit to all the OAuth routes.
@@ -1,8 +1,10 @@
1
1
  import { isEmailValid } from '@hapi/address'
2
2
  import { isDisposableEmail } from 'disposable-email-domains-js'
3
3
  import { z } from 'zod'
4
+ import { ensureValidHandle, normalizeHandle } from '@atproto/syntax'
4
5
  import { ClientId } from '../client/client-id.js'
5
6
  import { DeviceId } from '../device/device-id.js'
7
+ import { HcaptchaVerifyResult } from '../lib/hcaptcha.js'
6
8
  import { localeSchema } from '../lib/locale.js'
7
9
  import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
8
10
  import {
@@ -12,16 +14,29 @@ import {
12
14
  } from '../oauth-errors.js'
13
15
  import { Sub } from '../oidc/sub.js'
14
16
  import { Account } from './account.js'
17
+ import { SignUpInput } from './sign-up-input.js'
15
18
 
16
19
  // @NOTE Change the length here to force stronger passwords (through a reset)
17
20
  export const oldPasswordSchema = z.string().min(1)
18
21
  export const newPasswordSchema = z.string().min(8)
19
- export const tokenSchema = z.string().regex(/^[A-Z2-7]{5}-[A-Z2-7]{5}$/)
22
+ export const tokenSchema = z
23
+ .string()
24
+ .regex(/^[A-Z2-7]{5}-[A-Z2-7]{5}$/, 'Invalid token format')
20
25
  export const handleSchema = z
21
26
  .string()
22
- .min(3)
23
- .max(30)
24
- .regex(/^[a-z0-9][a-z0-9-]+[a-z0-9](?:\.[a-z0-9][a-z0-9-]+[a-z0-9])+$/)
27
+ // @NOTE: We only check against validity towards ATProto's syntax. Additional
28
+ // rules may be imposed by the store implementation.
29
+ .superRefine((value, ctx) => {
30
+ try {
31
+ ensureValidHandle(value)
32
+ } catch (err) {
33
+ ctx.addIssue({
34
+ code: z.ZodIssueCode.custom,
35
+ message: err instanceof Error ? err.message : 'Invalid handle',
36
+ })
37
+ }
38
+ })
39
+ .transform(normalizeHandle)
25
40
  export const emailSchema = z
26
41
  .string()
27
42
  .email()
@@ -34,13 +49,16 @@ export const emailSchema = z
34
49
  .refine((email) => !isDisposableEmail(email), {
35
50
  message: 'Disposable email addresses are not allowed',
36
51
  })
52
+ .transform((value) => value.toLowerCase())
53
+ export const inviteCodeSchema = z.string().min(1)
54
+ export type InviteCode = z.infer<typeof inviteCodeSchema>
37
55
 
38
56
  export const authenticateAccountDataSchema = z
39
57
  .object({
40
58
  locale: localeSchema,
41
59
  username: z.string(),
42
60
  password: oldPasswordSchema,
43
- emailOtp: z.string().optional(),
61
+ emailOtp: tokenSchema.optional(),
44
62
  })
45
63
  .strict()
46
64
 
@@ -54,7 +72,7 @@ export const createAccountDataSchema = z
54
72
  handle: handleSchema,
55
73
  email: emailSchema,
56
74
  password: z.intersection(oldPasswordSchema, newPasswordSchema),
57
- inviteCode: tokenSchema.optional(),
75
+ inviteCode: inviteCodeSchema.optional(),
58
76
  })
59
77
  .strict()
60
78
 
@@ -103,6 +121,11 @@ export type AccountInfo = {
103
121
  info: DeviceAccountInfo
104
122
  }
105
123
 
124
+ export type SignUpData = SignUpInput & {
125
+ hcaptchaResult?: HcaptchaVerifyResult
126
+ inviteCode?: InviteCode
127
+ }
128
+
106
129
  export interface AccountStore {
107
130
  /**
108
131
  * @throws {HandleUnavailableError} - To indicate that the handle is already taken
@@ -1,14 +1 @@
1
- import { Simplify } from '../lib/util/type.js'
2
- import { Sub } from '../oidc/sub.js'
3
-
4
- export type Account = Simplify<{
5
- sub: Sub // Account id
6
- aud: string | [string, ...string[]] // Resource server URL
7
-
8
- // OIDC inspired
9
- preferred_username?: string
10
- email?: string
11
- email_verified?: boolean
12
- picture?: string
13
- name?: string
14
- }>
1
+ export type { Account } from '@atproto/oauth-provider-api'
@@ -2,10 +2,10 @@ import { z } from 'zod'
2
2
  import { hcaptchaTokenSchema } from '../lib/hcaptcha.js'
3
3
  import { createAccountDataSchema } from './account-store.js'
4
4
 
5
- export const signUpDataSchema = createAccountDataSchema
5
+ export const signUpInputSchema = createAccountDataSchema
6
6
  .extend({
7
7
  hcaptchaToken: hcaptchaTokenSchema.optional(),
8
8
  })
9
9
  .strict()
10
10
 
11
- export type SignUpData = z.TypeOf<typeof signUpDataSchema>
11
+ export type SignUpInput = z.TypeOf<typeof signUpInputSchema>
@@ -1,33 +1,27 @@
1
+ import { assets } from '@atproto/oauth-provider-ui'
1
2
  import {
2
3
  Middleware,
3
4
  validateFetchDest,
4
5
  validateFetchSite,
5
6
  writeStream,
6
7
  } from '../lib/http/index.js'
7
- import { Asset } from './asset.js'
8
- import { ASSETS_URL_PREFIX, getAsset } from './index.js'
8
+
9
+ export const ASSETS_URL_PREFIX = '/@atproto/oauth-provider/~assets/'
10
+
11
+ export function buildAssetUrl(filename: string): string {
12
+ return `${ASSETS_URL_PREFIX}${encodeURIComponent(filename)}`
13
+ }
9
14
 
10
15
  export function authorizeAssetsMiddleware(): Middleware {
11
16
  return async function assetsMiddleware(req, res, next): Promise<void> {
12
17
  if (req.method !== 'GET' && req.method !== 'HEAD') return next()
13
18
  if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
14
19
 
15
- const [pathname, query] = req.url.split('?', 2) as [
16
- string,
17
- string | undefined,
18
- ]
19
- if (query) return next()
20
-
21
- const filename = pathname.slice(ASSETS_URL_PREFIX.length)
20
+ const filename = req.url.slice(ASSETS_URL_PREFIX.length)
22
21
  if (!filename) return next()
23
22
 
24
- let asset: Asset
25
- try {
26
- asset = getAsset(filename)
27
- } catch {
28
- // Filename not found or not valid
29
- return next()
30
- }
23
+ const asset = assets.get(filename)
24
+ if (!asset) return next()
31
25
 
32
26
  try {
33
27
  // Allow "null" (ie. no header) to allow loading assets outside of a
@@ -45,6 +39,6 @@ export function authorizeAssetsMiddleware(): Middleware {
45
39
  res.setHeader('ETag', asset.sha256)
46
40
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
47
41
 
48
- writeStream(res, asset.createStream(), { contentType: asset.type })
42
+ writeStream(res, asset.stream(), { contentType: asset.mime })
49
43
  }
50
44
  }
@@ -0,0 +1,10 @@
1
+ import { InvalidRequestError } from './invalid-request-error'
2
+
3
+ export class InvalidInviteCodeError extends InvalidRequestError {
4
+ constructor(details?: string, cause?: unknown) {
5
+ super(
6
+ 'This invite code is invalid.' + (details ? ` ${details}` : ''),
7
+ cause,
8
+ )
9
+ }
10
+ }
@@ -23,6 +23,6 @@ export class OAuthError extends Error {
23
23
  return {
24
24
  error: this.error,
25
25
  error_description: this.error_description,
26
- } as const
26
+ }
27
27
  }
28
28
  }
@@ -1,7 +1,8 @@
1
- import { Simplify } from '../util/type.js'
1
+ import { CombinedTuple, Simplify } from '../util/type.js'
2
2
 
3
3
  export type CspValue =
4
4
  | `data:`
5
+ | `http:${string}`
5
6
  | `https:${string}`
6
7
  | `'none'`
7
8
  | `'self'`
@@ -35,7 +36,7 @@ export type CspConfig = Simplify<
35
36
  } & {
36
37
  [K in (typeof STRING_DIRECTIVES)[number]]?: CspValue
37
38
  } & {
38
- [K in (typeof ARRAY_DIRECTIVES)[number]]?: readonly CspValue[]
39
+ [K in (typeof ARRAY_DIRECTIVES)[number]]?: Iterable<CspValue>
39
40
  }
40
41
  >
41
42
 
@@ -53,25 +54,27 @@ export function buildCsp(config: CspConfig): string {
53
54
  }
54
55
 
55
56
  for (const name of ARRAY_DIRECTIVES) {
56
- if (config[name]?.length) values.push(`${name} ${config[name].join(' ')}`)
57
+ // Remove duplicate values by using a Set
58
+ const val = config[name] ? new Set(config[name]) : undefined
59
+ if (val?.size) values.push(`${name} ${Array.from(val).join(' ')}`)
57
60
  }
58
61
 
59
62
  return values.join('; ')
60
63
  }
61
64
 
62
- export function mergeCsp(a: CspConfig, b?: CspConfig): CspConfig
63
- export function mergeCsp(a: CspConfig | undefined, b: CspConfig): CspConfig
64
- export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined
65
- export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined {
66
- if (!a) return b
67
- if (!b) return a
65
+ export function mergeCsp<C extends (CspConfig | null | undefined)[]>(
66
+ ...configs: C
67
+ ) {
68
+ return configs.filter((v) => v != null).reduce(combineCsp) as CombinedTuple<C>
69
+ }
68
70
 
71
+ export function combineCsp(a: CspConfig, b: CspConfig): CspConfig {
69
72
  const result: CspConfig = {}
70
73
 
71
74
  for (const name of BOOLEAN_DIRECTIVES) {
72
- if (a[name] || b[name]) {
73
- result[name] = true
74
- }
75
+ // @NOTE b (if defined) takes precedence
76
+ const value = b[name] ?? a[name]
77
+ if (value != null) result[name] = value
75
78
  }
76
79
 
77
80
  for (const name of STRING_DIRECTIVES) {
@@ -90,7 +93,7 @@ export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined {
90
93
  if (set.size > 1 && set.has(NONE)) set.delete(NONE)
91
94
  result[name] = [...set]
92
95
  } else if (a[name] || b[name]) {
93
- result[name] = Array.from((a[name] || b[name])!)
96
+ result[name] = a[name] || b[name]
94
97
  }
95
98
  }
96
99
 
@@ -27,9 +27,11 @@ export const hcaptchaConfigSchema = z.object({
27
27
  */
28
28
  tokenSalt: z.string().min(1),
29
29
  /**
30
- * The risk score over which the user is considered a threat and will be
30
+ * The risk score above which the user is considered a threat and will be
31
31
  * denied access. This will be ignored if the enterprise features are not
32
32
  * available.
33
+ *
34
+ * Note: Score values ranges from 0.0 (no risk) to 1.0 (confirmed threat).
33
35
  */
34
36
  scoreThreshold: z.number().optional(),
35
37
  })
@@ -128,7 +130,7 @@ export class HCaptchaClient {
128
130
  this.fetch = bindFetch(fetch)
129
131
  }
130
132
 
131
- async verify(
133
+ public async verify(
132
134
  behaviorType: 'login' | 'signup',
133
135
  response: string,
134
136
  remoteip: string,
@@ -160,20 +162,21 @@ export class HCaptchaClient {
160
162
  }
161
163
  }
162
164
 
163
- isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
165
+ protected isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
164
166
  return (
165
167
  success &&
166
168
  // Fool-proofing: If this is false, the user is trying to use a token
167
169
  // generated for the same siteKey, but on another domain.
168
170
  hostname === this.hostname &&
169
171
  // Ignore if enterprise feature is not enabled
170
- score != null &&
171
- this.config.scoreThreshold != null &&
172
- score < this.config.scoreThreshold
172
+ (score == null ||
173
+ // Ignore if disabled through config
174
+ this.config.scoreThreshold == null ||
175
+ score < this.config.scoreThreshold)
173
176
  )
174
177
  }
175
178
 
176
- hashToken(value: string) {
179
+ protected hashToken(value: string) {
177
180
  const hash = createHash('sha256')
178
181
  hash.update(this.config.tokenSalt)
179
182
  hash.update(value)
@@ -4,7 +4,6 @@ import { html } from './tags.js'
4
4
 
5
5
  export type AssetRef = {
6
6
  url: string
7
- sha256: string
8
7
  }
9
8
 
10
9
  export type Attrs = Record<string, boolean | string | undefined>
@@ -59,6 +58,7 @@ export type BuildDocumentOptions = {
59
58
  base?: URL
60
59
  meta?: readonly MetaAttrs[]
61
60
  links?: readonly LinkAttrs[]
61
+ preloads?: readonly AssetRef[]
62
62
  head?: HtmlValue
63
63
  title?: HtmlValue
64
64
  scripts?: readonly (Html | AssetRef)[]
@@ -76,6 +76,7 @@ export const buildDocument = ({
76
76
  base,
77
77
  meta,
78
78
  links,
79
+ preloads,
79
80
  scripts,
80
81
  styles,
81
82
  }: BuildDocumentOptions) => html`<!doctype html>
@@ -86,8 +87,7 @@ export const buildDocument = ({
86
87
  ${base && html`<base href="${base.href}" />`}
87
88
  ${meta?.some(isViewportMeta) ? null : defaultViewport}
88
89
  ${meta?.map(metaToHtml)}
89
- ${styles?.map(linkPreload('style'))}
90
- ${scripts?.map(linkPreload('script'))}
90
+ ${preloads?.map(linkPreload)}
91
91
  ${links?.map(linkToHtml)}
92
92
  ${head}
93
93
  ${styles?.map(styleToHtml)}
@@ -120,11 +120,18 @@ function* attrsToHtml(attrs?: Attrs) {
120
120
  }
121
121
  }
122
122
 
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}" />`
123
+ function linkPreload(asset: AssetRef) {
124
+ const [path] = asset.url.split('?', 2)
125
+
126
+ if (path.endsWith('.js')) {
127
+ return html`<link rel="modulepreload" href="${asset.url}" />`
128
+ }
129
+
130
+ if (path.endsWith('.css')) {
131
+ return html`<link rel="preload" href="${asset.url}" as="style" />`
132
+ }
133
+
134
+ return undefined
128
135
  }
129
136
 
130
137
  function scriptToHtml(script: Html | AssetRef) {
@@ -1,5 +1,3 @@
1
- import { isString } from './util'
2
-
3
1
  const symbol = Symbol('Html.dangerouslyCreate')
4
2
 
5
3
  /**
@@ -7,34 +5,29 @@ const symbol = Symbol('Html.dangerouslyCreate')
7
5
  * or used as fragments to build a larger HTML document.
8
6
  */
9
7
  export class Html implements Iterable<string> {
10
- #fragments: Iterable<Html | string>
8
+ readonly #fragments: readonly (Html | string)[]
11
9
 
12
10
  private constructor(fragments: Iterable<Html | string>, guard: symbol) {
13
11
  if (guard !== symbol) {
14
- // Force developers to use `Html.dangerouslyCreate` to create an Html
12
+ // Forces developers to use `Html.dangerouslyCreate` to create an Html
15
13
  // instance, to make it clear that the content needs to be trusted.
16
14
  throw new TypeError(
17
15
  'Use Html.dangerouslyCreate() to create an Html instance',
18
16
  )
19
17
  }
20
18
 
21
- this.#fragments = fragments
19
+ // Transform into an array in case iterable can be consumed only once
20
+ // (e.g. a generator function).
21
+ this.#fragments = Array.from(fragments)
22
22
  }
23
23
 
24
24
  toString(): string {
25
- let result = ''
26
- for (const fragment of this) result += fragment
27
-
28
- // Cache result for future calls
29
- if (
30
- !Array.isArray(this.#fragments) ||
31
- this.#fragments.length > 1 ||
32
- !this.#fragments.every(isString)
33
- ) {
34
- this.#fragments = result ? [result] : []
35
- }
36
-
37
- return result
25
+ // More efficient than `return this.#fragments.join('')` because it avoids
26
+ // creating intermediate strings when items of this.#fragments are Html
27
+ // instances (as all their toString() would end-up being called, creating
28
+ // lots of intermediary strings). The approach here allows to do a full scan
29
+ // of all the child nodes and concatenate them in a single pass.
30
+ return Array.from(this).join('')
38
31
  }
39
32
 
40
33
  [Symbol.toPrimitive](hint): string {
@@ -15,7 +15,3 @@ export function* stringReplacer(
15
15
  }
16
16
  yield source.slice(previousIndex)
17
17
  }
18
-
19
- export function isString(value: unknown): value is string {
20
- return typeof value === 'string'
21
- }
@@ -1,5 +1,9 @@
1
1
  import type { ServerResponse } from 'node:http'
2
2
  import { type Readable, pipeline } from 'node:stream'
3
+ import {
4
+ SecurityHeadersOptions,
5
+ setSecurityHeaders,
6
+ } from './security-headers.js'
3
7
  import type { Handler, Middleware } from './types.js'
4
8
 
5
9
  export function appendHeader(
@@ -88,11 +92,15 @@ export function staticJsonMiddleware(
88
92
  }
89
93
  }
90
94
 
95
+ export type WriteHtmlOptions = WriteResponseOptions & SecurityHeadersOptions
96
+
91
97
  export function writeHtml(
92
98
  res: ServerResponse,
93
99
  html: Buffer | string,
94
- { contentType = 'text/html', ...options }: WriteResponseOptions = {},
100
+ { contentType = 'text/html', ...options }: WriteHtmlOptions = {},
95
101
  ): void {
102
+ // HTML pages should always be served with safety protection headers
103
+ setSecurityHeaders(res, options)
96
104
  writeBuffer(res, html, { ...options, contentType })
97
105
  }
98
106
 
@@ -0,0 +1,91 @@
1
+ import type { ServerResponse } from 'node:http'
2
+ import { type CspConfig, buildCsp } from '../csp/index.js'
3
+
4
+ /**
5
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy COEP on MDN}
6
+ */
7
+ export enum CrossOriginEmbedderPolicy {
8
+ unsafeNone = 'unsafe-none',
9
+ requireCorp = 'require-corp',
10
+ credentialless = 'credentialless',
11
+ }
12
+
13
+ /**
14
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy CORP on MDN}
15
+ */
16
+ export enum CrossOriginResourcePolicy {
17
+ sameSite = 'same-site',
18
+ sameOrigin = 'same-origin',
19
+ crossOrigin = 'cross-origin',
20
+ }
21
+
22
+ /**
23
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy COOP on MDN}
24
+ */
25
+ export enum CrossOriginOpenerPolicy {
26
+ unsafeNone = 'unsafe-none',
27
+ sameOriginAllowPopups = 'same-origin-allow-popups',
28
+ sameOrigin = 'same-origin',
29
+ noopenerAllowPopups = 'noopener-allow-popups',
30
+ }
31
+
32
+ export type HTTPStrictTransportSecurityConfig = {
33
+ maxAge: number
34
+ includeSubDomains?: boolean
35
+ preload?: boolean
36
+ }
37
+
38
+ export type SecurityHeadersOptions = {
39
+ /**
40
+ * Defaults to `default-src: 'none'`. Use an empty object to disable CSP.
41
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy CSP on MDN}
42
+ */
43
+ csp?: CspConfig
44
+ coep?: CrossOriginEmbedderPolicy
45
+ corp?: CrossOriginResourcePolicy
46
+ coop?: CrossOriginOpenerPolicy
47
+ /**
48
+ * Defaults to 2 years. Use `false` to disable HSTS.
49
+ */
50
+ hsts?: HTTPStrictTransportSecurityConfig | false
51
+ }
52
+
53
+ export function setSecurityHeaders(
54
+ res: ServerResponse,
55
+ {
56
+ csp = { 'default-src': ["'none'"] },
57
+ coep = CrossOriginEmbedderPolicy.requireCorp,
58
+ corp = CrossOriginResourcePolicy.sameOrigin,
59
+ coop = CrossOriginOpenerPolicy.sameOrigin,
60
+ hsts = { maxAge: 63072000 },
61
+ }: SecurityHeadersOptions,
62
+ ): void {
63
+ // @NOTE Never set CSP through http-equiv meta as not all directives will
64
+ // be honored. Always set it through the Content-Security-Policy header.
65
+ const cspString = buildCsp(csp)
66
+ if (cspString) {
67
+ res.setHeader('Content-Security-Policy', cspString)
68
+ }
69
+
70
+ res.setHeader('Cross-Origin-Embedder-Policy', coep)
71
+ res.setHeader('Cross-Origin-Resource-Policy', corp)
72
+ res.setHeader('Cross-Origin-Opener-Policy', coop)
73
+
74
+ if (hsts) {
75
+ res.setHeader('Strict-Transport-Security', buildHstsValue(hsts))
76
+ }
77
+
78
+ // @TODO: make these headers configurable (?)
79
+ res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
80
+ res.setHeader('Referrer-Policy', 'same-origin')
81
+ res.setHeader('X-Frame-Options', 'DENY')
82
+ res.setHeader('X-Content-Type-Options', 'nosniff')
83
+ res.setHeader('X-XSS-Protection', '0')
84
+ }
85
+
86
+ function buildHstsValue(config: HTTPStrictTransportSecurityConfig): string {
87
+ let value = `max-age=${config.maxAge}`
88
+ if (config.includeSubDomains) value += '; includeSubDomains'
89
+ if (config.preload) value += '; preload'
90
+ return value
91
+ }