@atproto/oauth-provider 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/account/account-manager.d.ts +7 -5
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js +34 -25
  5. package/dist/account/account-manager.js.map +1 -1
  6. package/dist/account/account-store.d.ts +7 -0
  7. package/dist/account/account-store.d.ts.map +1 -1
  8. package/dist/account/account-store.js.map +1 -1
  9. package/dist/account/account.d.ts +1 -11
  10. package/dist/account/account.d.ts.map +1 -1
  11. package/dist/account/{sign-up-data.d.ts → sign-up-input.d.ts} +3 -3
  12. package/dist/account/sign-up-input.d.ts.map +1 -0
  13. package/dist/account/{sign-up-data.js → sign-up-input.js} +3 -3
  14. package/dist/account/sign-up-input.js.map +1 -0
  15. package/dist/assets/assets-middleware.d.ts +2 -0
  16. package/dist/assets/assets-middleware.d.ts.map +1 -1
  17. package/dist/assets/assets-middleware.js +12 -14
  18. package/dist/assets/assets-middleware.js.map +1 -1
  19. package/dist/lib/csp/index.d.ts +5 -6
  20. package/dist/lib/csp/index.d.ts.map +1 -1
  21. package/dist/lib/csp/index.js +14 -11
  22. package/dist/lib/csp/index.js.map +1 -1
  23. package/dist/lib/hcaptcha.d.ts +5 -3
  24. package/dist/lib/hcaptcha.d.ts.map +1 -1
  25. package/dist/lib/hcaptcha.js +7 -4
  26. package/dist/lib/hcaptcha.js.map +1 -1
  27. package/dist/lib/html/build-document.d.ts +2 -2
  28. package/dist/lib/html/build-document.d.ts.map +1 -1
  29. package/dist/lib/html/build-document.js +11 -7
  30. package/dist/lib/html/build-document.js.map +1 -1
  31. package/dist/lib/html/html.d.ts.map +1 -1
  32. package/dist/lib/html/html.js +10 -13
  33. package/dist/lib/html/html.js.map +1 -1
  34. package/dist/lib/html/util.d.ts +0 -1
  35. package/dist/lib/html/util.d.ts.map +1 -1
  36. package/dist/lib/html/util.js +0 -4
  37. package/dist/lib/html/util.js.map +1 -1
  38. package/dist/lib/http/response.d.ts +3 -1
  39. package/dist/lib/http/response.d.ts.map +1 -1
  40. package/dist/lib/http/response.js +3 -0
  41. package/dist/lib/http/response.js.map +1 -1
  42. package/dist/lib/http/security-headers.d.ts +48 -0
  43. package/dist/lib/http/security-headers.d.ts.map +1 -0
  44. package/dist/lib/http/security-headers.js +62 -0
  45. package/dist/lib/http/security-headers.js.map +1 -0
  46. package/dist/lib/util/type.d.ts +8 -0
  47. package/dist/lib/util/type.d.ts.map +1 -1
  48. package/dist/lib/util/type.js.map +1 -1
  49. package/dist/oauth-hooks.d.ts +4 -25
  50. package/dist/oauth-hooks.d.ts.map +1 -1
  51. package/dist/oauth-provider.js +2 -2
  52. package/dist/oauth-provider.js.map +1 -1
  53. package/dist/output/backend-data.d.ts +4 -0
  54. package/dist/output/backend-data.d.ts.map +1 -0
  55. package/dist/output/backend-data.js +19 -0
  56. package/dist/output/backend-data.js.map +1 -0
  57. package/dist/output/build-authorize-data.d.ts +3 -19
  58. package/dist/output/build-authorize-data.d.ts.map +1 -1
  59. package/dist/output/build-authorize-data.js.map +1 -1
  60. package/dist/output/build-customization-data.d.ts +11 -18
  61. package/dist/output/build-customization-data.d.ts.map +1 -1
  62. package/dist/output/build-customization-data.js +1 -1
  63. package/dist/output/build-customization-data.js.map +1 -1
  64. package/dist/output/build-error-data.d.ts +3 -0
  65. package/dist/output/build-error-data.d.ts.map +1 -0
  66. package/dist/output/build-error-data.js +10 -0
  67. package/dist/output/build-error-data.js.map +1 -0
  68. package/dist/output/build-error-payload.d.ts +2 -1
  69. package/dist/output/build-error-payload.d.ts.map +1 -1
  70. package/dist/output/build-error-payload.js.map +1 -1
  71. package/dist/output/output-manager.d.ts +10 -4
  72. package/dist/output/output-manager.d.ts.map +1 -1
  73. package/dist/output/output-manager.js +68 -39
  74. package/dist/output/output-manager.js.map +1 -1
  75. package/dist/output/send-web-page.d.ts +6 -10
  76. package/dist/output/send-web-page.d.ts.map +1 -1
  77. package/dist/output/send-web-page.js +27 -47
  78. package/dist/output/send-web-page.js.map +1 -1
  79. package/dist/signer/signed-token-payload.d.ts +3 -3
  80. package/dist/signer/signer.d.ts +2 -2
  81. package/package.json +7 -40
  82. package/src/account/account-manager.ts +55 -34
  83. package/src/account/account-store.ts +8 -0
  84. package/src/account/account.ts +1 -14
  85. package/src/account/{sign-up-data.ts → sign-up-input.ts} +2 -2
  86. package/src/assets/assets-middleware.ts +11 -17
  87. package/src/lib/csp/index.ts +16 -13
  88. package/src/lib/hcaptcha.ts +10 -7
  89. package/src/lib/html/build-document.ts +15 -8
  90. package/src/lib/html/html.ts +11 -18
  91. package/src/lib/html/util.ts +0 -4
  92. package/src/lib/http/response.ts +9 -1
  93. package/src/lib/http/security-headers.ts +91 -0
  94. package/src/lib/util/type.ts +18 -0
  95. package/src/oauth-hooks.ts +4 -25
  96. package/src/oauth-provider.ts +2 -2
  97. package/src/output/backend-data.ts +18 -0
  98. package/src/output/build-authorize-data.ts +3 -26
  99. package/src/output/build-customization-data.ts +2 -13
  100. package/src/output/build-error-data.ts +8 -0
  101. package/src/output/build-error-payload.ts +4 -2
  102. package/src/output/output-manager.ts +86 -47
  103. package/src/output/send-web-page.ts +29 -58
  104. package/tsconfig.backend.json +1 -2
  105. package/tsconfig.backend.tsbuildinfo +1 -1
  106. package/tsconfig.json +1 -5
  107. package/.linguirc +0 -57
  108. package/dist/account/sign-up-data.d.ts.map +0 -1
  109. package/dist/account/sign-up-data.js.map +0 -1
  110. package/dist/assets/app/bundle-manifest.json +0 -614
  111. package/dist/assets/app/index-DZHZ9kCP.js +0 -36
  112. package/dist/assets/app/index-DZHZ9kCP.js.map +0 -1
  113. package/dist/assets/app/main-B_dNxQo_.js +0 -4
  114. package/dist/assets/app/main-B_dNxQo_.js.map +0 -1
  115. package/dist/assets/app/main-Dr6y26KY.css +0 -3
  116. package/dist/assets/app/main-Dr6y26KY.js +0 -306
  117. package/dist/assets/app/main-Dr6y26KY.js.map +0 -1
  118. package/dist/assets/app/messages-6_mYuGzB.js +0 -4
  119. package/dist/assets/app/messages-6_mYuGzB.js.map +0 -1
  120. package/dist/assets/app/messages-7wdeBTpD.js +0 -4
  121. package/dist/assets/app/messages-7wdeBTpD.js.map +0 -1
  122. package/dist/assets/app/messages-B-YFoWKc.js +0 -4
  123. package/dist/assets/app/messages-B-YFoWKc.js.map +0 -1
  124. package/dist/assets/app/messages-B10DUOE-.js +0 -4
  125. package/dist/assets/app/messages-B10DUOE-.js.map +0 -1
  126. package/dist/assets/app/messages-B4AwFEeZ.js +0 -4
  127. package/dist/assets/app/messages-B4AwFEeZ.js.map +0 -1
  128. package/dist/assets/app/messages-BDP8MyEC.js +0 -4
  129. package/dist/assets/app/messages-BDP8MyEC.js.map +0 -1
  130. package/dist/assets/app/messages-BIS87lxQ.js +0 -4
  131. package/dist/assets/app/messages-BIS87lxQ.js.map +0 -1
  132. package/dist/assets/app/messages-BI_Wbjdt.js +0 -4
  133. package/dist/assets/app/messages-BI_Wbjdt.js.map +0 -1
  134. package/dist/assets/app/messages-BMAouhRx.js +0 -4
  135. package/dist/assets/app/messages-BMAouhRx.js.map +0 -1
  136. package/dist/assets/app/messages-BdckMnJj.js +0 -4
  137. package/dist/assets/app/messages-BdckMnJj.js.map +0 -1
  138. package/dist/assets/app/messages-BgBLzc46.js +0 -4
  139. package/dist/assets/app/messages-BgBLzc46.js.map +0 -1
  140. package/dist/assets/app/messages-BobD78yK.js +0 -4
  141. package/dist/assets/app/messages-BobD78yK.js.map +0 -1
  142. package/dist/assets/app/messages-BtThT9UZ.js +0 -4
  143. package/dist/assets/app/messages-BtThT9UZ.js.map +0 -1
  144. package/dist/assets/app/messages-BwKHkbeh.js +0 -4
  145. package/dist/assets/app/messages-BwKHkbeh.js.map +0 -1
  146. package/dist/assets/app/messages-C417YUvA.js +0 -4
  147. package/dist/assets/app/messages-C417YUvA.js.map +0 -1
  148. package/dist/assets/app/messages-C4CxO4bO.js +0 -4
  149. package/dist/assets/app/messages-C4CxO4bO.js.map +0 -1
  150. package/dist/assets/app/messages-C5vd04e6.js +0 -4
  151. package/dist/assets/app/messages-C5vd04e6.js.map +0 -1
  152. package/dist/assets/app/messages-CAri2Wnz.js +0 -4
  153. package/dist/assets/app/messages-CAri2Wnz.js.map +0 -1
  154. package/dist/assets/app/messages-CPtWTZeG.js +0 -4
  155. package/dist/assets/app/messages-CPtWTZeG.js.map +0 -1
  156. package/dist/assets/app/messages-CiaM5zm8.js +0 -4
  157. package/dist/assets/app/messages-CiaM5zm8.js.map +0 -1
  158. package/dist/assets/app/messages-CkL-L2R6.js +0 -4
  159. package/dist/assets/app/messages-CkL-L2R6.js.map +0 -1
  160. package/dist/assets/app/messages-Cy_4XLNe.js +0 -4
  161. package/dist/assets/app/messages-Cy_4XLNe.js.map +0 -1
  162. package/dist/assets/app/messages-D5_ad-Eo.js +0 -4
  163. package/dist/assets/app/messages-D5_ad-Eo.js.map +0 -1
  164. package/dist/assets/app/messages-DChMl9mT.js +0 -4
  165. package/dist/assets/app/messages-DChMl9mT.js.map +0 -1
  166. package/dist/assets/app/messages-DWX-DIfv.js +0 -4
  167. package/dist/assets/app/messages-DWX-DIfv.js.map +0 -1
  168. package/dist/assets/app/messages-DgfsOphe.js +0 -4
  169. package/dist/assets/app/messages-DgfsOphe.js.map +0 -1
  170. package/dist/assets/app/messages-Dj5B_DR6.js +0 -4
  171. package/dist/assets/app/messages-Dj5B_DR6.js.map +0 -1
  172. package/dist/assets/app/messages-Dwzqo4eA.js +0 -4
  173. package/dist/assets/app/messages-Dwzqo4eA.js.map +0 -1
  174. package/dist/assets/app/messages-ESCIXJR7.js +0 -4
  175. package/dist/assets/app/messages-ESCIXJR7.js.map +0 -1
  176. package/dist/assets/app/messages-dglB2edb.js +0 -4
  177. package/dist/assets/app/messages-dglB2edb.js.map +0 -1
  178. package/dist/assets/app/messages-e_ClRrWc.js +0 -4
  179. package/dist/assets/app/messages-e_ClRrWc.js.map +0 -1
  180. package/dist/assets/app/messages-evvDxmrP.js +0 -4
  181. package/dist/assets/app/messages-evvDxmrP.js.map +0 -1
  182. package/dist/assets/app/messages-pPbdLb5B.js +0 -4
  183. package/dist/assets/app/messages-pPbdLb5B.js.map +0 -1
  184. package/dist/assets/app/messages-tJv8gHL2.js +0 -4
  185. package/dist/assets/app/messages-tJv8gHL2.js.map +0 -1
  186. package/dist/assets/app/messages-vLRVEw96.js +0 -4
  187. package/dist/assets/app/messages-vLRVEw96.js.map +0 -1
  188. package/dist/assets/asset.d.ts +0 -9
  189. package/dist/assets/asset.d.ts.map +0 -1
  190. package/dist/assets/asset.js +0 -3
  191. package/dist/assets/asset.js.map +0 -1
  192. package/dist/assets/index.d.ts +0 -5
  193. package/dist/assets/index.d.ts.map +0 -1
  194. package/dist/assets/index.js +0 -78
  195. package/dist/assets/index.js.map +0 -1
  196. package/rollup.config.js +0 -98
  197. package/src/assets/app/app.tsx +0 -43
  198. package/src/assets/app/backend-data.ts +0 -27
  199. package/src/assets/app/backend-types.ts +0 -66
  200. package/src/assets/app/components/forms/button-toggle-visibility.tsx +0 -43
  201. package/src/assets/app/components/forms/button.tsx +0 -60
  202. package/src/assets/app/components/forms/fieldset.tsx +0 -55
  203. package/src/assets/app/components/forms/form-card-async.tsx +0 -103
  204. package/src/assets/app/components/forms/form-card.tsx +0 -49
  205. package/src/assets/app/components/forms/input-checkbox.tsx +0 -78
  206. package/src/assets/app/components/forms/input-container.tsx +0 -107
  207. package/src/assets/app/components/forms/input-email-address.tsx +0 -65
  208. package/src/assets/app/components/forms/input-new-password.tsx +0 -62
  209. package/src/assets/app/components/forms/input-password.tsx +0 -87
  210. package/src/assets/app/components/forms/input-text.tsx +0 -82
  211. package/src/assets/app/components/forms/input-token.tsx +0 -94
  212. package/src/assets/app/components/forms/wizard-card.tsx +0 -116
  213. package/src/assets/app/components/layouts/layout-title-page.tsx +0 -77
  214. package/src/assets/app/components/layouts/layout-welcome.tsx +0 -73
  215. package/src/assets/app/components/utils/account-identifier.tsx +0 -23
  216. package/src/assets/app/components/utils/account-image.tsx +0 -33
  217. package/src/assets/app/components/utils/admonition.tsx +0 -52
  218. package/src/assets/app/components/utils/client-name.tsx +0 -45
  219. package/src/assets/app/components/utils/error-card.tsx +0 -93
  220. package/src/assets/app/components/utils/error-message.tsx +0 -88
  221. package/src/assets/app/components/utils/help-card.tsx +0 -46
  222. package/src/assets/app/components/utils/icons.tsx +0 -88
  223. package/src/assets/app/components/utils/link-anchor.tsx +0 -28
  224. package/src/assets/app/components/utils/link-title.tsx +0 -26
  225. package/src/assets/app/components/utils/multi-lang-string.tsx +0 -56
  226. package/src/assets/app/components/utils/password-strength-label.tsx +0 -37
  227. package/src/assets/app/components/utils/password-strength-meter.tsx +0 -58
  228. package/src/assets/app/components/utils/url-viewer.tsx +0 -73
  229. package/src/assets/app/cookies.ts +0 -11
  230. package/src/assets/app/hooks/use-api.ts +0 -178
  231. package/src/assets/app/hooks/use-async-action.ts +0 -120
  232. package/src/assets/app/hooks/use-bound-dispatch.ts +0 -5
  233. package/src/assets/app/hooks/use-browser-color-scheme.ts +0 -31
  234. package/src/assets/app/hooks/use-csrf-token.ts +0 -5
  235. package/src/assets/app/hooks/use-random-string.ts +0 -37
  236. package/src/assets/app/hooks/use-stepper.ts +0 -87
  237. package/src/assets/app/index.html +0 -182
  238. package/src/assets/app/lib/api.ts +0 -289
  239. package/src/assets/app/lib/clsx.ts +0 -6
  240. package/src/assets/app/lib/json-client.ts +0 -94
  241. package/src/assets/app/lib/password.ts +0 -98
  242. package/src/assets/app/lib/ref.ts +0 -17
  243. package/src/assets/app/lib/util.ts +0 -13
  244. package/src/assets/app/locales/an/messages.po +0 -490
  245. package/src/assets/app/locales/ast/messages.po +0 -490
  246. package/src/assets/app/locales/ca/messages.po +0 -490
  247. package/src/assets/app/locales/da/messages.po +0 -490
  248. package/src/assets/app/locales/de/messages.po +0 -490
  249. package/src/assets/app/locales/el/messages.po +0 -490
  250. package/src/assets/app/locales/en/messages.po +0 -490
  251. package/src/assets/app/locales/en-GB/messages.po +0 -490
  252. package/src/assets/app/locales/es/messages.po +0 -490
  253. package/src/assets/app/locales/eu/messages.po +0 -490
  254. package/src/assets/app/locales/fi/messages.po +0 -490
  255. package/src/assets/app/locales/fr/messages.po +0 -490
  256. package/src/assets/app/locales/ga/messages.po +0 -490
  257. package/src/assets/app/locales/gl/messages.po +0 -490
  258. package/src/assets/app/locales/hi/messages.po +0 -490
  259. package/src/assets/app/locales/hu/messages.po +0 -490
  260. package/src/assets/app/locales/ia/messages.po +0 -490
  261. package/src/assets/app/locales/id/messages.po +0 -490
  262. package/src/assets/app/locales/it/messages.po +0 -490
  263. package/src/assets/app/locales/ja/messages.po +0 -490
  264. package/src/assets/app/locales/km/messages.po +0 -490
  265. package/src/assets/app/locales/ko/messages.po +0 -490
  266. package/src/assets/app/locales/load.ts +0 -8
  267. package/src/assets/app/locales/locale-context.ts +0 -19
  268. package/src/assets/app/locales/locale-provider.tsx +0 -112
  269. package/src/assets/app/locales/locale-selector.tsx +0 -58
  270. package/src/assets/app/locales/locales.ts +0 -168
  271. package/src/assets/app/locales/ne/messages.po +0 -490
  272. package/src/assets/app/locales/nl/messages.po +0 -490
  273. package/src/assets/app/locales/pl/messages.po +0 -490
  274. package/src/assets/app/locales/pt-BR/messages.po +0 -490
  275. package/src/assets/app/locales/ro/messages.po +0 -490
  276. package/src/assets/app/locales/ru/messages.po +0 -490
  277. package/src/assets/app/locales/sv/messages.po +0 -490
  278. package/src/assets/app/locales/th/messages.po +0 -490
  279. package/src/assets/app/locales/tr/messages.po +0 -490
  280. package/src/assets/app/locales/uk/messages.po +0 -490
  281. package/src/assets/app/locales/vi/messages.po +0 -490
  282. package/src/assets/app/locales/zh-CN/messages.po +0 -490
  283. package/src/assets/app/locales/zh-HK/messages.po +0 -490
  284. package/src/assets/app/locales/zh-TW/messages.po +0 -490
  285. package/src/assets/app/main.css +0 -33
  286. package/src/assets/app/main.tsx +0 -44
  287. package/src/assets/app/views/authorize/accept/accept-form.tsx +0 -150
  288. package/src/assets/app/views/authorize/accept/accept-view.tsx +0 -70
  289. package/src/assets/app/views/authorize/authorize-view.tsx +0 -180
  290. package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +0 -88
  291. package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +0 -80
  292. package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +0 -127
  293. package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +0 -242
  294. package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +0 -116
  295. package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +0 -145
  296. package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +0 -142
  297. package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +0 -51
  298. package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +0 -287
  299. package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +0 -108
  300. package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +0 -158
  301. package/src/assets/app/views/authorize/welcome/welcome-view.tsx +0 -56
  302. package/src/assets/app/views/error/error-view.tsx +0 -31
  303. package/src/assets/asset.ts +0 -9
  304. package/src/assets/index.ts +0 -86
  305. package/tailwind.config.js +0 -31
  306. package/tsconfig.frontend.json +0 -11
  307. package/tsconfig.frontend.tsbuildinfo +0 -1
  308. package/tsconfig.tools.json +0 -8
  309. package/tsconfig.tools.tsbuildinfo +0 -1
  310. package/vite.config.mjs +0 -16
