@atproto/oauth-provider 0.4.0 → 0.5.1

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 (402) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +29 -0
  3. package/dist/account/account-manager.d.ts +17 -3
  4. package/dist/account/account-manager.d.ts.map +1 -1
  5. package/dist/account/account-manager.js +102 -8
  6. package/dist/account/account-manager.js.map +1 -1
  7. package/dist/account/account-store.d.ts +81 -15
  8. package/dist/account/account-store.d.ts.map +1 -1
  9. package/dist/account/account-store.js +70 -19
  10. package/dist/account/account-store.js.map +1 -1
  11. package/dist/account/sign-in-data.d.ts +28 -0
  12. package/dist/account/sign-in-data.d.ts.map +1 -0
  13. package/dist/account/sign-in-data.js +16 -0
  14. package/dist/account/sign-in-data.js.map +1 -0
  15. package/dist/account/sign-up-data.d.ts +26 -0
  16. package/dist/account/sign-up-data.d.ts.map +1 -0
  17. package/dist/account/sign-up-data.js +11 -0
  18. package/dist/account/sign-up-data.js.map +1 -0
  19. package/dist/assets/app/bundle-manifest.json +598 -6
  20. package/dist/assets/app/index-ItwwtJ8r.js +36 -0
  21. package/dist/assets/app/index-ItwwtJ8r.js.map +1 -0
  22. package/dist/assets/app/main-B_dNxQo_.js +4 -0
  23. package/dist/assets/app/main-B_dNxQo_.js.map +1 -0
  24. package/dist/assets/app/main-CSatvmRR.css +3 -0
  25. package/dist/assets/app/main-CSatvmRR.js +306 -0
  26. package/dist/assets/app/main-CSatvmRR.js.map +1 -0
  27. package/dist/assets/app/messages-BQeltXSF.js +4 -0
  28. package/dist/assets/app/messages-BQeltXSF.js.map +1 -0
  29. package/dist/assets/app/messages-BQkEhfjg.js +4 -0
  30. package/dist/assets/app/messages-BQkEhfjg.js.map +1 -0
  31. package/dist/assets/app/messages-BUjKj_UJ.js +4 -0
  32. package/dist/assets/app/messages-BUjKj_UJ.js.map +1 -0
  33. package/dist/assets/app/messages-BWIQa8fO.js +4 -0
  34. package/dist/assets/app/messages-BWIQa8fO.js.map +1 -0
  35. package/dist/assets/app/messages-BaNVb0bp.js +4 -0
  36. package/dist/assets/app/messages-BaNVb0bp.js.map +1 -0
  37. package/dist/assets/app/messages-BaizVXcF.js +4 -0
  38. package/dist/assets/app/messages-BaizVXcF.js.map +1 -0
  39. package/dist/assets/app/messages-BfoClA1Y.js +4 -0
  40. package/dist/assets/app/messages-BfoClA1Y.js.map +1 -0
  41. package/dist/assets/app/messages-BsKGDZnC.js +4 -0
  42. package/dist/assets/app/messages-BsKGDZnC.js.map +1 -0
  43. package/dist/assets/app/messages-Bu-TJhml.js +4 -0
  44. package/dist/assets/app/messages-Bu-TJhml.js.map +1 -0
  45. package/dist/assets/app/messages-BvOKnBQk.js +4 -0
  46. package/dist/assets/app/messages-BvOKnBQk.js.map +1 -0
  47. package/dist/assets/app/messages-BxDzCiWz.js +4 -0
  48. package/dist/assets/app/messages-BxDzCiWz.js.map +1 -0
  49. package/dist/assets/app/messages-CDgFOy4S.js +4 -0
  50. package/dist/assets/app/messages-CDgFOy4S.js.map +1 -0
  51. package/dist/assets/app/messages-CLbTz0o9.js +4 -0
  52. package/dist/assets/app/messages-CLbTz0o9.js.map +1 -0
  53. package/dist/assets/app/messages-CNwSh0t7.js +4 -0
  54. package/dist/assets/app/messages-CNwSh0t7.js.map +1 -0
  55. package/dist/assets/app/messages-CSMNJ6P8.js +4 -0
  56. package/dist/assets/app/messages-CSMNJ6P8.js.map +1 -0
  57. package/dist/assets/app/messages-CZQUw3mp.js +4 -0
  58. package/dist/assets/app/messages-CZQUw3mp.js.map +1 -0
  59. package/dist/assets/app/messages-CZT41oVp.js +4 -0
  60. package/dist/assets/app/messages-CZT41oVp.js.map +1 -0
  61. package/dist/assets/app/messages-C_b-d3t8.js +4 -0
  62. package/dist/assets/app/messages-C_b-d3t8.js.map +1 -0
  63. package/dist/assets/app/messages-C_u3MTc2.js +4 -0
  64. package/dist/assets/app/messages-C_u3MTc2.js.map +1 -0
  65. package/dist/assets/app/messages-Cn8nHZic.js +4 -0
  66. package/dist/assets/app/messages-Cn8nHZic.js.map +1 -0
  67. package/dist/assets/app/messages-CtDywJUm.js +4 -0
  68. package/dist/assets/app/messages-CtDywJUm.js.map +1 -0
  69. package/dist/assets/app/messages-CurtIjBF.js +4 -0
  70. package/dist/assets/app/messages-CurtIjBF.js.map +1 -0
  71. package/dist/assets/app/messages-Cv6zIbaP.js +4 -0
  72. package/dist/assets/app/messages-Cv6zIbaP.js.map +1 -0
  73. package/dist/assets/app/messages-D1eLQuPE.js +4 -0
  74. package/dist/assets/app/messages-D1eLQuPE.js.map +1 -0
  75. package/dist/assets/app/messages-D8vHEaYW.js +4 -0
  76. package/dist/assets/app/messages-D8vHEaYW.js.map +1 -0
  77. package/dist/assets/app/messages-DJ1Q4GeC.js +4 -0
  78. package/dist/assets/app/messages-DJ1Q4GeC.js.map +1 -0
  79. package/dist/assets/app/messages-DRL3exqd.js +4 -0
  80. package/dist/assets/app/messages-DRL3exqd.js.map +1 -0
  81. package/dist/assets/app/messages-DWLPQRTp.js +4 -0
  82. package/dist/assets/app/messages-DWLPQRTp.js.map +1 -0
  83. package/dist/assets/app/messages-DjVaE9YE.js +4 -0
  84. package/dist/assets/app/messages-DjVaE9YE.js.map +1 -0
  85. package/dist/assets/app/messages-DqpMfFJR.js +4 -0
  86. package/dist/assets/app/messages-DqpMfFJR.js.map +1 -0
  87. package/dist/assets/app/messages-ETjhJBEN.js +4 -0
  88. package/dist/assets/app/messages-ETjhJBEN.js.map +1 -0
  89. package/dist/assets/app/messages-EUKrgrGn.js +4 -0
  90. package/dist/assets/app/messages-EUKrgrGn.js.map +1 -0
  91. package/dist/assets/app/messages-QQrOUcPW.js +4 -0
  92. package/dist/assets/app/messages-QQrOUcPW.js.map +1 -0
  93. package/dist/assets/app/messages-e2QGqFL6.js +4 -0
  94. package/dist/assets/app/messages-e2QGqFL6.js.map +1 -0
  95. package/dist/assets/app/messages-p61py7gD.js +4 -0
  96. package/dist/assets/app/messages-p61py7gD.js.map +1 -0
  97. package/dist/assets/asset.d.ts +1 -0
  98. package/dist/assets/asset.d.ts.map +1 -1
  99. package/dist/assets/assets-middleware.d.ts.map +1 -1
  100. package/dist/assets/assets-middleware.js +12 -7
  101. package/dist/assets/assets-middleware.js.map +1 -1
  102. package/dist/assets/index.d.ts +3 -2
  103. package/dist/assets/index.d.ts.map +1 -1
  104. package/dist/assets/index.js +13 -1
  105. package/dist/assets/index.js.map +1 -1
  106. package/dist/client/client-store.d.ts +3 -3
  107. package/dist/client/client-store.d.ts.map +1 -1
  108. package/dist/client/client-store.js +6 -5
  109. package/dist/client/client-store.js.map +1 -1
  110. package/dist/device/device-manager.d.ts +9 -8
  111. package/dist/device/device-manager.d.ts.map +1 -1
  112. package/dist/device/device-manager.js.map +1 -1
  113. package/dist/device/device-store.d.ts +3 -3
  114. package/dist/device/device-store.d.ts.map +1 -1
  115. package/dist/device/device-store.js +10 -9
  116. package/dist/device/device-store.js.map +1 -1
  117. package/dist/dpop/dpop-manager.d.ts +15 -7
  118. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  119. package/dist/dpop/dpop-manager.js +17 -3
  120. package/dist/dpop/dpop-manager.js.map +1 -1
  121. package/dist/dpop/dpop-nonce.d.ts +11 -5
  122. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  123. package/dist/dpop/dpop-nonce.js +47 -38
  124. package/dist/dpop/dpop-nonce.js.map +1 -1
  125. package/dist/errors/handle-unavailable-error.d.ts +11 -0
  126. package/dist/errors/handle-unavailable-error.d.ts.map +1 -0
  127. package/dist/errors/handle-unavailable-error.js +19 -0
  128. package/dist/errors/handle-unavailable-error.js.map +1 -0
  129. package/dist/errors/invalid-request-error.d.ts +6 -8
  130. package/dist/errors/invalid-request-error.d.ts.map +1 -1
  131. package/dist/errors/invalid-request-error.js +10 -8
  132. package/dist/errors/invalid-request-error.js.map +1 -1
  133. package/dist/lib/csp/index.d.ts +18 -0
  134. package/dist/lib/csp/index.d.ts.map +1 -0
  135. package/dist/lib/csp/index.js +72 -0
  136. package/dist/lib/csp/index.js.map +1 -0
  137. package/dist/lib/hcaptcha.d.ts +177 -0
  138. package/dist/lib/hcaptcha.d.ts.map +1 -0
  139. package/dist/lib/hcaptcha.js +155 -0
  140. package/dist/lib/hcaptcha.js.map +1 -0
  141. package/dist/lib/html/build-document.d.ts +11 -3
  142. package/dist/lib/html/build-document.d.ts.map +1 -1
  143. package/dist/lib/html/build-document.js +51 -15
  144. package/dist/lib/html/build-document.js.map +1 -1
  145. package/dist/lib/http/middleware.d.ts.map +1 -1
  146. package/dist/lib/http/middleware.js +4 -1
  147. package/dist/lib/http/middleware.js.map +1 -1
  148. package/dist/lib/http/request.d.ts +5 -2
  149. package/dist/lib/http/request.d.ts.map +1 -1
  150. package/dist/lib/http/request.js +16 -1
  151. package/dist/lib/http/request.js.map +1 -1
  152. package/dist/lib/http/response.d.ts +4 -2
  153. package/dist/lib/http/response.d.ts.map +1 -1
  154. package/dist/lib/http/response.js +23 -5
  155. package/dist/lib/http/response.js.map +1 -1
  156. package/dist/lib/locale.d.ts +15 -0
  157. package/dist/lib/locale.d.ts.map +1 -0
  158. package/dist/lib/locale.js +17 -0
  159. package/dist/lib/locale.js.map +1 -0
  160. package/dist/lib/util/function.d.ts +2 -2
  161. package/dist/lib/util/function.d.ts.map +1 -1
  162. package/dist/lib/util/function.js.map +1 -1
  163. package/dist/lib/util/type.d.ts +88 -1
  164. package/dist/lib/util/type.d.ts.map +1 -1
  165. package/dist/lib/util/type.js +41 -0
  166. package/dist/lib/util/type.js.map +1 -1
  167. package/dist/metadata/build-metadata.d.ts +2 -2
  168. package/dist/metadata/build-metadata.d.ts.map +1 -1
  169. package/dist/metadata/build-metadata.js.map +1 -1
  170. package/dist/oauth-errors.d.ts +1 -0
  171. package/dist/oauth-errors.d.ts.map +1 -1
  172. package/dist/oauth-errors.js +3 -1
  173. package/dist/oauth-errors.js.map +1 -1
  174. package/dist/oauth-hooks.d.ts +60 -3
  175. package/dist/oauth-hooks.d.ts.map +1 -1
  176. package/dist/oauth-hooks.js +3 -3
  177. package/dist/oauth-hooks.js.map +1 -1
  178. package/dist/oauth-provider.d.ts +23 -18
  179. package/dist/oauth-provider.d.ts.map +1 -1
  180. package/dist/oauth-provider.js +207 -204
  181. package/dist/oauth-provider.js.map +1 -1
  182. package/dist/oauth-verifier.d.ts +1 -1
  183. package/dist/oauth-verifier.d.ts.map +1 -1
  184. package/dist/oauth-verifier.js +2 -1
  185. package/dist/oauth-verifier.js.map +1 -1
  186. package/dist/output/build-authorize-data.d.ts +0 -1
  187. package/dist/output/build-authorize-data.d.ts.map +1 -1
  188. package/dist/output/build-authorize-data.js +0 -1
  189. package/dist/output/build-authorize-data.js.map +1 -1
  190. package/dist/output/build-customization-data.d.ts +241 -0
  191. package/dist/output/build-customization-data.d.ts.map +1 -0
  192. package/dist/output/build-customization-data.js +174 -0
  193. package/dist/output/build-customization-data.js.map +1 -0
  194. package/dist/output/output-manager.d.ts +16 -9
  195. package/dist/output/output-manager.d.ts.map +1 -1
  196. package/dist/output/output-manager.js +78 -42
  197. package/dist/output/output-manager.js.map +1 -1
  198. package/dist/output/send-authorize-redirect.d.ts +9 -6
  199. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  200. package/dist/output/send-authorize-redirect.js +20 -14
  201. package/dist/output/send-authorize-redirect.js.map +1 -1
  202. package/dist/output/send-web-page.d.ts +7 -2
  203. package/dist/output/send-web-page.d.ts.map +1 -1
  204. package/dist/output/send-web-page.js +37 -21
  205. package/dist/output/send-web-page.js.map +1 -1
  206. package/dist/request/request-manager.d.ts +1 -1
  207. package/dist/request/request-manager.d.ts.map +1 -1
  208. package/dist/request/request-manager.js +4 -4
  209. package/dist/request/request-manager.js.map +1 -1
  210. package/dist/request/request-store.d.ts +3 -3
  211. package/dist/request/request-store.d.ts.map +1 -1
  212. package/dist/request/request-store.js +11 -10
  213. package/dist/request/request-store.js.map +1 -1
  214. package/dist/token/token-store.d.ts +4 -4
  215. package/dist/token/token-store.d.ts.map +1 -1
  216. package/dist/token/token-store.js +13 -12
  217. package/dist/token/token-store.js.map +1 -1
  218. package/package.json +43 -20
  219. package/rollup.config.js +61 -17
  220. package/src/account/account-manager.ts +159 -8
  221. package/src/account/account-store.ts +127 -32
  222. package/src/account/sign-in-data.ts +15 -0
  223. package/src/account/sign-up-data.ts +11 -0
  224. package/src/assets/app/app.tsx +31 -16
  225. package/src/assets/app/backend-data.ts +15 -60
  226. package/src/assets/app/backend-types.ts +66 -0
  227. package/src/assets/app/components/forms/button-toggle-visibility.tsx +43 -0
  228. package/src/assets/app/components/forms/button.tsx +60 -0
  229. package/src/assets/app/components/forms/fieldset.tsx +55 -0
  230. package/src/assets/app/components/forms/form-card-async.tsx +103 -0
  231. package/src/assets/app/components/forms/form-card.tsx +49 -0
  232. package/src/assets/app/components/forms/input-checkbox.tsx +73 -0
  233. package/src/assets/app/components/forms/input-container.tsx +107 -0
  234. package/src/assets/app/components/forms/input-email-address.tsx +66 -0
  235. package/src/assets/app/components/forms/input-new-password.tsx +62 -0
  236. package/src/assets/app/components/forms/input-password.tsx +88 -0
  237. package/src/assets/app/components/forms/input-text.tsx +76 -0
  238. package/src/assets/app/components/forms/input-token.tsx +94 -0
  239. package/src/assets/app/components/forms/wizard-card.tsx +116 -0
  240. package/src/assets/app/components/layouts/layout-title-page.tsx +77 -0
  241. package/src/assets/app/components/layouts/layout-welcome.tsx +73 -0
  242. package/src/assets/app/components/utils/account-identifier.tsx +23 -0
  243. package/src/assets/app/components/utils/account-image.tsx +33 -0
  244. package/src/assets/app/components/utils/admonition.tsx +52 -0
  245. package/src/assets/app/components/utils/client-name.tsx +45 -0
  246. package/src/assets/app/components/utils/error-card.tsx +93 -0
  247. package/src/assets/app/components/utils/error-message.tsx +62 -0
  248. package/src/assets/app/components/utils/help-card.tsx +46 -0
  249. package/src/assets/app/components/utils/icons.tsx +88 -0
  250. package/src/assets/app/components/utils/link-anchor.tsx +28 -0
  251. package/src/assets/app/components/utils/link-title.tsx +26 -0
  252. package/src/assets/app/components/utils/multi-lang-string.tsx +56 -0
  253. package/src/assets/app/components/utils/password-strength-label.tsx +37 -0
  254. package/src/assets/app/components/utils/password-strength-meter.tsx +58 -0
  255. package/src/assets/app/components/{url-viewer.tsx → utils/url-viewer.tsx} +9 -6
  256. package/src/assets/app/hooks/use-api.ts +128 -55
  257. package/src/assets/app/hooks/use-async-action.ts +120 -0
  258. package/src/assets/app/hooks/use-browser-color-scheme.ts +31 -0
  259. package/src/assets/app/hooks/use-csrf-token.ts +1 -1
  260. package/src/assets/app/hooks/use-random-string.ts +37 -0
  261. package/src/assets/app/hooks/use-stepper.ts +87 -0
  262. package/src/assets/app/index.html +182 -0
  263. package/src/assets/app/lib/api.ts +248 -79
  264. package/src/assets/app/lib/clsx.ts +5 -8
  265. package/src/assets/app/lib/json-client.ts +94 -0
  266. package/src/assets/app/lib/password.ts +98 -0
  267. package/src/assets/app/lib/ref.ts +17 -0
  268. package/src/assets/app/locales/an/messages.po +492 -0
  269. package/src/assets/app/locales/ast/messages.po +492 -0
  270. package/src/assets/app/locales/ca/messages.po +492 -0
  271. package/src/assets/app/locales/da/messages.po +492 -0
  272. package/src/assets/app/locales/de/messages.po +492 -0
  273. package/src/assets/app/locales/el/messages.po +492 -0
  274. package/src/assets/app/locales/en/messages.po +492 -0
  275. package/src/assets/app/locales/en-GB/messages.po +492 -0
  276. package/src/assets/app/locales/es/messages.po +492 -0
  277. package/src/assets/app/locales/eu/messages.po +492 -0
  278. package/src/assets/app/locales/fi/messages.po +492 -0
  279. package/src/assets/app/locales/fr/messages.po +492 -0
  280. package/src/assets/app/locales/ga/messages.po +492 -0
  281. package/src/assets/app/locales/gl/messages.po +492 -0
  282. package/src/assets/app/locales/hi/messages.po +492 -0
  283. package/src/assets/app/locales/hu/messages.po +492 -0
  284. package/src/assets/app/locales/ia/messages.po +492 -0
  285. package/src/assets/app/locales/id/messages.po +492 -0
  286. package/src/assets/app/locales/it/messages.po +492 -0
  287. package/src/assets/app/locales/ja/messages.po +492 -0
  288. package/src/assets/app/locales/km/messages.po +492 -0
  289. package/src/assets/app/locales/ko/messages.po +492 -0
  290. package/src/assets/app/locales/load.ts +8 -0
  291. package/src/assets/app/locales/locale-context.ts +19 -0
  292. package/src/assets/app/locales/locale-provider.tsx +112 -0
  293. package/src/assets/app/locales/locale-selector.tsx +58 -0
  294. package/src/assets/app/locales/locales.ts +168 -0
  295. package/src/assets/app/locales/ne/messages.po +492 -0
  296. package/src/assets/app/locales/nl/messages.po +492 -0
  297. package/src/assets/app/locales/pl/messages.po +492 -0
  298. package/src/assets/app/locales/pt-BR/messages.po +492 -0
  299. package/src/assets/app/locales/ro/messages.po +492 -0
  300. package/src/assets/app/locales/ru/messages.po +492 -0
  301. package/src/assets/app/locales/sv/messages.po +492 -0
  302. package/src/assets/app/locales/th/messages.po +492 -0
  303. package/src/assets/app/locales/tr/messages.po +492 -0
  304. package/src/assets/app/locales/uk/messages.po +492 -0
  305. package/src/assets/app/locales/vi/messages.po +492 -0
  306. package/src/assets/app/locales/zh-CN/messages.po +492 -0
  307. package/src/assets/app/locales/zh-HK/messages.po +492 -0
  308. package/src/assets/app/locales/zh-TW/messages.po +492 -0
  309. package/src/assets/app/main.css +23 -2
  310. package/src/assets/app/main.tsx +24 -8
  311. package/src/assets/app/views/authorize/accept/accept-form.tsx +150 -0
  312. package/src/assets/app/views/authorize/accept/accept-view.tsx +70 -0
  313. package/src/assets/app/views/authorize/authorize-view.tsx +180 -0
  314. package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  315. package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  316. package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +127 -0
  317. package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +244 -0
  318. package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  319. package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +145 -0
  320. package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +140 -0
  321. package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  322. package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +289 -0
  323. package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  324. package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +158 -0
  325. package/src/assets/app/views/authorize/welcome/welcome-view.tsx +56 -0
  326. package/src/assets/app/views/error/error-view.tsx +31 -0
  327. package/src/assets/asset.ts +1 -0
  328. package/src/assets/assets-middleware.ts +13 -8
  329. package/src/assets/index.ts +15 -2
  330. package/src/client/client-store.ts +10 -12
  331. package/src/device/device-manager.ts +8 -12
  332. package/src/device/device-store.ts +9 -15
  333. package/src/dpop/dpop-manager.ts +20 -8
  334. package/src/dpop/dpop-nonce.ts +58 -40
  335. package/src/errors/handle-unavailable-error.ts +18 -0
  336. package/src/errors/invalid-request-error.ts +10 -8
  337. package/src/lib/csp/index.ts +98 -0
  338. package/src/lib/hcaptcha.ts +182 -0
  339. package/src/lib/html/build-document.ts +60 -16
  340. package/src/lib/http/middleware.ts +4 -3
  341. package/src/lib/http/request.ts +31 -1
  342. package/src/lib/http/response.ts +22 -9
  343. package/src/lib/locale.ts +21 -0
  344. package/src/lib/util/function.ts +0 -3
  345. package/src/lib/util/type.ts +130 -1
  346. package/src/metadata/build-metadata.ts +2 -1
  347. package/src/oauth-errors.ts +1 -0
  348. package/src/oauth-hooks.ts +69 -3
  349. package/src/oauth-provider.ts +403 -307
  350. package/src/oauth-verifier.ts +3 -1
  351. package/src/output/build-authorize-data.ts +1 -3
  352. package/src/output/build-customization-data.ts +228 -0
  353. package/src/output/output-manager.ts +111 -48
  354. package/src/output/send-authorize-redirect.ts +43 -36
  355. package/src/output/send-web-page.ts +40 -26
  356. package/src/request/request-manager.ts +4 -4
  357. package/src/request/request-store.ts +12 -16
  358. package/src/token/token-store.ts +14 -18
  359. package/tailwind.config.js +5 -0
  360. package/tsconfig.backend.tsbuildinfo +1 -1
  361. package/tsconfig.frontend.tsbuildinfo +1 -1
  362. package/tsconfig.tools.tsbuildinfo +1 -1
  363. package/vite.config.mjs +16 -0
  364. package/.postcssrc.yml +0 -3
  365. package/dist/assets/app/main.css +0 -3
  366. package/dist/assets/app/main.js +0 -20
  367. package/dist/assets/app/main.js.map +0 -1
  368. package/dist/output/customization.d.ts +0 -27
  369. package/dist/output/customization.d.ts.map +0 -1
  370. package/dist/output/customization.js +0 -88
  371. package/dist/output/customization.js.map +0 -1
  372. package/src/assets/app/components/accept-form.tsx +0 -137
  373. package/src/assets/app/components/account-identifier.tsx +0 -18
  374. package/src/assets/app/components/account-picker.tsx +0 -127
  375. package/src/assets/app/components/button.tsx +0 -34
  376. package/src/assets/app/components/client-name.tsx +0 -37
  377. package/src/assets/app/components/fieldset.tsx +0 -26
  378. package/src/assets/app/components/form-card.tsx +0 -47
  379. package/src/assets/app/components/help-card.tsx +0 -42
  380. package/src/assets/app/components/icons/alert-icon.tsx +0 -5
  381. package/src/assets/app/components/icons/at-symbol-icon.tsx +0 -5
  382. package/src/assets/app/components/icons/caret-right-icon.tsx +0 -5
  383. package/src/assets/app/components/icons/lock-icon.tsx +0 -5
  384. package/src/assets/app/components/icons/token-icon.tsx +0 -5
  385. package/src/assets/app/components/icons/util.tsx +0 -17
  386. package/src/assets/app/components/info-card.tsx +0 -45
  387. package/src/assets/app/components/input-checkbox.tsx +0 -47
  388. package/src/assets/app/components/input-container.tsx +0 -37
  389. package/src/assets/app/components/input-layout.tsx +0 -47
  390. package/src/assets/app/components/input-text.tsx +0 -69
  391. package/src/assets/app/components/layout-title-page.tsx +0 -60
  392. package/src/assets/app/components/layout-welcome.tsx +0 -74
  393. package/src/assets/app/components/sign-in-form.tsx +0 -337
  394. package/src/assets/app/components/sign-up-account-form.tsx +0 -194
  395. package/src/assets/app/components/sign-up-disclaimer.tsx +0 -44
  396. package/src/assets/app/views/accept-view.tsx +0 -55
  397. package/src/assets/app/views/authorize-view.tsx +0 -106
  398. package/src/assets/app/views/error-view.tsx +0 -36
  399. package/src/assets/app/views/sign-in-view.tsx +0 -111
  400. package/src/assets/app/views/sign-up-view.tsx +0 -86
  401. package/src/assets/app/views/welcome-view.tsx +0 -54
  402. package/src/output/customization.ts +0 -118
