@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
@@ -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,189 @@
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
+ export const ColorsDefinitionSchema = z.record(colorNameSchema, z.string())
13
+ export type ColorsDefinition = z.infer<typeof ColorsDefinitionSchema>
14
+
15
+ export const localizedStringSchema = z.union([
16
+ z.string(),
17
+ multiLangStringSchema,
18
+ ])
19
+ export type LocalizedString = z.infer<typeof localizedStringSchema>
20
+
21
+ export const linkRelSchema = z.string().refine(isLinkRel, 'Invalid link rel')
22
+ export type LinkRel = z.infer<typeof linkRelSchema>
23
+
24
+ export const linkDefinitionSchema = z.object({
25
+ title: localizedStringSchema,
26
+ href: z.string().url(),
27
+ rel: linkRelSchema.optional(),
28
+ })
29
+ export type LinkDefinition = z.infer<typeof linkDefinitionSchema>
30
+
31
+ /**
32
+ * Aesthetic customization
33
+ */
34
+ export const brandingConfigSchema = z.object({
35
+ name: z.string().optional(),
36
+ logo: z.string().optional(),
37
+ colors: ColorsDefinitionSchema.optional(),
38
+ links: z.array(linkDefinitionSchema).readonly().optional(),
39
+ })
40
+ export type BrandingConfig = z.infer<typeof brandingConfigSchema>
41
+
42
+ export const customizationSchema = z.object({
43
+ /**
44
+ * Available user domains that can be used to sign up. A non-empty array
45
+ * is required to enable the sign-up feature.
46
+ */
47
+ availableUserDomains: z.array(z.string()).optional(),
48
+ /**
49
+ * UI customizations
50
+ */
51
+ branding: brandingConfigSchema.optional(),
52
+ /**
53
+ * Is an invite code required to sign up?
54
+ */
55
+ inviteCodeRequired: z.boolean().optional(),
56
+ /**
57
+ * Enables hCaptcha during sign-up.
58
+ */
59
+ hcaptcha: hcaptchaConfigSchema.optional(),
60
+ })
61
+ export type Customization = z.infer<typeof customizationSchema>
62
+
63
+ export type CustomizationData = {
64
+ // Functional customization
65
+ hcaptchaSiteKey?: string
66
+ inviteCodeRequired?: boolean
67
+ availableUserDomains?: string[]
68
+
69
+ // Aesthetic customization
70
+ name?: string
71
+ logo?: string
72
+ links?: readonly LinkDefinition[]
73
+ }
74
+
75
+ export function buildCustomizationData({
76
+ branding,
77
+ availableUserDomains,
78
+ inviteCodeRequired,
79
+ hcaptcha,
80
+ }: Customization): CustomizationData {
81
+ // @NOTE the front end does not need colors here as they will be injected as
82
+ // CSS variables.
83
+ // @NOTE We only copy the values explicitly needed to avoid leaking sensitive
84
+ // data (in case the caller passed more than what we expect).
85
+ return {
86
+ availableUserDomains,
87
+ inviteCodeRequired,
88
+ hcaptchaSiteKey: hcaptcha?.siteKey,
89
+ name: branding?.name,
90
+ logo: branding?.logo,
91
+ links: branding?.links,
92
+ }
93
+ }
94
+
95
+ export function buildCustomizationCss({ branding }: Customization) {
96
+ const vars = Array.from(buildCustomizationVars(branding))
97
+ if (vars.length) return `:root { ${vars.join(' ')} }`
98
+
99
+ return ''
100
+ }
101
+
102
+ function* buildCustomizationVars(branding?: BrandingConfig) {
103
+ if (branding?.colors) {
104
+ for (const name of colorNames) {
105
+ const value = branding.colors[name]
106
+ if (!value) continue
107
+
108
+ // Skip undefined values
109
+ if (value === undefined) continue
110
+
111
+ const { r, g, b, a } = parseColor(value)
112
+
113
+ // Tailwind does not apply alpha values to base colors
114
+ if (a !== undefined) throw new TypeError('Alpha not supported')
115
+
116
+ const contrast = computeLuma({ r, g, b }) > 128 ? '0 0 0' : '255 255 255'
117
+
118
+ yield `--color-${name}: ${r} ${g} ${b};`
119
+ yield `--color-${name}-c: ${contrast};`
120
+ }
121
+ }
122
+ }
123
+
124
+ type RgbaColor = { r: number; g: number; b: number; a?: number }
125
+ function parseColor(color: unknown): RgbaColor {
126
+ if (typeof color !== 'string') {
127
+ throw new TypeError(`Invalid color value: ${typeof color}`)
128
+ }
129
+
130
+ if (color.startsWith('#')) {
131
+ if (color.length === 4 || color.length === 5) {
132
+ const r = parseUi8Hex(color.slice(1, 2))
133
+ const g = parseUi8Hex(color.slice(2, 3))
134
+ const b = parseUi8Hex(color.slice(3, 4))
135
+ const a = color.length > 4 ? parseUi8Hex(color.slice(4, 5)) : undefined
136
+ return { r, g, b, a }
137
+ }
138
+
139
+ if (color.length === 7 || color.length === 9) {
140
+ const r = parseUi8Hex(color.slice(1, 3))
141
+ const g = parseUi8Hex(color.slice(3, 5))
142
+ const b = parseUi8Hex(color.slice(5, 7))
143
+ const a = color.length > 8 ? parseUi8Hex(color.slice(7, 9)) : undefined
144
+ return { r, g, b, a }
145
+ }
146
+
147
+ throw new TypeError(`Invalid hex color: ${color}`)
148
+ }
149
+
150
+ const rgbMatch = color.match(
151
+ /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
152
+ )
153
+ if (rgbMatch) {
154
+ const r = parseUi8Dec(rgbMatch[1])
155
+ const g = parseUi8Dec(rgbMatch[2])
156
+ const b = parseUi8Dec(rgbMatch[3])
157
+ return { r, g, b }
158
+ }
159
+
160
+ const rgbaMatch = color.match(
161
+ /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
162
+ )
163
+ if (rgbaMatch) {
164
+ const r = parseUi8Dec(rgbaMatch[1])
165
+ const g = parseUi8Dec(rgbaMatch[2])
166
+ const b = parseUi8Dec(rgbaMatch[3])
167
+ const a = parseUi8Dec(rgbaMatch[4])
168
+ return { r, g, b, a }
169
+ }
170
+
171
+ throw new TypeError(`Unsupported color format: ${color}`)
172
+ }
173
+
174
+ function computeLuma({ r, g, b }: RgbaColor) {
175
+ return 0.299 * r + 0.587 * g + 0.114 * b
176
+ }
177
+
178
+ function parseUi8Hex(v: string) {
179
+ return asUi8(parseInt(v, 16))
180
+ }
181
+
182
+ function parseUi8Dec(v: string) {
183
+ return asUi8(parseInt(v, 10))
184
+ }
185
+
186
+ function asUi8(v: number) {
187
+ if (v >= 0 && v <= 255 && v === (v | 0)) return v
188
+ throw new TypeError(`Invalid color component: ${v}`)
189
+ }
@@ -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
  }
