@atproto/oauth-provider-ui 0.0.2

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 (208) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +7 -0
  3. package/CONTRIBUTING.md +6 -0
  4. package/LICENSE.txt +7 -0
  5. package/dist/assets/COdVzed-.css +3 -0
  6. package/dist/assets/COdVzed-.js +100 -0
  7. package/dist/assets/COdVzed-.js.map +1 -0
  8. package/dist/assets/Cqnfnbvc.js +6 -0
  9. package/dist/assets/Cqnfnbvc.js.map +1 -0
  10. package/dist/assets/bundle-manifest.json +630 -0
  11. package/dist/assets/error-view-Bu4y7Nd8.js +208 -0
  12. package/dist/assets/error-view-Bu4y7Nd8.js.map +1 -0
  13. package/dist/assets/index-DXlCRM6V.js +36 -0
  14. package/dist/assets/index-DXlCRM6V.js.map +1 -0
  15. package/dist/assets/messages-2GoTm2qL.js +4 -0
  16. package/dist/assets/messages-2GoTm2qL.js.map +1 -0
  17. package/dist/assets/messages-6Cn2Jbhw.js +4 -0
  18. package/dist/assets/messages-6Cn2Jbhw.js.map +1 -0
  19. package/dist/assets/messages-75hFgOK2.js +4 -0
  20. package/dist/assets/messages-75hFgOK2.js.map +1 -0
  21. package/dist/assets/messages-B3OK4k0O.js +4 -0
  22. package/dist/assets/messages-B3OK4k0O.js.map +1 -0
  23. package/dist/assets/messages-BNXlPzKV.js +4 -0
  24. package/dist/assets/messages-BNXlPzKV.js.map +1 -0
  25. package/dist/assets/messages-BUygB8mD.js +4 -0
  26. package/dist/assets/messages-BUygB8mD.js.map +1 -0
  27. package/dist/assets/messages-BVPPcwNr.js +4 -0
  28. package/dist/assets/messages-BVPPcwNr.js.map +1 -0
  29. package/dist/assets/messages-BbbWUQS8.js +4 -0
  30. package/dist/assets/messages-BbbWUQS8.js.map +1 -0
  31. package/dist/assets/messages-BibKCYyW.js +4 -0
  32. package/dist/assets/messages-BibKCYyW.js.map +1 -0
  33. package/dist/assets/messages-BlPrr9_7.js +4 -0
  34. package/dist/assets/messages-BlPrr9_7.js.map +1 -0
  35. package/dist/assets/messages-ByVCw40U.js +4 -0
  36. package/dist/assets/messages-ByVCw40U.js.map +1 -0
  37. package/dist/assets/messages-C5DU1neP.js +4 -0
  38. package/dist/assets/messages-C5DU1neP.js.map +1 -0
  39. package/dist/assets/messages-C6IgUtbX.js +4 -0
  40. package/dist/assets/messages-C6IgUtbX.js.map +1 -0
  41. package/dist/assets/messages-C92Zzt2o.js +4 -0
  42. package/dist/assets/messages-C92Zzt2o.js.map +1 -0
  43. package/dist/assets/messages-CGZqYT14.js +4 -0
  44. package/dist/assets/messages-CGZqYT14.js.map +1 -0
  45. package/dist/assets/messages-CGlsy4wt.js +4 -0
  46. package/dist/assets/messages-CGlsy4wt.js.map +1 -0
  47. package/dist/assets/messages-CPT1nd0u.js +4 -0
  48. package/dist/assets/messages-CPT1nd0u.js.map +1 -0
  49. package/dist/assets/messages-CTTdXyw_.js +4 -0
  50. package/dist/assets/messages-CTTdXyw_.js.map +1 -0
  51. package/dist/assets/messages-ChK_C_Pj.js +4 -0
  52. package/dist/assets/messages-ChK_C_Pj.js.map +1 -0
  53. package/dist/assets/messages-CjJbk7Uf.js +4 -0
  54. package/dist/assets/messages-CjJbk7Uf.js.map +1 -0
  55. package/dist/assets/messages-CoiLjLYO.js +4 -0
  56. package/dist/assets/messages-CoiLjLYO.js.map +1 -0
  57. package/dist/assets/messages-Cwx6B4Ti.js +4 -0
  58. package/dist/assets/messages-Cwx6B4Ti.js.map +1 -0
  59. package/dist/assets/messages-D0uXAp_H.js +4 -0
  60. package/dist/assets/messages-D0uXAp_H.js.map +1 -0
  61. package/dist/assets/messages-DG0_arU0.js +4 -0
  62. package/dist/assets/messages-DG0_arU0.js.map +1 -0
  63. package/dist/assets/messages-DOXFJh9K.js +4 -0
  64. package/dist/assets/messages-DOXFJh9K.js.map +1 -0
  65. package/dist/assets/messages-DPK7nOoC.js +4 -0
  66. package/dist/assets/messages-DPK7nOoC.js.map +1 -0
  67. package/dist/assets/messages-Duccgtu0.js +4 -0
  68. package/dist/assets/messages-Duccgtu0.js.map +1 -0
  69. package/dist/assets/messages-DxTqgsHq.js +4 -0
  70. package/dist/assets/messages-DxTqgsHq.js.map +1 -0
  71. package/dist/assets/messages-E5_lTg7A.js +4 -0
  72. package/dist/assets/messages-E5_lTg7A.js.map +1 -0
  73. package/dist/assets/messages-UhunAjh1.js +4 -0
  74. package/dist/assets/messages-UhunAjh1.js.map +1 -0
  75. package/dist/assets/messages-Xg_3YLGw.js +4 -0
  76. package/dist/assets/messages-Xg_3YLGw.js.map +1 -0
  77. package/dist/assets/messages-iliBQHY2.js +4 -0
  78. package/dist/assets/messages-iliBQHY2.js.map +1 -0
  79. package/dist/assets/messages-lRprpIl-.js +4 -0
  80. package/dist/assets/messages-lRprpIl-.js.map +1 -0
  81. package/dist/assets/messages-pbPHQbz1.js +4 -0
  82. package/dist/assets/messages-pbPHQbz1.js.map +1 -0
  83. package/dist/assets/messages-q-O7ZQGs.js +4 -0
  84. package/dist/assets/messages-q-O7ZQGs.js.map +1 -0
  85. package/dist/lib/index.d.ts +19 -0
  86. package/dist/lib/index.d.ts.map +1 -0
  87. package/dist/lib/index.js +47 -0
  88. package/dist/lib/index.js.map +1 -0
  89. package/dist/tsconfig.backend.tsbuildinfo +1 -0
  90. package/lib/index.ts +72 -0
  91. package/package.json +73 -0
  92. package/rollup.config.js +102 -0
  93. package/src/authorization-page.html +183 -0
  94. package/src/authorization-page.tsx +55 -0
  95. package/src/backend-data.ts +35 -0
  96. package/src/components/forms/button-toggle-visibility.tsx +43 -0
  97. package/src/components/forms/button.tsx +60 -0
  98. package/src/components/forms/fieldset.tsx +55 -0
  99. package/src/components/forms/form-card-async.tsx +103 -0
  100. package/src/components/forms/form-card.tsx +49 -0
  101. package/src/components/forms/input-checkbox.tsx +78 -0
  102. package/src/components/forms/input-container.tsx +107 -0
  103. package/src/components/forms/input-email-address.tsx +65 -0
  104. package/src/components/forms/input-new-password.tsx +62 -0
  105. package/src/components/forms/input-password.tsx +87 -0
  106. package/src/components/forms/input-text.tsx +82 -0
  107. package/src/components/forms/input-token.tsx +94 -0
  108. package/src/components/forms/wizard-card.tsx +116 -0
  109. package/src/components/layouts/layout-title-page.tsx +77 -0
  110. package/src/components/layouts/layout-welcome.tsx +73 -0
  111. package/src/components/utils/account-identifier.tsx +23 -0
  112. package/src/components/utils/account-image.tsx +33 -0
  113. package/src/components/utils/admonition.tsx +52 -0
  114. package/src/components/utils/client-name.tsx +45 -0
  115. package/src/components/utils/error-card.tsx +93 -0
  116. package/src/components/utils/error-message.tsx +88 -0
  117. package/src/components/utils/help-card.tsx +46 -0
  118. package/src/components/utils/icons.tsx +88 -0
  119. package/src/components/utils/link-anchor.tsx +28 -0
  120. package/src/components/utils/link-title.tsx +26 -0
  121. package/src/components/utils/multi-lang-string.tsx +56 -0
  122. package/src/components/utils/password-strength-label.tsx +37 -0
  123. package/src/components/utils/password-strength-meter.tsx +58 -0
  124. package/src/components/utils/url-viewer.tsx +73 -0
  125. package/src/cookies.ts +11 -0
  126. package/src/error-page.html +125 -0
  127. package/src/error-page.tsx +29 -0
  128. package/src/hooks/use-api.ts +182 -0
  129. package/src/hooks/use-async-action.ts +120 -0
  130. package/src/hooks/use-bound-dispatch.ts +5 -0
  131. package/src/hooks/use-browser-color-scheme.ts +31 -0
  132. package/src/hooks/use-csrf-token.ts +5 -0
  133. package/src/hooks/use-random-string.ts +37 -0
  134. package/src/hooks/use-stepper.ts +87 -0
  135. package/src/index.html +13 -0
  136. package/src/lib/api.ts +234 -0
  137. package/src/lib/backend-data.ts +6 -0
  138. package/src/lib/clsx.ts +6 -0
  139. package/src/lib/json-client.ts +97 -0
  140. package/src/lib/password.ts +98 -0
  141. package/src/lib/ref.ts +17 -0
  142. package/src/lib/util.ts +13 -0
  143. package/src/locales/an/messages.po +487 -0
  144. package/src/locales/ast/messages.po +487 -0
  145. package/src/locales/ca/messages.po +487 -0
  146. package/src/locales/da/messages.po +487 -0
  147. package/src/locales/de/messages.po +487 -0
  148. package/src/locales/el/messages.po +487 -0
  149. package/src/locales/en/messages.po +487 -0
  150. package/src/locales/en-GB/messages.po +487 -0
  151. package/src/locales/es/messages.po +487 -0
  152. package/src/locales/eu/messages.po +487 -0
  153. package/src/locales/fi/messages.po +487 -0
  154. package/src/locales/fr/messages.po +487 -0
  155. package/src/locales/ga/messages.po +487 -0
  156. package/src/locales/gl/messages.po +487 -0
  157. package/src/locales/hi/messages.po +487 -0
  158. package/src/locales/hu/messages.po +487 -0
  159. package/src/locales/ia/messages.po +487 -0
  160. package/src/locales/id/messages.po +487 -0
  161. package/src/locales/it/messages.po +487 -0
  162. package/src/locales/ja/messages.po +487 -0
  163. package/src/locales/km/messages.po +487 -0
  164. package/src/locales/ko/messages.po +487 -0
  165. package/src/locales/load.ts +8 -0
  166. package/src/locales/locale-context.ts +19 -0
  167. package/src/locales/locale-provider.tsx +112 -0
  168. package/src/locales/locale-selector.tsx +58 -0
  169. package/src/locales/locales.ts +168 -0
  170. package/src/locales/ne/messages.po +487 -0
  171. package/src/locales/nl/messages.po +487 -0
  172. package/src/locales/pl/messages.po +487 -0
  173. package/src/locales/pt-BR/messages.po +487 -0
  174. package/src/locales/ro/messages.po +487 -0
  175. package/src/locales/ru/messages.po +487 -0
  176. package/src/locales/sv/messages.po +487 -0
  177. package/src/locales/th/messages.po +487 -0
  178. package/src/locales/tr/messages.po +487 -0
  179. package/src/locales/uk/messages.po +487 -0
  180. package/src/locales/vi/messages.po +487 -0
  181. package/src/locales/zh-CN/messages.po +487 -0
  182. package/src/locales/zh-HK/messages.po +487 -0
  183. package/src/locales/zh-TW/messages.po +487 -0
  184. package/src/styles.css +33 -0
  185. package/src/views/authorize/accept/accept-form.tsx +150 -0
  186. package/src/views/authorize/accept/accept-view.tsx +70 -0
  187. package/src/views/authorize/authorize-view.tsx +183 -0
  188. package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  189. package/src/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  190. package/src/views/authorize/reset-password/reset-password-view.tsx +127 -0
  191. package/src/views/authorize/sign-in/sign-in-form.tsx +242 -0
  192. package/src/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  193. package/src/views/authorize/sign-in/sign-in-view.tsx +145 -0
  194. package/src/views/authorize/sign-up/sign-up-account-form.tsx +142 -0
  195. package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  196. package/src/views/authorize/sign-up/sign-up-handle-form.tsx +287 -0
  197. package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  198. package/src/views/authorize/sign-up/sign-up-view.tsx +158 -0
  199. package/src/views/authorize/welcome/welcome-view.tsx +56 -0
  200. package/src/views/error/error-view.tsx +31 -0
  201. package/tailwind.config.js +31 -0
  202. package/tsconfig.backend.json +8 -0
  203. package/tsconfig.frontend.json +10 -0
  204. package/tsconfig.frontend.tsbuildinfo +1 -0
  205. package/tsconfig.json +8 -0
  206. package/tsconfig.tools.json +8 -0
  207. package/tsconfig.tools.tsbuildinfo +1 -0
  208. package/vite.config.mjs +16 -0
