@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.
- package/.linguirc +57 -0
- package/CHANGELOG.md +29 -0
- package/dist/account/account-manager.d.ts +17 -3
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js +102 -8
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +81 -15
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +70 -19
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/sign-in-data.d.ts +28 -0
- package/dist/account/sign-in-data.d.ts.map +1 -0
- package/dist/account/sign-in-data.js +16 -0
- package/dist/account/sign-in-data.js.map +1 -0
- package/dist/account/sign-up-data.d.ts +26 -0
- package/dist/account/sign-up-data.d.ts.map +1 -0
- package/dist/account/sign-up-data.js +11 -0
- package/dist/account/sign-up-data.js.map +1 -0
- package/dist/assets/app/bundle-manifest.json +598 -6
- package/dist/assets/app/index-ItwwtJ8r.js +36 -0
- package/dist/assets/app/index-ItwwtJ8r.js.map +1 -0
- package/dist/assets/app/main-B_dNxQo_.js +4 -0
- package/dist/assets/app/main-B_dNxQo_.js.map +1 -0
- package/dist/assets/app/main-CSatvmRR.css +3 -0
- package/dist/assets/app/main-CSatvmRR.js +306 -0
- package/dist/assets/app/main-CSatvmRR.js.map +1 -0
- package/dist/assets/app/messages-BQeltXSF.js +4 -0
- package/dist/assets/app/messages-BQeltXSF.js.map +1 -0
- package/dist/assets/app/messages-BQkEhfjg.js +4 -0
- package/dist/assets/app/messages-BQkEhfjg.js.map +1 -0
- package/dist/assets/app/messages-BUjKj_UJ.js +4 -0
- package/dist/assets/app/messages-BUjKj_UJ.js.map +1 -0
- package/dist/assets/app/messages-BWIQa8fO.js +4 -0
- package/dist/assets/app/messages-BWIQa8fO.js.map +1 -0
- package/dist/assets/app/messages-BaNVb0bp.js +4 -0
- package/dist/assets/app/messages-BaNVb0bp.js.map +1 -0
- package/dist/assets/app/messages-BaizVXcF.js +4 -0
- package/dist/assets/app/messages-BaizVXcF.js.map +1 -0
- package/dist/assets/app/messages-BfoClA1Y.js +4 -0
- package/dist/assets/app/messages-BfoClA1Y.js.map +1 -0
- package/dist/assets/app/messages-BsKGDZnC.js +4 -0
- package/dist/assets/app/messages-BsKGDZnC.js.map +1 -0
- package/dist/assets/app/messages-Bu-TJhml.js +4 -0
- package/dist/assets/app/messages-Bu-TJhml.js.map +1 -0
- package/dist/assets/app/messages-BvOKnBQk.js +4 -0
- package/dist/assets/app/messages-BvOKnBQk.js.map +1 -0
- package/dist/assets/app/messages-BxDzCiWz.js +4 -0
- package/dist/assets/app/messages-BxDzCiWz.js.map +1 -0
- package/dist/assets/app/messages-CDgFOy4S.js +4 -0
- package/dist/assets/app/messages-CDgFOy4S.js.map +1 -0
- package/dist/assets/app/messages-CLbTz0o9.js +4 -0
- package/dist/assets/app/messages-CLbTz0o9.js.map +1 -0
- package/dist/assets/app/messages-CNwSh0t7.js +4 -0
- package/dist/assets/app/messages-CNwSh0t7.js.map +1 -0
- package/dist/assets/app/messages-CSMNJ6P8.js +4 -0
- package/dist/assets/app/messages-CSMNJ6P8.js.map +1 -0
- package/dist/assets/app/messages-CZQUw3mp.js +4 -0
- package/dist/assets/app/messages-CZQUw3mp.js.map +1 -0
- package/dist/assets/app/messages-CZT41oVp.js +4 -0
- package/dist/assets/app/messages-CZT41oVp.js.map +1 -0
- package/dist/assets/app/messages-C_b-d3t8.js +4 -0
- package/dist/assets/app/messages-C_b-d3t8.js.map +1 -0
- package/dist/assets/app/messages-C_u3MTc2.js +4 -0
- package/dist/assets/app/messages-C_u3MTc2.js.map +1 -0
- package/dist/assets/app/messages-Cn8nHZic.js +4 -0
- package/dist/assets/app/messages-Cn8nHZic.js.map +1 -0
- package/dist/assets/app/messages-CtDywJUm.js +4 -0
- package/dist/assets/app/messages-CtDywJUm.js.map +1 -0
- package/dist/assets/app/messages-CurtIjBF.js +4 -0
- package/dist/assets/app/messages-CurtIjBF.js.map +1 -0
- package/dist/assets/app/messages-Cv6zIbaP.js +4 -0
- package/dist/assets/app/messages-Cv6zIbaP.js.map +1 -0
- package/dist/assets/app/messages-D1eLQuPE.js +4 -0
- package/dist/assets/app/messages-D1eLQuPE.js.map +1 -0
- package/dist/assets/app/messages-D8vHEaYW.js +4 -0
- package/dist/assets/app/messages-D8vHEaYW.js.map +1 -0
- package/dist/assets/app/messages-DJ1Q4GeC.js +4 -0
- package/dist/assets/app/messages-DJ1Q4GeC.js.map +1 -0
- package/dist/assets/app/messages-DRL3exqd.js +4 -0
- package/dist/assets/app/messages-DRL3exqd.js.map +1 -0
- package/dist/assets/app/messages-DWLPQRTp.js +4 -0
- package/dist/assets/app/messages-DWLPQRTp.js.map +1 -0
- package/dist/assets/app/messages-DjVaE9YE.js +4 -0
- package/dist/assets/app/messages-DjVaE9YE.js.map +1 -0
- package/dist/assets/app/messages-DqpMfFJR.js +4 -0
- package/dist/assets/app/messages-DqpMfFJR.js.map +1 -0
- package/dist/assets/app/messages-ETjhJBEN.js +4 -0
- package/dist/assets/app/messages-ETjhJBEN.js.map +1 -0
- package/dist/assets/app/messages-EUKrgrGn.js +4 -0
- package/dist/assets/app/messages-EUKrgrGn.js.map +1 -0
- package/dist/assets/app/messages-QQrOUcPW.js +4 -0
- package/dist/assets/app/messages-QQrOUcPW.js.map +1 -0
- package/dist/assets/app/messages-e2QGqFL6.js +4 -0
- package/dist/assets/app/messages-e2QGqFL6.js.map +1 -0
- package/dist/assets/app/messages-p61py7gD.js +4 -0
- package/dist/assets/app/messages-p61py7gD.js.map +1 -0
- package/dist/assets/asset.d.ts +1 -0
- package/dist/assets/asset.d.ts.map +1 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +12 -7
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/assets/index.d.ts +3 -2
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +13 -1
- package/dist/assets/index.js.map +1 -1
- package/dist/client/client-store.d.ts +3 -3
- package/dist/client/client-store.d.ts.map +1 -1
- package/dist/client/client-store.js +6 -5
- package/dist/client/client-store.js.map +1 -1
- package/dist/device/device-manager.d.ts +9 -8
- package/dist/device/device-manager.d.ts.map +1 -1
- package/dist/device/device-manager.js.map +1 -1
- package/dist/device/device-store.d.ts +3 -3
- package/dist/device/device-store.d.ts.map +1 -1
- package/dist/device/device-store.js +10 -9
- package/dist/device/device-store.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts +15 -7
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js +17 -3
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/dpop/dpop-nonce.d.ts +11 -5
- package/dist/dpop/dpop-nonce.d.ts.map +1 -1
- package/dist/dpop/dpop-nonce.js +47 -38
- package/dist/dpop/dpop-nonce.js.map +1 -1
- package/dist/errors/handle-unavailable-error.d.ts +11 -0
- package/dist/errors/handle-unavailable-error.d.ts.map +1 -0
- package/dist/errors/handle-unavailable-error.js +19 -0
- package/dist/errors/handle-unavailable-error.js.map +1 -0
- package/dist/errors/invalid-request-error.d.ts +6 -8
- package/dist/errors/invalid-request-error.d.ts.map +1 -1
- package/dist/errors/invalid-request-error.js +10 -8
- package/dist/errors/invalid-request-error.js.map +1 -1
- package/dist/lib/csp/index.d.ts +18 -0
- package/dist/lib/csp/index.d.ts.map +1 -0
- package/dist/lib/csp/index.js +72 -0
- package/dist/lib/csp/index.js.map +1 -0
- package/dist/lib/hcaptcha.d.ts +177 -0
- package/dist/lib/hcaptcha.d.ts.map +1 -0
- package/dist/lib/hcaptcha.js +155 -0
- package/dist/lib/hcaptcha.js.map +1 -0
- package/dist/lib/html/build-document.d.ts +11 -3
- package/dist/lib/html/build-document.d.ts.map +1 -1
- package/dist/lib/html/build-document.js +51 -15
- package/dist/lib/html/build-document.js.map +1 -1
- package/dist/lib/http/middleware.d.ts.map +1 -1
- package/dist/lib/http/middleware.js +4 -1
- package/dist/lib/http/middleware.js.map +1 -1
- package/dist/lib/http/request.d.ts +5 -2
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +16 -1
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/response.d.ts +4 -2
- package/dist/lib/http/response.d.ts.map +1 -1
- package/dist/lib/http/response.js +23 -5
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/locale.d.ts +15 -0
- package/dist/lib/locale.d.ts.map +1 -0
- package/dist/lib/locale.js +17 -0
- package/dist/lib/locale.js.map +1 -0
- package/dist/lib/util/function.d.ts +2 -2
- package/dist/lib/util/function.d.ts.map +1 -1
- package/dist/lib/util/function.js.map +1 -1
- package/dist/lib/util/type.d.ts +88 -1
- package/dist/lib/util/type.d.ts.map +1 -1
- package/dist/lib/util/type.js +41 -0
- package/dist/lib/util/type.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts +2 -2
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +60 -3
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js +3 -3
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts +23 -18
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +207 -204
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +1 -1
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js +2 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts +0 -1
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js +0 -1
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/output/build-customization-data.d.ts +241 -0
- package/dist/output/build-customization-data.d.ts.map +1 -0
- package/dist/output/build-customization-data.js +174 -0
- package/dist/output/build-customization-data.js.map +1 -0
- package/dist/output/output-manager.d.ts +16 -9
- package/dist/output/output-manager.d.ts.map +1 -1
- package/dist/output/output-manager.js +78 -42
- package/dist/output/output-manager.js.map +1 -1
- package/dist/output/send-authorize-redirect.d.ts +9 -6
- package/dist/output/send-authorize-redirect.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.js +20 -14
- package/dist/output/send-authorize-redirect.js.map +1 -1
- package/dist/output/send-web-page.d.ts +7 -2
- package/dist/output/send-web-page.d.ts.map +1 -1
- package/dist/output/send-web-page.js +37 -21
- package/dist/output/send-web-page.js.map +1 -1
- package/dist/request/request-manager.d.ts +1 -1
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +4 -4
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.d.ts +3 -3
- package/dist/request/request-store.d.ts.map +1 -1
- package/dist/request/request-store.js +11 -10
- package/dist/request/request-store.js.map +1 -1
- package/dist/token/token-store.d.ts +4 -4
- package/dist/token/token-store.d.ts.map +1 -1
- package/dist/token/token-store.js +13 -12
- package/dist/token/token-store.js.map +1 -1
- package/package.json +43 -20
- package/rollup.config.js +61 -17
- package/src/account/account-manager.ts +159 -8
- package/src/account/account-store.ts +127 -32
- package/src/account/sign-in-data.ts +15 -0
- package/src/account/sign-up-data.ts +11 -0
- package/src/assets/app/app.tsx +31 -16
- package/src/assets/app/backend-data.ts +15 -60
- package/src/assets/app/backend-types.ts +66 -0
- package/src/assets/app/components/forms/button-toggle-visibility.tsx +43 -0
- package/src/assets/app/components/forms/button.tsx +60 -0
- package/src/assets/app/components/forms/fieldset.tsx +55 -0
- package/src/assets/app/components/forms/form-card-async.tsx +103 -0
- package/src/assets/app/components/forms/form-card.tsx +49 -0
- package/src/assets/app/components/forms/input-checkbox.tsx +73 -0
- package/src/assets/app/components/forms/input-container.tsx +107 -0
- package/src/assets/app/components/forms/input-email-address.tsx +66 -0
- package/src/assets/app/components/forms/input-new-password.tsx +62 -0
- package/src/assets/app/components/forms/input-password.tsx +88 -0
- package/src/assets/app/components/forms/input-text.tsx +76 -0
- package/src/assets/app/components/forms/input-token.tsx +94 -0
- package/src/assets/app/components/forms/wizard-card.tsx +116 -0
- package/src/assets/app/components/layouts/layout-title-page.tsx +77 -0
- package/src/assets/app/components/layouts/layout-welcome.tsx +73 -0
- package/src/assets/app/components/utils/account-identifier.tsx +23 -0
- package/src/assets/app/components/utils/account-image.tsx +33 -0
- package/src/assets/app/components/utils/admonition.tsx +52 -0
- package/src/assets/app/components/utils/client-name.tsx +45 -0
- package/src/assets/app/components/utils/error-card.tsx +93 -0
- package/src/assets/app/components/utils/error-message.tsx +62 -0
- package/src/assets/app/components/utils/help-card.tsx +46 -0
- package/src/assets/app/components/utils/icons.tsx +88 -0
- package/src/assets/app/components/utils/link-anchor.tsx +28 -0
- package/src/assets/app/components/utils/link-title.tsx +26 -0
- package/src/assets/app/components/utils/multi-lang-string.tsx +56 -0
- package/src/assets/app/components/utils/password-strength-label.tsx +37 -0
- package/src/assets/app/components/utils/password-strength-meter.tsx +58 -0
- package/src/assets/app/components/{url-viewer.tsx → utils/url-viewer.tsx} +9 -6
- package/src/assets/app/hooks/use-api.ts +128 -55
- package/src/assets/app/hooks/use-async-action.ts +120 -0
- package/src/assets/app/hooks/use-browser-color-scheme.ts +31 -0
- package/src/assets/app/hooks/use-csrf-token.ts +1 -1
- package/src/assets/app/hooks/use-random-string.ts +37 -0
- package/src/assets/app/hooks/use-stepper.ts +87 -0
- package/src/assets/app/index.html +182 -0
- package/src/assets/app/lib/api.ts +248 -79
- package/src/assets/app/lib/clsx.ts +5 -8
- package/src/assets/app/lib/json-client.ts +94 -0
- package/src/assets/app/lib/password.ts +98 -0
- package/src/assets/app/lib/ref.ts +17 -0
- package/src/assets/app/locales/an/messages.po +492 -0
- package/src/assets/app/locales/ast/messages.po +492 -0
- package/src/assets/app/locales/ca/messages.po +492 -0
- package/src/assets/app/locales/da/messages.po +492 -0
- package/src/assets/app/locales/de/messages.po +492 -0
- package/src/assets/app/locales/el/messages.po +492 -0
- package/src/assets/app/locales/en/messages.po +492 -0
- package/src/assets/app/locales/en-GB/messages.po +492 -0
- package/src/assets/app/locales/es/messages.po +492 -0
- package/src/assets/app/locales/eu/messages.po +492 -0
- package/src/assets/app/locales/fi/messages.po +492 -0
- package/src/assets/app/locales/fr/messages.po +492 -0
- package/src/assets/app/locales/ga/messages.po +492 -0
- package/src/assets/app/locales/gl/messages.po +492 -0
- package/src/assets/app/locales/hi/messages.po +492 -0
- package/src/assets/app/locales/hu/messages.po +492 -0
- package/src/assets/app/locales/ia/messages.po +492 -0
- package/src/assets/app/locales/id/messages.po +492 -0
- package/src/assets/app/locales/it/messages.po +492 -0
- package/src/assets/app/locales/ja/messages.po +492 -0
- package/src/assets/app/locales/km/messages.po +492 -0
- package/src/assets/app/locales/ko/messages.po +492 -0
- package/src/assets/app/locales/load.ts +8 -0
- package/src/assets/app/locales/locale-context.ts +19 -0
- package/src/assets/app/locales/locale-provider.tsx +112 -0
- package/src/assets/app/locales/locale-selector.tsx +58 -0
- package/src/assets/app/locales/locales.ts +168 -0
- package/src/assets/app/locales/ne/messages.po +492 -0
- package/src/assets/app/locales/nl/messages.po +492 -0
- package/src/assets/app/locales/pl/messages.po +492 -0
- package/src/assets/app/locales/pt-BR/messages.po +492 -0
- package/src/assets/app/locales/ro/messages.po +492 -0
- package/src/assets/app/locales/ru/messages.po +492 -0
- package/src/assets/app/locales/sv/messages.po +492 -0
- package/src/assets/app/locales/th/messages.po +492 -0
- package/src/assets/app/locales/tr/messages.po +492 -0
- package/src/assets/app/locales/uk/messages.po +492 -0
- package/src/assets/app/locales/vi/messages.po +492 -0
- package/src/assets/app/locales/zh-CN/messages.po +492 -0
- package/src/assets/app/locales/zh-HK/messages.po +492 -0
- package/src/assets/app/locales/zh-TW/messages.po +492 -0
- package/src/assets/app/main.css +23 -2
- package/src/assets/app/main.tsx +24 -8
- package/src/assets/app/views/authorize/accept/accept-form.tsx +150 -0
- package/src/assets/app/views/authorize/accept/accept-view.tsx +70 -0
- package/src/assets/app/views/authorize/authorize-view.tsx +180 -0
- package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
- package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
- package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +127 -0
- package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +244 -0
- package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +116 -0
- package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +145 -0
- package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +140 -0
- package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
- package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +289 -0
- package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
- package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +158 -0
- package/src/assets/app/views/authorize/welcome/welcome-view.tsx +56 -0
- package/src/assets/app/views/error/error-view.tsx +31 -0
- package/src/assets/asset.ts +1 -0
- package/src/assets/assets-middleware.ts +13 -8
- package/src/assets/index.ts +15 -2
- package/src/client/client-store.ts +10 -12
- package/src/device/device-manager.ts +8 -12
- package/src/device/device-store.ts +9 -15
- package/src/dpop/dpop-manager.ts +20 -8
- package/src/dpop/dpop-nonce.ts +58 -40
- package/src/errors/handle-unavailable-error.ts +18 -0
- package/src/errors/invalid-request-error.ts +10 -8
- package/src/lib/csp/index.ts +98 -0
- package/src/lib/hcaptcha.ts +182 -0
- package/src/lib/html/build-document.ts +60 -16
- package/src/lib/http/middleware.ts +4 -3
- package/src/lib/http/request.ts +31 -1
- package/src/lib/http/response.ts +22 -9
- package/src/lib/locale.ts +21 -0
- package/src/lib/util/function.ts +0 -3
- package/src/lib/util/type.ts +130 -1
- package/src/metadata/build-metadata.ts +2 -1
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +69 -3
- package/src/oauth-provider.ts +403 -307
- package/src/oauth-verifier.ts +3 -1
- package/src/output/build-authorize-data.ts +1 -3
- package/src/output/build-customization-data.ts +228 -0
- package/src/output/output-manager.ts +111 -48
- package/src/output/send-authorize-redirect.ts +43 -36
- package/src/output/send-web-page.ts +40 -26
- package/src/request/request-manager.ts +4 -4
- package/src/request/request-store.ts +12 -16
- package/src/token/token-store.ts +14 -18
- package/tailwind.config.js +5 -0
- package/tsconfig.backend.tsbuildinfo +1 -1
- package/tsconfig.frontend.tsbuildinfo +1 -1
- package/tsconfig.tools.tsbuildinfo +1 -1
- package/vite.config.mjs +16 -0
- package/.postcssrc.yml +0 -3
- package/dist/assets/app/main.css +0 -3
- package/dist/assets/app/main.js +0 -20
- package/dist/assets/app/main.js.map +0 -1
- package/dist/output/customization.d.ts +0 -27
- package/dist/output/customization.d.ts.map +0 -1
- package/dist/output/customization.js +0 -88
- package/dist/output/customization.js.map +0 -1
- package/src/assets/app/components/accept-form.tsx +0 -137
- package/src/assets/app/components/account-identifier.tsx +0 -18
- package/src/assets/app/components/account-picker.tsx +0 -127
- package/src/assets/app/components/button.tsx +0 -34
- package/src/assets/app/components/client-name.tsx +0 -37
- package/src/assets/app/components/fieldset.tsx +0 -26
- package/src/assets/app/components/form-card.tsx +0 -47
- package/src/assets/app/components/help-card.tsx +0 -42
- package/src/assets/app/components/icons/alert-icon.tsx +0 -5
- package/src/assets/app/components/icons/at-symbol-icon.tsx +0 -5
- package/src/assets/app/components/icons/caret-right-icon.tsx +0 -5
- package/src/assets/app/components/icons/lock-icon.tsx +0 -5
- package/src/assets/app/components/icons/token-icon.tsx +0 -5
- package/src/assets/app/components/icons/util.tsx +0 -17
- package/src/assets/app/components/info-card.tsx +0 -45
- package/src/assets/app/components/input-checkbox.tsx +0 -47
- package/src/assets/app/components/input-container.tsx +0 -37
- package/src/assets/app/components/input-layout.tsx +0 -47
- package/src/assets/app/components/input-text.tsx +0 -69
- package/src/assets/app/components/layout-title-page.tsx +0 -60
- package/src/assets/app/components/layout-welcome.tsx +0 -74
- package/src/assets/app/components/sign-in-form.tsx +0 -337
- package/src/assets/app/components/sign-up-account-form.tsx +0 -194
- package/src/assets/app/components/sign-up-disclaimer.tsx +0 -44
- package/src/assets/app/views/accept-view.tsx +0 -55
- package/src/assets/app/views/authorize-view.tsx +0 -106
- package/src/assets/app/views/error-view.tsx +0 -36
- package/src/assets/app/views/sign-in-view.tsx +0 -111
- package/src/assets/app/views/sign-up-view.tsx +0 -86
- package/src/assets/app/views/welcome-view.tsx +0 -54
- package/src/output/customization.ts +0 -118
package/src/oauth-verifier.ts
CHANGED
@@ -94,8 +94,10 @@ export class OAuthVerifier {
|
|
94
94
|
: new ReplayStoreMemory(),
|
95
95
|
accessTokenType = AccessTokenType.jwt,
|
96
96
|
|
97
|
-
...
|
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-
|
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 {
|
4
|
-
import {
|
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 {
|
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
|
19
|
-
readonly
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
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.
|
53
|
-
jsAsset, // Last (to be able to read the "backend data" variables)
|
95
|
+
...this.scripts,
|
54
96
|
],
|
55
|
-
styles:
|
56
|
-
|
57
|
-
|
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(
|
67
|
-
|
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.
|
74
|
-
jsAsset, // Last (to be able to read the "backend data" variables)
|
117
|
+
...this.scripts,
|
75
118
|
],
|
76
|
-
styles:
|
77
|
-
|
78
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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:
|
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][] =
|
53
|
-
iss
|
54
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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-
|
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 &
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
67
|
-
|
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
|
}
|