@@ -308,13 +308,13 @@ export class RequestManager {
308
308
 
309
309
  async get(
310
310
  uri: RequestUri,
311
- clientId: ClientId,
312
311
  deviceId: DeviceId,
312
+ clientId?: ClientId,
313
313
  ): Promise<RequestInfo> {
314
314
  const id = decodeRequestUri(uri)
315
315
 
316
316
  const data = await this.store.readRequest(id)
317
- if (!data) throw new InvalidRequestError(`Unknown request_uri "${uri}"`)
317
+ if (!data) throw new InvalidRequestError('Unknown request_uri')
318
318
 
319
319
  const updates: UpdateRequestData = {}
320
320
 
@@ -336,7 +336,7 @@ export class RequestManager {
336
336
  )
337
337
  }
338
338
 
339
- if (data.clientId !== clientId) {
339
+ if (clientId != null && data.clientId !== clientId) {
340
340
  throw new AccessDeniedError(
341
341
  data.parameters,
342
342
  'This request was initiated for another client',
@@ -380,7 +380,7 @@ export class RequestManager {
380
380
  const id = decodeRequestUri(uri)
381
381
 
382
382
  const data = await this.store.readRequest(id)
383
- if (!data) throw new InvalidRequestError(`Unknown request_uri "${uri}"`)
383
+ if (!data) throw new InvalidRequestError('Unknown request_uri')
384
384
 
385
385
  try {
386
386
  if (data.expiresAt < new Date()) {