package/lib/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ // This file allows exposing the result of the build of the `../src` folder as
2
+ // assets that can be used from a backend service. Specifically, the assets map
3
+ // bellow exposes both the asset items from their manifest, as well as a method
4
+ // to get the stream data (as a Readable stream) of the asset.
5
+
6
+ // When this library is used as a NodeJS dependency (typically from
7
+ // node_modules), the assets will simply read on disk from the node_modules
8
+ // directory. However, if this file is bundled (e.g. via rollup), the assets
9
+ // need to be copied to the bundler's output directory for the code bellow to
10
+ // work. Most bundlers support this (webpack, rollup, etc.) by re-writing `new
11
+ // URL('./path', import.meta.url)` calls to point to the correct output
12
+ // directory.
13
+
14
+ // However, that syntax only works in ESM modules. Atproto uses CJS modules, so,
15
+ // at the moment, the logic bellow is **not** compatible with bundlers. To allow
16
+ // bundling this file as a CJS module, the code bellow should not be dependent
17
+ // on reading the files on disk. This can be done by modifying the build system
18
+ // to embed the asset bytes directly into the bundle-manifest.json (see the
19
+ // `data` option of "@atproto-labs/rollup-plugin-bundle-manifest" in
20
+ // rollup.config.js).
21
+
22
+ // https://github.com/evanw/esbuild/issues/795
23
+ // https://www.npmjs.com/package/@web/rollup-plugin-import-meta-assets
24
+
25
+ // Note that the bundle-manifest -- being a JSON file -- can be imported
26
+ // directly without any special handling. This is because both bundlers and
27
+ // NodeJS, support JSON imports out of the box.
28
+
29
+ import { createReadStream } from 'node:fs'
30
+ import { join } from 'node:path'
31
+ import { Readable } from 'node:stream'
32
+ import type { ManifestItem } from '@atproto-labs/rollup-plugin-bundle-manifest'
33
+ // @ts-expect-error: This file is generated at build time
34
+ // eslint-disable-next-line import/no-unresolved
35
+ import bundleManifest from '../assets/bundle-manifest.json'
36
+
37
+ // @NOTE Not relying on ManifestItem to describe this type to avoid dependency
38
+ // of built code on '@atproto-labs/rollup-plugin-bundle-manifest'.
39
+ export type Asset =
40
+ | {
41
+ type: 'asset'
42
+ mime?: string
43
+ sha256: string
44
+ stream: () => Readable
45
+ }
46
+ | {
47
+ type: 'chunk'
48
+ mime: string
49
+ sha256: string
50
+ dynamicImports: string[]
51
+ isDynamicEntry: boolean
52
+ isEntry: boolean
53
+ isImplicitEntry: boolean
54
+ name: string
55
+ stream: () => Readable
56
+ }
57
+
58
+ export const assets = new Map<string, Asset>(
59
+ Object.entries<ManifestItem>(bundleManifest).map(
60
+ ([filename, { data, ...item }]) => {
61
+ const buffer = data ? Buffer.from(data, 'base64') : null
62
+ const stream = buffer
63
+ ? () => Readable.from(buffer)
64
+ : () =>
65
+ // ESM version:
66
+ // createReadStream(new URL(`../assets/${filename}`, import.meta.url))
67
+ // CJS version:
68
+ createReadStream(join(__dirname, '..', 'assets', filename))
69
+ return [filename, { ...item, stream }]
70
+ },
71
+ ),
72
+ )
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@atproto/oauth-provider-ui",
3
+ "version": "0.0.2",
4
+ "license": "MIT",
5
+ "description": "Sign-in & Sign-up UI for the @atproto/oauth-provider",
6
+ "homepage": "https://atproto.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/bluesky-social/atproto",
10
+ "directory": "packages/oauth/oauth-provider-ui"
11
+ },
12
+ "type": "commonjs",
13
+ "main": "dist/lib/index.js",
14
+ "types": "dist/lib/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/lib/index.d.ts",
18
+ "default": "./dist/lib/index.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "node": ">=18.7.0"
23
+ },
24
+ "devDependencies": {
25
+ "@hcaptcha/react-hcaptcha": "^1.11.2",
26
+ "@lingui/cli": "^5.2.0",
27
+ "@lingui/core": "^5.2.0",
28
+ "@lingui/react": "^5.2.0",
29
+ "@lingui/swc-plugin": "^5.4.0",
30
+ "@lingui/vite-plugin": "^5.2.0",
31
+ "@rollup/plugin-commonjs": "^28.0.2",
32
+ "@rollup/plugin-dynamic-import-vars": "^2.1.5",
33
+ "@rollup/plugin-node-resolve": "^16.0.0",
34
+ "@rollup/plugin-swc": "^0.4.0",
35
+ "@swc/core": "^1.10.18",
36
+ "@swc/helpers": "^0.5.15",
37
+ "@types/react": "^19.0.10",
38
+ "@types/react-dom": "^19.0.4",
39
+ "@vitejs/plugin-react-swc": "^3.8.0",
40
+ "@web/rollup-plugin-import-meta-assets": "^2.2.1",
41
+ "autoprefixer": "^10.4.17",
42
+ "postcss": "^8.4.38",
43
+ "react": "^19.0.0",
44
+ "react-dom": "^19.0.0",
45
+ "react-error-boundary": "^5.0.0",
46
+ "rollup": "^4.13.0",
47
+ "rollup-plugin-postcss": "^4.0.2",
48
+ "tailwindcss": "^3.4.3",
49
+ "typescript": "^5.6.3",
50
+ "vite": "^6.2.0",
51
+ "@atproto-labs/fetch": "0.2.2",
52
+ "@atproto-labs/rollup-plugin-bundle-manifest": "0.1.2",
53
+ "@atproto/oauth-provider-api": "0.0.1",
54
+ "@atproto/oauth-types": "0.2.4"
55
+ },
56
+ "postcss": {
57
+ "plugins": {
58
+ "tailwindcss": {},
59
+ "autoprefixer": {}
60
+ }
61
+ },
62
+ "scripts": {
63
+ "po:extract": "lingui extract --clean",
64
+ "po:compile": "lingui compile --typescript",
65
+ "prebuild:frontend": "pnpm run po:compile",
66
+ "build:frontend": "rollup --config rollup.config.js",
67
+ "build:backend": "tsc --build tsconfig.backend.json",
68
+ "dev:ui": "vite",
69
+ "dev:frontend": "rollup --config rollup.config.js --watch",
70
+ "dev:catalogs": "pnpm run po:extract --debounce 250 --watch > /dev/null",
71
+ "dev:messages": "pnpm run po:compile --debounce 500 --watch"
72
+ }
73
+ }
@@ -0,0 +1,102 @@
1
+ /* eslint-env node */
2
+
3
+ const { default: commonjs } = require('@rollup/plugin-commonjs')
4
+ const {
5
+ default: dynamicImportVars,
6
+ } = require('@rollup/plugin-dynamic-import-vars')
7
+ const { default: nodeResolve } = require('@rollup/plugin-node-resolve')
8
+ const { default: swc } = require('@rollup/plugin-swc')
9
+ const {
10
+ default: manifest,
11
+ } = require('@atproto-labs/rollup-plugin-bundle-manifest')
12
+ const postcss = ((m) => m.default || m)(require('rollup-plugin-postcss'))
13
+
14
+ /**
15
+ * @type {import('rollup').RollupOptionsFunction}
16
+ */
17
+ module.exports = (commandLineArguments) => {
18
+ const NODE_ENV =
19
+ process.env['NODE_ENV'] ??
20
+ (commandLineArguments.watch ? 'development' : 'production')
21
+
22
+ const devMode = NODE_ENV === 'development'
23
+
24
+ return {
25
+ input: [`src/authorization-page.tsx`, `src/error-page.tsx`],
26
+ output: {
27
+ manualChunks: undefined,
28
+ sourcemap: true,
29
+ dir: 'dist/assets',
30
+ format: 'module',
31
+ entryFileNames: devMode ? '[name]-[hash].js' : '[hash].js',
32
+ },
33
+ plugins: [
34
+ {
35
+ name: 'resolve-swc-helpers',
36
+ resolveId(src) {
37
+ // For some reason, "nodeResolve" doesn't resolve these:
38
+ if (src.startsWith('@swc/helpers/')) return require.resolve(src)
39
+ },
40
+ },
41
+ nodeResolve({
42
+ preferBuiltins: false,
43
+ browser: true,
44
+ exportConditions: ['browser', 'module', 'import', 'default'],
45
+ }),
46
+ commonjs(),
47
+ postcss({ config: true, extract: true, minimize: !devMode }),
48
+ swc({
49
+ swc: {
50
+ swcrc: false,
51
+ configFile: false,
52
+ sourceMaps: true,
53
+ minify: !devMode,
54
+ jsc: {
55
+ experimental: {
56
+ // @NOTE Because of the experimental nature of SWC plugins, A
57
+ // very particular version of @swc/core needs to be used. The
58
+ // link below allows to determine with version of @swc/core is
59
+ // compatible based on the version of @lingui/swc-plugin used
60
+ // (click on the swc_core version in the right column to see
61
+ // which version of the @swc/core is compatible)
62
+ //
63
+ // https://github.com/lingui/swc-plugin?tab=readme-ov-file#compatibility
64
+ plugins: [['@lingui/swc-plugin', {}]],
65
+ },
66
+ minify: {
67
+ compress: true,
68
+ mangle: true,
69
+ },
70
+ externalHelpers: true,
71
+ target: 'es2020',
72
+ parser: { syntax: 'typescript', tsx: true },
73
+ transform: {
74
+ useDefineForClassFields: true,
75
+ react: { runtime: 'automatic' },
76
+ optimizer: {
77
+ simplify: true,
78
+ globals: {
79
+ vars: {
80
+ 'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ }),
88
+ dynamicImportVars({ errorWhenNoFilesFound: true }),
89
+
90
+ // Change `data` to `true` to include assets data in the manifest,
91
+ // allowing for easier bundling of the backend code (eg. using esbuild) as
92
+ // bundlers know how to bundle JSON files but not how to bundle assets
93
+ // referenced at runtime.
94
+ manifest({ data: false }),
95
+ ],
96
+ onwarn(warning, warn) {
97
+ // 'use client' directives are fine
98
+ if (warning.code === 'MODULE_LEVEL_DIRECTIVE') return
99
+ warn(warning)
100
+ },
101
+ }
102
+ }
@@ -0,0 +1,183 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Mock - OAuth Provider</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script>
11
+ /*
12
+ * This file's purpose is to provide a way to develop the UI without
13
+ * running a full featured OAuth server. It mocks the server responses and
14
+ * provides configuration data to the UI.
15
+ *
16
+ * This file is not part of the production build.
17
+ *
18
+ * Start the development server with the following command from the
19
+ * oauth-provider root:
20
+ *
21
+ * ```sh
22
+ * pnpm run start:ui
23
+ * ```
24
+ *
25
+ * Then open the browser at http://localhost:5173/
26
+ */
27
+ </script>
28
+ <style>
29
+ /*
30
+ * PDS branding configuration (colors), in R G B format.
31
+ *
32
+ * The variables here are meant to override the default values defined in
33
+ * main.css. These values are typically generated by the backend and
34
+ * injected into the HTML. The colors suffixed with "-c" denote the
35
+ * "contrast" color of the corresponding color name. These are also
36
+ * automatically generated by the backend from the branding colors.
37
+ *
38
+ * The default colors can be seen by commenting out a color name (and
39
+ * corresponding "-c" contrast color) below:
40
+ */
41
+ :root {
42
+ --color-brand: 10 122 255;
43
+ --color-brand-c: 255 255 255;
44
+ --color-error: 244 11 66;
45
+ --color-error-c: 255 255 255;
46
+ --color-warning: 251 86 7;
47
+ --color-warning-c: 255 255 255;
48
+ --color-success: 2 195 154;
49
+ --color-success-c: 0 0 0;
50
+ }
51
+ </style>
52
+ <script type="module">
53
+ /*
54
+ * PDS branding configuration
55
+ */
56
+
57
+ const name = 'Bluesky'
58
+ const links = [
59
+ {
60
+ title: { en: 'Home' },
61
+ href: 'https://bsky.social/',
62
+ rel: 'canonical', // prevents the login page from being indexed by search engines
63
+ },
64
+ {
65
+ title: { en: 'Terms of Service' },
66
+ href: 'https://bsky.social/about/support/tos',
67
+ rel: 'terms-of-service',
68
+ },
69
+ {
70
+ title: { en: 'Privacy Policy' },
71
+ href: 'https://bsky.social/about/support/privacy-policy',
72
+ rel: 'privacy-policy',
73
+ },
74
+ {
75
+ title: { en: 'Support' },
76
+ href: 'https://blueskyweb.zendesk.com/hc/en-us',
77
+ rel: 'help',
78
+ },
79
+ ]
80
+ const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /></svg>')}`
81
+
82
+ // Provide a value here to test the "sing-in only" flow
83
+ const loginHint = undefined // 'alice.test'
84
+
85
+ // Use empty array to disable the "sing-up" flow, use a single value to
86
+ // disable the domain selector.
87
+ const availableUserDomains = ['.bsky.social', '.bsky.team']
88
+
89
+ // Use non empty string to enable hCaptcha during "sing-up" flow
90
+ const hcaptchaSiteKey = undefined
91
+
92
+ /*
93
+ * Client branding configuration
94
+ */
95
+
96
+ // Use an "http://" URL to test the "an app on your device" flow
97
+ const clientId = 'https://example.com/client.json'
98
+ const clientName = 'My App'
99
+ const clientPolicyUri = 'https://bsky.app'
100
+ const clientTosUri = 'https://bsky.app'
101
+ const clientLogoUri = 'https://bsky.app'
102
+
103
+ // Mock data
104
+
105
+ const requestUri = 'foo-bar'
106
+
107
+ document.cookie = `csrf-${requestUri}=xyz; path=/`
108
+
109
+ window.__availableLocales = ['en', 'fr']
110
+
111
+ window.__customizationData = {
112
+ availableUserDomains,
113
+ inviteCodeRequired: false,
114
+ hcaptchaSiteKey,
115
+ name,
116
+ links,
117
+ logo,
118
+ }
119
+
120
+ window.__authorizeData = {
121
+ clientId: clientId,
122
+ clientMetadata: {
123
+ client_id: clientId,
124
+ client_name: clientName,
125
+ policy_uri: clientPolicyUri,
126
+ tos_uri: clientTosUri,
127
+ logo_uri: clientLogoUri,
128
+ },
129
+ clientTrusted: false,
130
+ requestUri,
131
+ loginHint,
132
+ newSessionsRequireConsent: true,
133
+ sessions: [],
134
+ scopeDetails: [
135
+ { scope: 'atproto' },
136
+ { scope: 'transition:generic' },
137
+ { scope: 'transition:chat.bsky' },
138
+ ],
139
+ }
140
+
141
+ const origFetch = window.fetch
142
+
143
+ async function mockFetch(...args) {
144
+ const [input, init] = args
145
+
146
+ if (typeof input === 'string' && init.method === 'POST') {
147
+ const url = new URL(input, window.location)
148
+ switch (url.pathname) {
149
+ case '/oauth/authorize/sign-up':
150
+ case '/oauth/authorize/sign-in':
151
+ return new Response(
152
+ JSON.stringify({
153
+ consentRequired: false,
154
+ account: {
155
+ sub: 'did:plc:123',
156
+ name: 'Alice',
157
+ email: 'alice@test.com',
158
+ email_verified: false,
159
+ preferred_username: 'alice.test',
160
+ picture: 'https://cat.com/cat.jpg',
161
+ },
162
+ }),
163
+ { status: 200 },
164
+ )
165
+ case '/oauth/authorize/verify-handle-availability':
166
+ case '/oauth/authorize/reset-password-request':
167
+ case '/oauth/authorize/reset-password-confirm':
168
+ return new Response(null, { status: 204 })
169
+ }
170
+ }
171
+
172
+ return origFetch.call(this, ...args)
173
+ }
174
+
175
+ Object.defineProperty(window, 'fetch', {
176
+ value: mockFetch,
177
+ writable: true,
178
+ configurable: true,
179
+ })
180
+ </script>
181
+ <script src="./authorization-page.tsx" type="module"></script>
182
+ </body>
183
+ </html>
@@ -0,0 +1,55 @@
1
+ import './styles.css'
2
+
3
+ import { StrictMode } from 'react'
4
+ import { createRoot } from 'react-dom/client'
5
+ import { ErrorBoundary } from 'react-error-boundary'
6
+ import type {
7
+ AuthorizeData,
8
+ AvailableLocales,
9
+ CustomizationData,
10
+ } from '@atproto/oauth-provider-api'
11
+ import { readBackendData } from './lib/backend-data.ts'
12
+ import { LocaleProvider } from './locales/locale-provider.tsx'
13
+ import { AuthorizeView } from './views/authorize/authorize-view.tsx'
14
+ import { ErrorView } from './views/error/error-view.tsx'
15
+
16
+ export const availableLocales =
17
+ readBackendData<AvailableLocales>('__availableLocales')
18
+ export const customizationData = readBackendData<CustomizationData>(
19
+ '__customizationData',
20
+ )
21
+ export const authorizeData = readBackendData<AuthorizeData>('__authorizeData')
22
+
23
+ if (authorizeData) {
24
+ // When the user is logging in, make sure the page URL contains the
25
+ // "request_uri" in case the user refreshes the page.
26
+ const url = new URL(window.location.href)
27
+ if (
28
+ url.pathname === '/oauth/authorize' &&
29
+ !url.searchParams.has('request_uri')
30
+ ) {
31
+ url.search = ''
32
+ url.searchParams.set('client_id', authorizeData.clientId)
33
+ url.searchParams.set('request_uri', authorizeData.requestUri)
34
+ window.history.replaceState(history.state, '', url.pathname + url.search)
35
+ }
36
+ }
37
+
38
+ const container = document.getElementById('root')!
39
+
40
+ createRoot(container).render(
41
+ <StrictMode>
42
+ <LocaleProvider availableLocales={availableLocales}>
43
+ <ErrorBoundary
44
+ fallbackRender={({ error }) => (
45
+ <ErrorView error={error} customizationData={customizationData} />
46
+ )}
47
+ >
48
+ <AuthorizeView
49
+ customizationData={customizationData}
50
+ authorizeData={authorizeData}
51
+ />
52
+ </ErrorBoundary>
53
+ </LocaleProvider>
54
+ </StrictMode>,
55
+ )
@@ -0,0 +1,35 @@
1
+ import type {
2
+ AuthorizeData,
3
+ AvailableLocales,
4
+ CustomizationData,
5
+ ErrorData,
6
+ } from '@atproto/oauth-provider-api'
7
+
8
+ function readBackendData<T>(key: string, required: true): T
9
+ function readBackendData<T>(key: string, requires?: false): T | undefined
10
+ function readBackendData<T>(key: string, required = false): T | undefined {
11
+ const value = window[key] as T | undefined
12
+ delete window[key] // Prevent accidental usage / potential leaks to dependencies
13
+ if (required && value === undefined) {
14
+ throw new TypeError(`Backend data "${key}" is missing`)
15
+ }
16
+ return value
17
+ }
18
+
19
+ // These values are injected by the backend when it builds the
20
+ // page HTML. See "declareBackendData()" in the backend.
21
+
22
+ /** @deprecated Do not import directly. Only import this from main.tsx */
23
+ export const availableLocales = readBackendData<AvailableLocales>(
24
+ '__availableLocales',
25
+ true,
26
+ )
27
+ /** @deprecated Do not import directly. Only import this from main.tsx */
28
+ export const customizationData = readBackendData<CustomizationData>(
29
+ '__customizationData',
30
+ true,
31
+ )
32
+ /** @deprecated Do not import directly. Only import this from main.tsx */
33
+ export const errorData = readBackendData<ErrorData>('__errorData')
34
+ /** @deprecated Do not import directly. Only import this from main.tsx */
35
+ export const authorizeData = readBackendData<AuthorizeData>('__authorizeData')
@@ -0,0 +1,43 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { Override } from '../../lib/util.ts'
3
+ import { EyeIcon, EyeSlashIcon } from '../utils/icons.tsx'
4
+ import { Button, ButtonProps } from './button.tsx'
5
+
6
+ export type ButtonToggleVisibilityProps = Override<
7
+ Omit<ButtonProps, 'aria-label' | 'square'>,
8
+ {
9
+ visible: boolean
10
+ toggleVisible: () => void
11
+ }
12
+ >
13
+
14
+ /**
15
+ * Generic button to toggle visibility of an item (e.g. password).
16
+ */
17
+ export function ButtonToggleVisibility({
18
+ visible,
19
+ toggleVisible,
20
+
21
+ // button
22
+ onClick,
23
+ ...props
24
+ }: ButtonToggleVisibilityProps) {
25
+ const { t } = useLingui()
26
+ return (
27
+ <Button
28
+ {...props}
29
+ square
30
+ onClick={(event) => {
31
+ onClick?.(event)
32
+ if (!event.defaultPrevented) toggleVisible()
33
+ }}
34
+ aria-label={visible ? t`Hide` : t`Make visible`}
35
+ >
36
+ {visible ? (
37
+ <EyeIcon className="w-5" aria-hidden />
38
+ ) : (
39
+ <EyeSlashIcon className="w-5" aria-hidden />
40
+ )}
41
+ </Button>
42
+ )
43
+ }
@@ -0,0 +1,60 @@
1
+ import { JSX } from 'react'
2
+ import { clsx } from '../../lib/clsx.ts'
3
+ import { Override } from '../../lib/util.ts'
4
+
5
+ export type ButtonProps = Override<
6
+ JSX.IntrinsicElements['button'],
7
+ {
8
+ color?: 'brand' | 'grey'
9
+ loading?: boolean
10
+ transparent?: boolean
11
+ square?: boolean
12
+ }
13
+ >
14
+
15
+ export function Button({
16
+ color = 'grey',
17
+ transparent = false,
18
+ loading = undefined,
19
+ square = false,
20
+
21
+ // button
22
+ children,
23
+ className,
24
+ type = 'button',
25
+ role = 'Button',
26
+ disabled = false,
27
+ ...props
28
+ }: ButtonProps) {
29
+ return (
30
+ <button
31
+ role={role}
32
+ type={type}
33
+ disabled={disabled || loading === true}
34
+ {...props}
35
+ className={clsx(
36
+ 'rounded-lg truncate cursor-pointer touch-manipulation tracking-wide overflow-hidden',
37
+ square ? 'p-2' : 'py-2 px-6',
38
+ color === 'brand'
39
+ ? clsx(
40
+ 'accent-slate-100',
41
+ transparent
42
+ ? 'bg-transparent text-brand'
43
+ : 'bg-brand text-brand-c',
44
+ )
45
+ : color === 'grey'
46
+ ? clsx(
47
+ 'accent-brand',
48
+ 'text-slate-600 dark:text-slate-300',
49
+ 'hover:bg-gray-200 dark:hover:bg-gray-700',
50
+ transparent ? 'bg-transparent' : 'bg-gray-100 dark:bg-gray-800',
51
+ )
52
+ : undefined,
53
+ 'disabled:opacity-50',
54
+ className,
55
+ )}
56
+ >
57
+ {children}
58
+ </button>
59
+ )
60
+ }
@@ -0,0 +1,55 @@
1
+ import { JSX, ReactNode, createContext, useMemo } from 'react'
2
+ import { useRandomString } from '../../hooks/use-random-string.ts'
3
+ import { Override } from '../../lib/util.ts'
4
+
5
+ export type FieldsetContextValue = {
6
+ disabled: boolean
7
+ labelId?: string
8
+ }
9
+
10
+ export const FieldsetContext = createContext<FieldsetContextValue>({
11
+ disabled: false,
12
+ })
13
+ FieldsetContext.displayName = 'FieldsetContext'
14
+
15
+ export type FieldsetCardProps = Override<
16
+ Omit<JSX.IntrinsicElements['fieldset'], 'aria-labelledby'>,
17
+ {
18
+ label?: ReactNode
19
+ }
20
+ >
21
+
22
+ export function Fieldset({
23
+ label,
24
+ children,
25
+ disabled,
26
+ ...props
27
+ }: FieldsetCardProps) {
28
+ const labelId = useRandomString({ prefix: 'fieldset-' })
29
+
30
+ const contextValue = useMemo(
31
+ () => ({
32
+ disabled: disabled ?? false,
33
+ labelId: label ? labelId : undefined,
34
+ }),
35
+ [disabled, label, labelId],
36
+ )
37
+
38
+ return (
39
+ <fieldset {...props} aria-labelledby={labelId} disabled={disabled}>
40
+ {label && (
41
+ <legend
42
+ id={labelId}
43
+ key="title"
44
+ className="mb-1 text-slate-600 dark:text-slate-400 text-sm font-medium"
45
+ >
46
+ {label}
47
+ </legend>
48
+ )}
49
+
50
+ <div className="flex flex-col space-y-4">
51
+ <FieldsetContext value={contextValue}>{children}</FieldsetContext>
52
+ </div>
53
+ </fieldset>
54
+ )
55
+ }