@@ -1,33 +1,27 @@
1
+ import { assets } from '@atproto/oauth-provider-ui'
1
2
  import {
2
3
  Middleware,
3
4
  validateFetchDest,
4
5
  validateFetchSite,
5
6
  writeStream,
6
7
  } from '../lib/http/index.js'
7
- import { Asset } from './asset.js'
8
- import { ASSETS_URL_PREFIX, getAsset } from './index.js'
8
+
9
+ export const ASSETS_URL_PREFIX = '/@atproto/oauth-provider/~assets/'
10
+
11
+ export function buildAssetUrl(filename: string): string {
12
+ return `${ASSETS_URL_PREFIX}${encodeURIComponent(filename)}`
13
+ }
9
14
 
10
15
  export function authorizeAssetsMiddleware(): Middleware {
11
16
  return async function assetsMiddleware(req, res, next): Promise<void> {
12
17
  if (req.method !== 'GET' && req.method !== 'HEAD') return next()
13
18
  if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
14
19
 
15
- const [pathname, query] = req.url.split('?', 2) as [
16
- string,
17
- string | undefined,
18
- ]
19
- if (query) return next()
20
-
21
- const filename = pathname.slice(ASSETS_URL_PREFIX.length)
20
+ const filename = req.url.slice(ASSETS_URL_PREFIX.length)
22
21
  if (!filename) return next()
23
22
 
24
- let asset: Asset
25
- try {
26
- asset = getAsset(filename)
27
- } catch {
28
- // Filename not found or not valid
29
- return next()
30
- }
23
+ const asset = assets.get(filename)
24
+ if (!asset) return next()
31
25
 
32
26
  try {
33
27
  // Allow "null" (ie. no header) to allow loading assets outside of a
@@ -45,6 +39,6 @@ export function authorizeAssetsMiddleware(): Middleware {
45
39
  res.setHeader('ETag', asset.sha256)
46
40
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
47
41
 
48
- writeStream(res, asset.createStream(), { contentType: asset.type })
42
+ writeStream(res, asset.stream(), { contentType: asset.mime })
49
43
  }
50
44
  }
@@ -1,7 +1,8 @@
1
- import { Simplify } from '../util/type.js'
1
+ import { CombinedTuple, Simplify } from '../util/type.js'
2
2
 
3
3
  export type CspValue =
4
4
  | `data:`
5
+ | `http:${string}`
5
6
  | `https:${string}`
6
7
  | `'none'`
7
8
  | `'self'`
@@ -35,7 +36,7 @@ export type CspConfig = Simplify<
35
36
  } & {
36
37
  [K in (typeof STRING_DIRECTIVES)[number]]?: CspValue
37
38
  } & {
38
- [K in (typeof ARRAY_DIRECTIVES)[number]]?: readonly CspValue[]
39
+ [K in (typeof ARRAY_DIRECTIVES)[number]]?: Iterable<CspValue>
39
40
  }
40
41
  >
41
42
 
@@ -53,25 +54,27 @@ export function buildCsp(config: CspConfig): string {
53
54
  }
54
55
 
55
56
  for (const name of ARRAY_DIRECTIVES) {
56
- if (config[name]?.length) values.push(`${name} ${config[name].join(' ')}`)
57
+ // Remove duplicate values by using a Set
58
+ const val = config[name] ? new Set(config[name]) : undefined
59
+ if (val?.size) values.push(`${name} ${Array.from(val).join(' ')}`)
57
60
  }
58
61
 
59
62
  return values.join('; ')
60
63
  }