@@ -94,8 +94,10 @@ export class OAuthVerifier {
94
94
  : new ReplayStoreMemory(),
95
95
  accessTokenType = AccessTokenType.jwt,
96
96
 
97
- ...dpopMgrOptions
97
+ ...rest
98
98
  }: OAuthVerifierOptions) {
99
+ const dpopMgrOptions: DpopManagerOptions = rest
100
+
99
101
  const issuerParsed = oauthIssuerIdentifierSchema.parse(issuer)
100
102
  const issuerUrl = new URL(issuerParsed)
101
103
 
@@ -31,7 +31,7 @@ export type AuthorizationResultAuthorize = {
31
31
  }
32
32
 
33
33
  // TODO: find a way to share this type with the frontend code
34
- // (app/backend-data.ts)
34
+ // (app/backend-types.ts)
35
35
 
36
36
  type Session = {
37
37
  account: Account
@@ -47,7 +47,6 @@ export type AuthorizeData = {
47
47
  clientMetadata: OAuthClientMetadata
48
48
  clientTrusted: boolean
49
49
  requestUri: string
50
- csrfCookie: string
51
50
  loginHint?: string
52
51
  scopeDetails?: ScopeDetail[]
53
52
  newSessionsRequireConsent: boolean
@@ -62,7 +61,6 @@ export function buildAuthorizeData(
62
61
  clientMetadata: data.client.metadata,
63
62
  clientTrusted: data.client.info.isTrusted,
64
63
  requestUri: data.authorize.uri,
65
- csrfCookie: `csrf-${data.authorize.uri}`,
66
64
  loginHint: data.parameters.login_hint,
67
65
  newSessionsRequireConsent: data.parameters.prompt === 'consent',
68
66
  scopeDetails: data.authorize.scopeDetails,
@@ -0,0 +1,228 @@
1
+ import { z } from 'zod'
2
+ import { hcaptchaConfigSchema } from '../lib/hcaptcha.js'
3
+ import { isLinkRel } from '../lib/html/build-document.js'
4
+ import { multiLangStringSchema } from '../lib/locale.js'
5
+ export { type HcaptchaConfig, hcaptchaConfigSchema } from '../lib/hcaptcha.js'
6
+
7
+ // Matches colors defined in tailwind.config.js
8
+ export const colorNames = ['brand', 'error', 'warning', 'success'] as const
9
+ export const colorNameSchema = z.enum(colorNames)
10
+ export type ColorName = z.infer<typeof colorNameSchema>
11
+
12
+ const parsedColorSchema = z.string().transform((value, ctx): RgbColor => {
13
+ try {
14
+ const { r, g, b, a } = parseColor(value)
15
+ if (a != null) {
16
+ ctx.addIssue({
17
+ code: z.ZodIssueCode.custom,
18
+ message: 'Alpha values are not supported',
19
+ })
20
+ }
21
+ return { r, g, b }
22
+ } catch (e) {
23
+ ctx.addIssue({
24
+ code: z.ZodIssueCode.custom,
25
+ message: e instanceof Error ? e.message : 'Invalid color value',
26
+ })
27
+ // Won't actually be used (since an issue was added):
28
+ return { r: 0, g: 0, b: 0 }
29
+ }
30
+ })
31
+ export type ParsedColor = z.infer<typeof parsedColorSchema> // Same as RgbColor
32
+
33
+ export const colorsDefinitionSchema = z.record(
34
+ colorNameSchema,
35
+ parsedColorSchema.optional(),
36
+ )
37
+ export type ColorsDefinition = z.infer<typeof colorsDefinitionSchema>
38
+
39
+ export const localizedStringSchema = z.union([
40
+ z.string(),
41
+ multiLangStringSchema,
42
+ ])
43
+ export type LocalizedString = z.infer<typeof localizedStringSchema>
44
+
45
+ export const linkRelSchema = z.string().refine(isLinkRel, 'Invalid link rel')
46
+ export type LinkRel = z.infer<typeof linkRelSchema>
47
+
48
+ export const linkDefinitionSchema = z.object({
49
+ title: localizedStringSchema,
50
+ href: z.string().url(),
51
+ rel: linkRelSchema.optional(),
52
+ })
53
+ export type LinkDefinition = z.infer<typeof linkDefinitionSchema>
54
+
55
+ /**
56
+ * Aesthetic customization
57
+ */
58
+ export const brandingConfigSchema = z.object({
59
+ name: z.string().optional(),
60
+ logo: z.string().optional(),
61
+ colors: colorsDefinitionSchema.optional(),
62
+ links: z.array(linkDefinitionSchema).readonly().optional(),
63
+ })
64
+ export type BrandingInput = z.input<typeof brandingConfigSchema>
65
+ export type Branding = z.infer<typeof brandingConfigSchema>
66
+
67
+ export const customizationSchema = z.object({
68
+ /**
69
+ * Available user domains that can be used to sign up. A non-empty array
70
+ * is required to enable the sign-up feature.
71
+ */
72
+ availableUserDomains: z.array(z.string()).optional(),
73
+ /**
74
+ * UI customizations
75
+ */
76
+ branding: brandingConfigSchema.optional(),
77
+ /**
78
+ * Is an invite code required to sign up?
79
+ */
80
+ inviteCodeRequired: z.boolean().optional(),
81
+ /**
82
+ * Enables hCaptcha during sign-up.
83
+ */
84
+ hcaptcha: hcaptchaConfigSchema.optional(),
85
+ })
86
+ export type CustomizationInput = z.input<typeof customizationSchema>
87
+ export type Customization = z.infer<typeof customizationSchema>
88
+
89
+ export type CustomizationData = {
90
+ // Functional customization
91
+ hcaptchaSiteKey?: string
92
+ inviteCodeRequired?: boolean
93
+ availableUserDomains?: string[]
94
+
95
+ // Aesthetic customization
96
+ name?: string
97
+ logo?: string
98
+ links?: readonly LinkDefinition[]
99
+ }
100
+
101
+ export function buildCustomizationData({
102
+ branding,
103
+ availableUserDomains,
104
+ inviteCodeRequired,
105
+ hcaptcha,
106
+ }: Customization): CustomizationData {
107
+ // @NOTE the front end does not need colors here as they will be injected as
108
+ // CSS variables.
109
+ // @NOTE We only copy the values explicitly needed to avoid leaking sensitive
110
+ // data (in case the caller passed more than what we expect).
111
+ return {
112
+ availableUserDomains,
113
+ inviteCodeRequired,
114
+ hcaptchaSiteKey: hcaptcha?.siteKey,
115
+ name: branding?.name,
116
+ logo: branding?.logo,
117
+ links: branding?.links,
118
+ }
119
+ }
120
+
121
+ export function buildCustomizationCss({ branding }: Customization) {
122
+ const vars = Array.from(buildCustomizationVars(branding))
123
+ if (vars.length) return `:root { ${vars.join(' ')} }`
124
+
125
+ return ''
126
+ }
127
+
128
+ function* buildCustomizationVars(branding?: Branding) {
129
+ if (branding?.colors) {
130
+ for (const name of colorNames) {
131
+ const value = branding.colors[name]
132
+ if (!value) continue // Skip missing colors
133
+
134
+ const { r, g, b } = value
135
+
136
+ const contrast = computeLuma({ r, g, b }) > 128 ? '0 0 0' : '255 255 255'
137
+
138
+ yield `--color-${name}: ${r} ${g} ${b};`
139
+ yield `--color-${name}-c: ${contrast};`
140
+ }
141
+ }
142
+ }
143
+
144
+ type RgbColor = { r: number; g: number; b: number }
145
+ type RgbaColor = { r: number; g: number; b: number; a?: number }
146
+ function parseColor(color: string): RgbaColor {
147
+ if (color.startsWith('#')) {
148
+ return parseHexColor(color)
149
+ }
150
+
151
+ if (color.startsWith('rgba(')) {
152
+ return parseRgbaColor(color)
153
+ }
154
+
155
+ if (color.startsWith('rgb(')) {
156
+ return parseRgbColor(color)
157
+ }
158
+
159
+ // Should never happen (as long as the input is a validated WebColor)
160
+ throw new TypeError(`Invalid color value: ${color}`)
161
+ }
162
+
163
+ function parseHexColor(v: string) {
164
+ // parseInt('az', 16) does not return NaN so we need to check the format
165
+ if (!/^#[0-9a-f]+$/i.test(v)) {
166
+ throw new TypeError(`Invalid hex color value: ${v}`)
167
+ }
168
+
169
+ if (v.length === 4 || v.length === 5) {
170
+ const r = parseUi8Hex(v.slice(1, 2))
171
+ const g = parseUi8Hex(v.slice(2, 3))
172
+ const b = parseUi8Hex(v.slice(3, 4))
173
+ const a = v.length > 4 ? parseUi8Hex(v.slice(4, 5)) : undefined
174
+ return { r, g, b, a }
175
+ }
176
+
177
+ if (v.length === 7 || v.length === 9) {
178
+ const r = parseUi8Hex(v.slice(1, 3))
179
+ const g = parseUi8Hex(v.slice(3, 5))
180
+ const b = parseUi8Hex(v.slice(5, 7))
181
+ const a = v.length > 8 ? parseUi8Hex(v.slice(7, 9)) : undefined
182
+ return { r, g, b, a }
183
+ }
184
+
185
+ throw new TypeError(`Invalid hex color value: ${v}`)
186
+ }
187
+
188
+ function parseRgbColor(v: string) {
189
+ const matches = v.match(/^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/)
190
+ if (!matches) throw new TypeError(`Invalid rgb color value: ${v}`)
191
+
192
+ const r = parseUi8Dec(matches[1])
193
+ const g = parseUi8Dec(matches[2])
194
+ const b = parseUi8Dec(matches[3])
195
+ return { r, g, b }
196
+ }
197
+
198
+ function parseRgbaColor(v: string) {
199
+ const matches = v.match(
200
+ /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
201
+ )
202
+ if (!matches) throw new TypeError(`Invalid rgba color value: ${v}`)
203
+
204
+ const r = parseUi8Dec(matches[1])
205
+ const g = parseUi8Dec(matches[2])
206
+ const b = parseUi8Dec(matches[3])
207
+ const a = parseUi8Dec(matches[4])
208
+ return { r, g, b, a }
209
+ }
210
+
211
+ function computeLuma({ r, g, b }: RgbaColor) {
212
+ return 0.299 * r + 0.587 * g + 0.114 * b
213
+ }
214
+
215
+ function parseUi8Hex(v: string) {
216
+ return asUi8(parseInt(v, 16))
217
+ }
218
+
219
+ function parseUi8Dec(v: string) {
220
+ return asUi8(parseInt(v, 10))
221
+ }
222
+
223
+ function asUi8(v: number) {
224
+ if (v >= 0 && v <= 255 && v === (v | 0)) return v
225
+ throw new TypeError(
226
+ `Invalid color component "${v}" (expected an integer between 0 and 255)`,
227
+ )
228
+ }
@@ -1,86 +1,149 @@
1
1
  import type { ServerResponse } from 'node:http'
2
2
  import { Asset } from '../assets/asset.js'
3
- import { getAsset } from '../assets/index.js'
4
- import { Html, cssCode, html } from '../lib/html/index.js'
3
+ import { enumerateAssets } from '../assets/index.js'
4
+ import { CspConfig, mergeCsp } from '../lib/csp/index.js'
5
+ import {
6
+ Html,
7
+ LinkAttrs,
8
+ MetaAttrs,
9
+ cssCode,
10
+ html,
11
+ isLinkRel,
12
+ } from '../lib/html/index.js'
13
+ import { AVAILABLE_LOCALES, Locale, isAvailableLocale } from '../lib/locale.js'
5
14
  import {
6
15
  AuthorizationResultAuthorize,
7
16
  buildAuthorizeData,
8
17
  } from './build-authorize-data.js'
9
- import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
10
18
  import {
11
19
  Customization,
20
+ LinkDefinition,
12
21
  buildCustomizationCss,
13
22
  buildCustomizationData,
14
- } from './customization.js'
15
- import { declareBackendData, sendWebPage } from './send-web-page.js'
23
+ } from './build-customization-data.js'
24
+ import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
25
+ import { assetToCsp, declareBackendData, sendWebPage } from './send-web-page.js'
26
+
27
+ const HCAPTCHA_CSP = {
28
+ 'script-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
29
+ 'frame-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
30
+ 'style-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
31
+ 'connect-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],
32
+ } as const satisfies CspConfig
33
+
34
+ export type SendPageOptions = {
35
+ preferredLocales?: readonly string[]
36
+ }
16
37
 
17
38
  export class OutputManager {
18
- readonly customizationScript: Html
19
- readonly customizationStyle: Html
20
- readonly customizationLinks?: Customization['links']
21
-
22
- // Could technically cause an "UnhandledPromiseRejection", which might cause
23
- // the process to exit. This is intentional, as it's a critical error. It
24
- // should never happen in practice, as the built assets are bundled with the
25
- // package.
26
- readonly assetsPromise: Promise<[js: Asset, css: Asset]> = Promise.all([
27
- getAsset('main.js'),
28
- getAsset('main.css'),
29
- ] as const)
30
-
31
- constructor(customization?: Customization) {
32
- // Note: building this here for two reasons:
39
+ readonly links?: readonly LinkDefinition[]
40
+ readonly meta: readonly MetaAttrs[] = [
41
+ { name: 'robots', content: 'noindex' },
42
+ { name: 'description', content: 'ATProto OAuth authorization page' },
43
+ ]
44
+ readonly scripts: readonly (Asset | Html)[]
45
+ readonly styles: readonly (Asset | Html)[]
46
+ readonly csp: CspConfig
47
+
48
+ constructor(customization: Customization) {
49
+ this.links = customization.branding?.links
50
+
51
+ const scripts = Array.from(enumerateAssets('application/javascript'))
52
+ const styles = Array.from(enumerateAssets('text/css'))
53
+
54
+ // Note: building scripts/styles/csp here for two reasons:
33
55
  // 1. To avoid re-building it on every request
34
- // 2. To throw during init if the customization is invalid
35
- this.customizationScript = declareBackendData(
36
- '__customizationData',
37
- buildCustomizationData(customization),
38
- )
39
- this.customizationStyle = cssCode(buildCustomizationCss(customization))
40
- this.customizationLinks = customization?.links
56
+ // 2. To throw during init if the customization/config is invalid
57
+
58
+ this.scripts = [
59
+ declareBackendData('__availableLocales', AVAILABLE_LOCALES),
60
+ declareBackendData(
61
+ '__customizationData',
62
+ buildCustomizationData(customization),
63
+ ),
64
+ // Last (to be able to read the "backend data" variables)
65
+ ...scripts.filter((asset) => asset.isEntry),
66
+ ]
67
+
68
+ this.styles = [
69
+ // First (to be overridden by customization)
70
+ ...styles,
71
+ cssCode(buildCustomizationCss(customization)),
72
+ ]
73
+
74
+ const customizationCsp = customization?.hcaptcha ? HCAPTCHA_CSP : undefined
75
+ const assetsCsp: CspConfig = {
76
+ 'script-src': scripts.map(assetToCsp),
77
+ 'style-src': styles.map(assetToCsp),
78
+ }
79
+
80
+ this.csp = mergeCsp(customizationCsp, assetsCsp)
41
81
  }
42
82
 
43
83
  async sendAuthorizePage(
44
84
  res: ServerResponse,
45
85
  data: AuthorizationResultAuthorize,
86
+ options?: SendPageOptions,
46
87
  ): Promise<void> {
47
- const [jsAsset, cssAsset] = await this.assetsPromise
88
+ const locale = negotiateLocale(
89
+ data.parameters.ui_locales?.split(' ') ?? options?.preferredLocales,
90
+ )
48
91
 
49
92
  return sendWebPage(res, {
50
93
  scripts: [
51
94
  declareBackendData('__authorizeData', buildAuthorizeData(data)),
52
- this.customizationScript,
53
- jsAsset, // Last (to be able to read the "backend data" variables)
95
+ ...this.scripts,
54
96
  ],
55
- styles: [
56
- cssAsset, // First (to be overridden by customization)
57
- this.customizationStyle,
58
- ],
59
- links: this.customizationLinks,
60
- htmlAttrs: { lang: 'en' },
61
- title: 'Authorize',
97
+ styles: this.styles,
98
+ meta: this.meta,
99
+ links: this.buildLinks(locale),
100
+ htmlAttrs: { lang: locale },
62
101
  body: html`<div id="root"></div>`,
102
+ csp: this.csp,
63
103
  })
64
104
  }
65
105
 
66
- async sendErrorPage(res: ServerResponse, err: unknown): Promise<void> {
67
- const [jsAsset, cssAsset] = await this.assetsPromise
106
+ async sendErrorPage(
107
+ res: ServerResponse,
108
+ err: unknown,
109
+ options?: SendPageOptions,
110
+ ): Promise<void> {
111
+ const locale = negotiateLocale(options?.preferredLocales)
68
112
 
69
113
  return sendWebPage(res, {
70
114
  status: buildErrorStatus(err),
71
115
  scripts: [
72
116
  declareBackendData('__errorData', buildErrorPayload(err)),
73
- this.customizationScript,
74
- jsAsset, // Last (to be able to read the "backend data" variables)
117
+ ...this.scripts,
75
118
  ],
76
- styles: [
77
- cssAsset, // First (to be overridden by customization)
78
- this.customizationStyle,
79
- ],
80
- links: this.customizationLinks,
81
- htmlAttrs: { lang: 'en' },
82
- title: 'Error',
119
+ styles: this.styles,
120
+ meta: this.meta,
121
+ links: this.buildLinks(locale),
122
+ htmlAttrs: { lang: locale },
83
123
  body: html`<div id="root"></div>`,
124
+ csp: this.csp,
84
125
  })
85
126
  }
127
+
128
+ buildLinks(locale: Locale) {
129
+ return this.links
130
+ ?.map(({ rel, href, title }: LinkDefinition): LinkAttrs | undefined =>
131
+ isLinkRel(rel)
132
+ ? typeof title === 'string'
133
+ ? { href, rel, title }
134
+ : { href, rel, title: title[locale] || title.en }
135
+ : undefined,
136
+ )
137
+ .filter((v) => v != null)
138
+ }
139
+ }
140
+
141
+ function negotiateLocale(desiredLocales?: readonly string[]): Locale {
142
+ if (desiredLocales) {
143
+ for (const locale of desiredLocales) {
144
+ if (locale === '*') break // use default
145
+ if (isAvailableLocale(locale)) return locale
146
+ }
147
+ }
148
+ return 'en'
86
149
  }
@@ -11,31 +11,42 @@ import { sendWebPage } from './send-web-page.js'
11
11
  // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-7.5.4
12
12
  const REDIRECT_STATUS_CODE = 303
13
13
 
14
- export type AuthorizationResponseParameters = {
15
- // Will be added from AuthorizationResultRedirect['issuer']
16
- // iss: string // rfc9207
17
-
18
- // Will be added from AuthorizationResultRedirect['parameters']
19
- // state?: string
20
-
21
- code?: Code
22
- id_token?: string
23
- access_token?: string
24
- token_type?: OAuthTokenType
25
- expires_in?: string
26
-
27
- response?: string // FAPI JARM
28
- session_state?: string // OIDC Session Management
29
-
30
- error?: string
31
- error_description?: string
32
- error_uri?: string
33
- }
14
+ /**
15
+ * @note `iss` and `state` will be added from the
16
+ * {@link AuthorizationResultRedirect} object.
17
+ */
18
+ export type AuthorizationRedirectParameters =
19
+ | {
20
+ // iss: string
21
+ // state?: string
22
+ code: Code
23
+ id_token?: string
24
+ access_token?: string
25
+ token_type?: OAuthTokenType
26
+ expires_in?: string
27
+ }
28
+ | {
29
+ // iss: string
30
+ // state?: string
31
+ error: string
32
+ error_description?: string
33
+ error_uri?: string
34
+ }
35
+
36
+ const SUCCESS_REDIRECT_KEYS = [
37
+ 'code',
38
+ 'id_token',
39
+ 'access_token',
40
+ 'expires_in',
41
+ 'token_type',
42
+ ] as const
43
+
44
+ const ERROR_REDIRECT_KEYS = ['error', 'error_description', 'error_uri'] as const
34
45
 
35
46
  export type AuthorizationResultRedirect = {
36
47
  issuer: string
37
48
  parameters: OAuthAuthorizationRequestParameters
38
- redirect: AuthorizationResponseParameters
49
+ redirect: AuthorizationRedirectParameters
39
50
  }
40
51
 
41
52
  export async function sendAuthorizeRedirect(
@@ -49,23 +60,19 @@ export async function sendAuthorizeRedirect(
49
60
 
50
61
  const mode = parameters.response_mode || 'query' // @TODO: default should depend on response_type
51
62
 
52
- const entries: [string, string][] = Object.entries({
53
- iss: issuer, // rfc9207
54
- state: parameters.state,
55
-
56
- response: redirect.response, // FAPI JARM
57
- session_state: redirect.session_state, // OIDC Session Management
63
+ const entries: [string, string][] = [
64
+ ['iss', issuer], // rfc9207
65
+ ]
58
66
 
59
- code: redirect.code,
60
- id_token: redirect.id_token,
61
- access_token: redirect.access_token,
62
- expires_in: redirect.expires_in,
63
- token_type: redirect.token_type,
67
+ if (parameters.state != null) {
68
+ entries.push(['state', parameters.state])
69
+ }
64
70
 
65
- error: redirect.error,
66
- error_description: redirect.error_description,
67
- error_uri: redirect.error_uri,
68
- }).filter((entry): entry is [string, string] => entry[1] != null)
71
+ const keys = 'code' in redirect ? SUCCESS_REDIRECT_KEYS : ERROR_REDIRECT_KEYS
72
+ for (const key of keys) {
73
+ const value = redirect[key]
74
+ if (value != null) entries.push([key, value])
75
+ }
69
76
 
70
77
  res.setHeader('Cache-Control', 'no-store')
71
78
 
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import type { ServerResponse } from 'node:http'
3
+ import { CspConfig, CspValue, buildCsp, mergeCsp } from '../lib/csp/index.js'
3
4
  import {
4
5
  AssetRef,
5
6
  BuildDocumentOptions,
@@ -13,16 +14,38 @@ export function declareBackendData(name: string, data: unknown) {
13
14
  // The script tag is removed after the data is assigned to the global variable
14
15
  // to prevent other scripts from deducing the value of the variable. The "app"
15
16
  // script will read the global variable and then unset it. See
16
- // "readBackendData" in "src/assets/app/backend-data.ts".
17
+ // "readBackendData" in "src/assets/app/backend-types.ts".
17
18
  return js`window[${name}]=${data};document.currentScript.remove();`
18
19
  }
19
20
 
20
- export type SendWebPageOptions = BuildDocumentOptions & WriteResponseOptions
21
+ export type SendWebPageOptions = BuildDocumentOptions &
22
+ WriteResponseOptions & {
23
+ csp?: CspConfig
24
+ }
21
25
 
22
26
  export async function sendWebPage(
23
27
  res: ServerResponse,
24
28
  options: SendWebPageOptions,
25
29
  ): Promise<void> {
30
+ const csp = mergeCsp(options.csp, {
31
+ 'default-src': ["'none'"],
32
+ 'base-uri': options.base?.origin as undefined | `https://${string}`,
33
+ 'script-src': ["'self'", ...assetsToCsp(options.scripts)],
34
+ 'style-src': ["'self'", ...assetsToCsp(options.styles)],
35
+ 'img-src': ["'self'", 'data:', 'https:'],
36
+ 'connect-src': ["'self'"],
37
+ 'upgrade-insecure-requests': true,
38
+
39
+ // Prevents the CSP to be embedded in a page <meta>:
40
+ 'frame-ancestors': ["'none'"],
41
+ })
42
+
43
+ // @NOTE the csp string might become too long. However, since we need to
44
+ // specify the "frame-ancestors" directive, we can't use a meta tag. For that
45
+ // reason, we won't try to avoid too long headers and let the proxy throw
46
+ // in case of a too long header.
47
+ res.setHeader('Content-Security-Policy', buildCsp(csp))
48
+
26
49
  // @TODO: make these headers configurable (?)
27
50
  res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
28
51
  res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless')
@@ -33,36 +56,27 @@ export async function sendWebPage(
33
56
  res.setHeader('X-Content-Type-Options', 'nosniff')
34
57
  res.setHeader('X-XSS-Protection', '0')
35
58
  res.setHeader('Strict-Transport-Security', 'max-age=63072000')
36
- res.setHeader(
37
- 'Content-Security-Policy',
38
- [
39
- `default-src 'none'`,
40
- `frame-ancestors 'none'`,
41
- `form-action 'none'`,
42
- `base-uri ${options.base?.origin || `'none'`}`,
43
- `script-src 'self' ${
44
- options.scripts?.map(assetToHash).map(hashToCspRule).join(' ') ?? ''
45
- }`,
46
- `style-src 'self' ${
47
- options.styles?.map(assetToHash).map(hashToCspRule).join(' ') ?? ''
48
- }`,
49
- `img-src 'self' data: https:`,
50
- `connect-src 'self'`,
51
- `upgrade-insecure-requests`,
52
- ].join('; '),
53
- )
54
59
 
55
60
  const html = buildDocument(options)
56
61
 
57
62
  return writeHtml(res, html.toString(), options)
58
63
  }
59
64
 
60
- function assetToHash(asset: Html | AssetRef): string {
61
- return asset instanceof Html
62
- ? createHash('sha256').update(asset.toString()).digest('base64')
63
- : asset.sha256
65
+ export function* assetsToCsp(
66
+ assets?: Iterable<Html | AssetRef>,
67
+ ): Generator<CspValue> {
68
+ if (assets) {
69
+ for (const asset of assets) {
70
+ yield assetToCsp(asset)
71
+ }
72
+ }
64
73
  }
65
74
 
66
- function hashToCspRule(hash: string): string {
67
- return `'sha256-${hash}'`
75
+ export function assetToCsp(asset: Html | AssetRef): CspValue {
76
+ if (asset instanceof Html) {
77
+ const hash = createHash('sha256').update(asset.toString()).digest('base64')
78
+ return `'sha256-${hash}'`
79
+ } else {
80
+ return `'sha256-${asset.sha256}'`
81
+ }
68
82
  }