61
64
 
62
- export function mergeCsp(a: CspConfig, b?: CspConfig): CspConfig
63
- export function mergeCsp(a: CspConfig | undefined, b: CspConfig): CspConfig
64
- export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined
65
- export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined {
66
- if (!a) return b
67
- if (!b) return a
65
+ export function mergeCsp<C extends (CspConfig | null | undefined)[]>(
66
+ ...configs: C
67
+ ) {
68
+ return configs.filter((v) => v != null).reduce(combineCsp) as CombinedTuple<C>
69
+ }
68
70
 
71
+ export function combineCsp(a: CspConfig, b: CspConfig): CspConfig {
69
72
  const result: CspConfig = {}
70
73
 
71
74
  for (const name of BOOLEAN_DIRECTIVES) {
72
- if (a[name] || b[name]) {
73
- result[name] = true
74
- }
75
+ // @NOTE b (if defined) takes precedence
76
+ const value = b[name] ?? a[name]
77
+ if (value != null) result[name] = value
75
78
  }
76
79
 
77
80
  for (const name of STRING_DIRECTIVES) {
@@ -90,7 +93,7 @@ export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined {
90
93
  if (set.size > 1 && set.has(NONE)) set.delete(NONE)
91
94
  result[name] = [...set]
92
95
  } else if (a[name] || b[name]) {
93
- result[name] = Array.from((a[name] || b[name])!)
96
+ result[name] = a[name] || b[name]
94
97
  }
95
98
  }
96
99
 
@@ -27,9 +27,11 @@ export const hcaptchaConfigSchema = z.object({
27
27
  */
28
28
  tokenSalt: z.string().min(1),
29
29
  /**
30
- * The risk score over which the user is considered a threat and will be
30
+ * The risk score above which the user is considered a threat and will be
31
31
  * denied access. This will be ignored if the enterprise features are not
32
32
  * available.
33
+ *
34
+ * Note: Score values ranges from 0.0 (no risk) to 1.0 (confirmed threat).
33
35
  */
34
36
  scoreThreshold: z.number().optional(),
35
37
  })
@@ -128,7 +130,7 @@ export class HCaptchaClient {
128
130
  this.fetch = bindFetch(fetch)
129
131
  }
130
132
 
131
- async verify(
133
+ public async verify(
132
134
  behaviorType: 'login' | 'signup',
133
135
  response: string,
134
136
  remoteip: string,
@@ -160,20 +162,21 @@ export class HCaptchaClient {
160
162
  }
161
163
  }
162
164
 
163
- isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
165
+ protected isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
164
166
  return (
165
167
  success &&
166
168
  // Fool-proofing: If this is false, the user is trying to use a token
167
169
  // generated for the same siteKey, but on another domain.
168
170
  hostname === this.hostname &&
169
171
  // Ignore if enterprise feature is not enabled
170
- score != null &&
171
- this.config.scoreThreshold != null &&
172
- score < this.config.scoreThreshold
172
+ (score == null ||
173
+ // Ignore if disabled through config
174
+ this.config.scoreThreshold == null ||
175
+ score < this.config.scoreThreshold)
173
176
  )
174
177
  }
175
178
 
176
- hashToken(value: string) {
179
+ protected hashToken(value: string) {
177
180
  const hash = createHash('sha256')
178
181
  hash.update(this.config.tokenSalt)
179
182
  hash.update(value)
@@ -4,7 +4,6 @@ import { html } from './tags.js'
4
4
 
5
5
  export type AssetRef = {
6
6
  url: string
7
- sha256: string
8
7
  }
9
8
 
10
9
  export type Attrs = Record<string, boolean | string | undefined>
@@ -59,6 +58,7 @@ export type BuildDocumentOptions = {
59
58
  base?: URL
60
59
  meta?: readonly MetaAttrs[]
61
60
  links?: readonly LinkAttrs[]
61
+ preloads?: readonly AssetRef[]
62
62
  head?: HtmlValue
63
63
  title?: HtmlValue
64
64
  scripts?: readonly (Html | AssetRef)[]
@@ -76,6 +76,7 @@ export const buildDocument = ({
76
76
  base,
77
77
  meta,
78
78
  links,
79
+ preloads,
79
80
  scripts,
80
81
  styles,
81
82
  }: BuildDocumentOptions) => html`<!doctype html>
@@ -86,8 +87,7 @@ export const buildDocument = ({
86
87
  ${base && html`<base href="${base.href}" />`}
87
88
  ${meta?.some(isViewportMeta) ? null : defaultViewport}
88
89
  ${meta?.map(metaToHtml)}
89
- ${styles?.map(linkPreload('style'))}
90
- ${scripts?.map(linkPreload('script'))}
90
+ ${preloads?.map(linkPreload)}
91
91
  ${links?.map(linkToHtml)}
92
92
  ${head}
93
93
  ${styles?.map(styleToHtml)}
@@ -120,11 +120,18 @@ function* attrsToHtml(attrs?: Attrs) {
120
120
  }
121
121
  }
122
122
 
123
- function linkPreload(as: 'script' | 'style') {
124
- return (style: Html | AssetRef) =>
125
- style instanceof Html
126
- ? undefined
127
- : html`<link rel="preload" href="${style.url}" as="${as}" />`
123
+ function linkPreload(asset: AssetRef) {
124
+ const [path] = asset.url.split('?', 2)
125
+
126
+ if (path.endsWith('.js')) {
127
+ return html`<link rel="modulepreload" href="${asset.url}" />`
128
+ }
129
+
130
+ if (path.endsWith('.css')) {
131
+ return html`<link rel="preload" href="${asset.url}" as="style" />`
132
+ }
133
+
134
+ return undefined
128
135
  }
129
136
 
130
137
  function scriptToHtml(script: Html | AssetRef) {
@@ -1,5 +1,3 @@
1
- import { isString } from './util'
2
-
3
1
  const symbol = Symbol('Html.dangerouslyCreate')
4
2
 
5
3
  /**
@@ -7,34 +5,29 @@ const symbol = Symbol('Html.dangerouslyCreate')
7
5
  * or used as fragments to build a larger HTML document.
8
6
  */
9
7
  export class Html implements Iterable<string> {
10
- #fragments: Iterable<Html | string>
8
+ readonly #fragments: readonly (Html | string)[]
11
9
 
12
10
  private constructor(fragments: Iterable<Html | string>, guard: symbol) {
13
11
  if (guard !== symbol) {
14
- // Force developers to use `Html.dangerouslyCreate` to create an Html
12
+ // Forces developers to use `Html.dangerouslyCreate` to create an Html
15
13
  // instance, to make it clear that the content needs to be trusted.
16
14
  throw new TypeError(
17
15
  'Use Html.dangerouslyCreate() to create an Html instance',
18
16
  )
19
17
  }
20
18
 
21
- this.#fragments = fragments
19
+ // Transform into an array in case iterable can be consumed only once
20
+ // (e.g. a generator function).
21
+ this.#fragments = Array.from(fragments)
22
22
  }
23
23
 
24
24
  toString(): string {
25
- let result = ''
26
- for (const fragment of this) result += fragment
27
-
28
- // Cache result for future calls
29
- if (
30
- !Array.isArray(this.#fragments) ||
31
- this.#fragments.length > 1 ||
32
- !this.#fragments.every(isString)
33
- ) {
34
- this.#fragments = result ? [result] : []
35
- }
36
-
37
- return result
25
+ // More efficient than `return this.#fragments.join('')` because it avoids
26
+ // creating intermediate strings when items of this.#fragments are Html
27
+ // instances (as all their toString() would end-up being called, creating
28
+ // lots of intermediary strings). The approach here allows to do a full scan
29
+ // of all the child nodes and concatenate them in a single pass.
30
+ return Array.from(this).join('')
38
31
  }
39
32
 
40
33
  [Symbol.toPrimitive](hint): string {
@@ -15,7 +15,3 @@ export function* stringReplacer(
15
15
  }
16
16
  yield source.slice(previousIndex)
17
17
  }
18
-
19
- export function isString(value: unknown): value is string {
20
- return typeof value === 'string'
21
- }
@@ -1,5 +1,9 @@
1
1
  import type { ServerResponse } from 'node:http'
2
2
  import { type Readable, pipeline } from 'node:stream'
3
+ import {
4
+ SecurityHeadersOptions,
5
+ setSecurityHeaders,
6
+ } from './security-headers.js'
3
7
  import type { Handler, Middleware } from './types.js'
4
8
 
5
9
  export function appendHeader(
@@ -88,11 +92,15 @@ export function staticJsonMiddleware(
88
92
  }
89
93
  }
90
94
 
95
+ export type WriteHtmlOptions = WriteResponseOptions & SecurityHeadersOptions
96
+
91
97
  export function writeHtml(
92
98
  res: ServerResponse,
93
99
  html: Buffer | string,
94
- { contentType = 'text/html', ...options }: WriteResponseOptions = {},
100
+ { contentType = 'text/html', ...options }: WriteHtmlOptions = {},
95
101
  ): void {
102
+ // HTML pages should always be served with safety protection headers
103
+ setSecurityHeaders(res, options)
96
104
  writeBuffer(res, html, { ...options, contentType })
97
105
  }
98
106
 
@@ -0,0 +1,91 @@
1
+ import type { ServerResponse } from 'node:http'
2
+ import { type CspConfig, buildCsp } from '../csp/index.js'
3
+
4
+ /**
5
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy COEP on MDN}
6
+ */
7
+ export enum CrossOriginEmbedderPolicy {
8
+ unsafeNone = 'unsafe-none',
9
+ requireCorp = 'require-corp',
10
+ credentialless = 'credentialless',
11
+ }
12
+
13
+ /**
14
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy CORP on MDN}
15
+ */
16
+ export enum CrossOriginResourcePolicy {
17
+ sameSite = 'same-site',
18
+ sameOrigin = 'same-origin',
19
+ crossOrigin = 'cross-origin',
20
+ }
21
+
22
+ /**
23
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy COOP on MDN}
24
+ */
25
+ export enum CrossOriginOpenerPolicy {
26
+ unsafeNone = 'unsafe-none',
27
+ sameOriginAllowPopups = 'same-origin-allow-popups',
28
+ sameOrigin = 'same-origin',
29
+ noopenerAllowPopups = 'noopener-allow-popups',
30
+ }
31
+
32
+ export type HTTPStrictTransportSecurityConfig = {
33
+ maxAge: number
34
+ includeSubDomains?: boolean
35
+ preload?: boolean
36
+ }
37
+
38
+ export type SecurityHeadersOptions = {
39
+ /**
40
+ * Defaults to `default-src: 'none'`. Use an empty object to disable CSP.
41
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy CSP on MDN}
42
+ */
43
+ csp?: CspConfig
44
+ coep?: CrossOriginEmbedderPolicy
45
+ corp?: CrossOriginResourcePolicy
46
+ coop?: CrossOriginOpenerPolicy
47
+ /**
48
+ * Defaults to 2 years. Use `false` to disable HSTS.
49
+ */
50
+ hsts?: HTTPStrictTransportSecurityConfig | false
51
+ }
52
+
53
+ export function setSecurityHeaders(
54
+ res: ServerResponse,
55
+ {
56
+ csp = { 'default-src': ["'none'"] },
57
+ coep = CrossOriginEmbedderPolicy.requireCorp,
58
+ corp = CrossOriginResourcePolicy.sameOrigin,
59
+ coop = CrossOriginOpenerPolicy.sameOrigin,
60
+ hsts = { maxAge: 63072000 },
61
+ }: SecurityHeadersOptions,
62
+ ): void {
63
+ // @NOTE Never set CSP through http-equiv meta as not all directives will
64
+ // be honored. Always set it through the Content-Security-Policy header.
65
+ const cspString = buildCsp(csp)
66
+ if (cspString) {
67
+ res.setHeader('Content-Security-Policy', cspString)
68
+ }
69
+
70
+ res.setHeader('Cross-Origin-Embedder-Policy', coep)
71
+ res.setHeader('Cross-Origin-Resource-Policy', corp)
72
+ res.setHeader('Cross-Origin-Opener-Policy', coop)
73
+
74
+ if (hsts) {
75
+ res.setHeader('Strict-Transport-Security', buildHstsValue(hsts))
76
+ }
77
+
78
+ // @TODO: make these headers configurable (?)
79
+ res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
80
+ res.setHeader('Referrer-Policy', 'same-origin')
81
+ res.setHeader('X-Frame-Options', 'DENY')
82
+ res.setHeader('X-Content-Type-Options', 'nosniff')
83
+ res.setHeader('X-XSS-Protection', '0')
84
+ }
85
+
86
+ function buildHstsValue(config: HTTPStrictTransportSecurityConfig): string {
87
+ let value = `max-age=${config.maxAge}`
88
+ if (config.includeSubDomains) value += '; includeSubDomains'
89
+ if (config.preload) value += '; preload'
90
+ return value
91
+ }
@@ -9,6 +9,24 @@ export type Override<T, V> = Simplify<{
9
9
  }>
10
10
  export type Awaitable<T> = T | Promise<T>
11
11
 
12
+ /**
13
+ * Converts a tuple to the equivalent type of combining every item into a single
14
+ * one. If any of the item in the tuple is non nullish, the result will be non
15
+ * nullish.
16
+ */
17
+ export type CombinedTuple<T extends readonly unknown[]> = T extends []
18
+ ? undefined
19
+ : Exclude<
20
+ T[number],
21
+ // If any item in the tuple is never `null` (resp. `undefined`), exclude
22
+ // `null` (resp. `undefined`) from `T[number]`
23
+ {
24
+ [K in keyof T]-?:
25
+ | (null extends T[K] ? never : null)
26
+ | (undefined extends T[K] ? never : undefined)
27
+ }[keyof T]
28
+ >
29
+
12
30
  /**
13
31
  * Similar to {@link Required} but also ensures that all values are defined.
14
32
  */
@@ -7,7 +7,7 @@ import {
7
7
  } from '@atproto/oauth-types'
8
8
  import { Account } from './account/account.js'
9
9
  import { SignInData } from './account/sign-in-data.js'
10
- import { SignUpData } from './account/sign-up-data.js'
10
+ import { SignUpInput } from './account/sign-up-input.js'
11
11
  import { ClientAuth } from './client/client-auth.js'
12
12
  import { ClientId } from './client/client-id.js'
13
13
  import { ClientInfo } from './client/client-info.js'
@@ -17,7 +17,7 @@ import { HcaptchaConfig, HcaptchaVerifyResult } from './lib/hcaptcha.js'
17
17
  import { RequestMetadata } from './lib/http/request.js'
18
18
  import { Awaitable } from './lib/util/type.js'
19
19
  import { AccessDeniedError, OAuthError } from './oauth-errors.js'
20
- import { DeviceAccountInfo, DeviceId } from './oauth-store.js'
20
+ import { DeviceAccountInfo, DeviceId, SignUpData } from './oauth-store.js'
21
21
 
22
22
  // Make sure all types needed to implement the OAuthHooks are exported
23
23
  export {
@@ -42,6 +42,7 @@ export {
42
42
  type RequestMetadata,
43
43
  type SignInData,
44
44
  type SignUpData,
45
+ type SignUpInput,
45
46
  }
46
47
 
47
48
  export type OAuthHooks = {
@@ -71,36 +72,14 @@ export type OAuthHooks = {
71
72
  account: Account
72
73
  }) => Awaitable<undefined | OAuthAuthorizationDetails>
73
74
 
74
- /**
75
- * This hook is called whenever an hcaptcha challenge is verified
76
- * during sign-up (if hcaptcha is enabled).
77
- *
78
- * @throws {InvalidRequestError} to deny the sign-up
79
- */
80
- onSignupHcaptchaResult?: (data: {
81
- data: SignUpData
82
- /**
83
- * This indicates not only wether the hCaptcha challenge succeeded, but also
84
- * if the score was low enough according to the
85
- * {@link HcaptchaConfig.scoreThreshold}.
86
- *
87
- * @see {@link HCaptchaClient.isAllowed}
88
- */
89
- allowed: boolean
90
- result: HcaptchaVerifyResult
91
- deviceId: DeviceId
92
- deviceMetadata: RequestMetadata
93
- }) => Awaitable<void>
94
-
95
75
  /**
96
76
  * This hook is called when a user attempts to sign up, after every validation
97
77
  * has passed (including hcaptcha).
98
78
  */
99
79
  onSignupAttempt?: (data: {
100
- data: SignUpData
80
+ input: SignUpInput
101
81
  deviceId: DeviceId
102
82
  deviceMetadata: RequestMetadata
103
- hcaptchaResult?: HcaptchaVerifyResult
104
83
  }) => Awaitable<void>
105
84
 
106
85
  /**
@@ -45,7 +45,7 @@ import {
45
45
  } from './account/account-store.js'
46
46
  import { Account } from './account/account.js'
47
47
  import { signInDataSchema } from './account/sign-in-data.js'
48
- import { signUpDataSchema } from './account/sign-up-data.js'
48
+ import { signUpInputSchema } from './account/sign-up-input.js'
49
49
  import { authorizeAssetsMiddleware } from './assets/assets-middleware.js'
50
50
  import { ClientAuth, authJwkThumbprint } from './client/client-auth.js'
51
51
  import {
@@ -1588,7 +1588,7 @@ export class OAuthProvider extends OAuthVerifier {
1588
1588
 
1589
1589
  router.post(
1590
1590
  '/oauth/authorize/sign-up',
1591
- apiHandler(signUpDataSchema, async function (req, res, data, ctx) {
1591
+ apiHandler(signUpInputSchema, async function (req, res, data, ctx) {
1592
1592
  return server.signUp(ctx, data)
1593
1593
  }),
1594
1594
  )
@@ -0,0 +1,18 @@
1
+ import { Html, js } from '../lib/html/index.js'
2
+
3
+ export function declareBackendData(values: Record<string, unknown>): Html {
4
+ return Html.dangerouslyCreate(backendDataGenerator(values))
5
+ }
6
+
7
+ export function* backendDataGenerator(
8
+ values: Record<string, unknown>,
9
+ ): Generator<Html> {
10
+ for (const [key, val] of Object.entries(values)) {
11
+ yield js`window[${key}]=${val};`
12
+ }
13
+ // The script tag is removed after the data is assigned to the global
14
+ // variables to prevent other scripts from reading the values. The "app"
15
+ // script will read the global variable and then unset it. See
16
+ // `readBackendData()` in "src/assets/app/backend-data.ts".
17
+ yield js`document.currentScript.remove();`
18
+ }
@@ -1,7 +1,5 @@
1
- import {
2
- OAuthAuthorizationRequestParameters,
3
- OAuthClientMetadata,
4
- } from '@atproto/oauth-types'
1
+ import type { AuthorizeData, Session } from '@atproto/oauth-provider-api'
2
+ import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
5
3
  import { DeviceAccountInfo } from '../account/account-store.js'
6
4
  import { Account } from '../account/account.js'
7
5
  import { Client } from '../client/client.js'
@@ -30,28 +28,7 @@ export type AuthorizationResultAuthorize = {
30
28
  }
31
29
  }
32
30
 
33
- // TODO: find a way to share this type with the frontend code
34
- // (app/backend-types.ts)
35
-
36
- type Session = {
37
- account: Account
38
- info?: never // Prevent accidental leaks to frontend
39
-
40
- selected: boolean
41
- loginRequired: boolean
42
- consentRequired: boolean
43
- }
44
-
45
- export type AuthorizeData = {
46
- clientId: string
47
- clientMetadata: OAuthClientMetadata
48
- clientTrusted: boolean
49
- requestUri: string
50
- loginHint?: string
51
- scopeDetails?: ScopeDetail[]
52
- newSessionsRequireConsent: boolean
53
- sessions: Session[]
54
- }
31
+ export type { AuthorizeData, Session }
55
32
 
56
33
  export function buildAuthorizeData(
57
34
  data: AuthorizationResultAuthorize,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { CustomizationData } from '@atproto/oauth-provider-api'
2
3
  import { hcaptchaConfigSchema } from '../lib/hcaptcha.js'
3
4
  import { isLinkRel } from '../lib/html/build-document.js'
4
5
  import { multiLangStringSchema } from '../lib/locale.js'
@@ -59,7 +60,7 @@ export const brandingConfigSchema = z.object({
59
60
  name: z.string().optional(),
60
61
  logo: z.string().optional(),
61
62
  colors: colorsDefinitionSchema.optional(),
62
- links: z.array(linkDefinitionSchema).readonly().optional(),
63
+ links: z.array(linkDefinitionSchema).optional(),
63
64
  })
64
65
  export type BrandingInput = z.input<typeof brandingConfigSchema>
65
66
  export type Branding = z.infer<typeof brandingConfigSchema>
@@ -86,18 +87,6 @@ export const customizationSchema = z.object({
86
87
  export type CustomizationInput = z.input<typeof customizationSchema>
87
88
  export type Customization = z.infer<typeof customizationSchema>
88
89
 
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
90
  export function buildCustomizationData({
102
91
  branding,
103
92
  availableUserDomains,
@@ -0,0 +1,8 @@
1
+ import { ErrorData } from '@atproto/oauth-provider-api'
2
+ import { buildErrorPayload } from './build-error-payload.js'
3
+
4
+ // @NOTE: The primary role of this function is to ensure that the ErrorPayload
5
+ // and ErrorData types are in sync.
6
+ export function buildErrorData(error: unknown): ErrorData {
7
+ return buildErrorPayload(error)
8
+ }
@@ -50,10 +50,12 @@ export function buildErrorStatus(error: unknown): number {
50
50
  return 500
51
51
  }
52
52
 
53
- export function buildErrorPayload(error: unknown): {
53
+ export type ErrorPayload = {
54
54
  error: string
55
55
  error_description: string
56
- } {
56
+ }
57
+
58
+ export function buildErrorPayload(error: unknown): ErrorPayload {
57
59
  if (error instanceof OAuthError) {
58
60
  return error.toJSON()
59
61
  }