@atproto/oauth-provider 0.1.0 → 0.1.2-rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/account/account-manager.d.ts +2 -2
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js.map +1 -1
  5. package/dist/account/account-store.d.ts +19 -6
  6. package/dist/account/account-store.d.ts.map +1 -1
  7. package/dist/account/account-store.js +16 -1
  8. package/dist/account/account-store.js.map +1 -1
  9. package/dist/assets/app/bundle-manifest.json +3 -3
  10. package/dist/assets/app/main.css +1 -1
  11. package/dist/assets/app/main.js +3 -3
  12. package/dist/assets/app/main.js.map +1 -1
  13. package/dist/client/client-auth.d.ts.map +1 -1
  14. package/dist/client/client-auth.js +2 -2
  15. package/dist/client/client-auth.js.map +1 -1
  16. package/dist/client/client-manager.d.ts.map +1 -1
  17. package/dist/client/client-manager.js +3 -1
  18. package/dist/client/client-manager.js.map +1 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +3 -3
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  23. package/dist/dpop/dpop-manager.js +3 -3
  24. package/dist/dpop/dpop-manager.js.map +1 -1
  25. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  26. package/dist/errors/invalid-token-error.js +3 -2
  27. package/dist/errors/invalid-token-error.js.map +1 -1
  28. package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
  29. package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
  30. package/dist/errors/second-authentication-factor-required-error.js +23 -0
  31. package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
  32. package/dist/lib/util/authorization-header.js +1 -1
  33. package/dist/lib/util/authorization-header.js.map +1 -1
  34. package/dist/metadata/build-metadata.d.ts.map +1 -1
  35. package/dist/metadata/build-metadata.js +2 -0
  36. package/dist/metadata/build-metadata.js.map +1 -1
  37. package/dist/oauth-errors.d.ts +1 -0
  38. package/dist/oauth-errors.d.ts.map +1 -1
  39. package/dist/oauth-errors.js +3 -1
  40. package/dist/oauth-errors.js.map +1 -1
  41. package/dist/oauth-provider.d.ts +101 -4
  42. package/dist/oauth-provider.d.ts.map +1 -1
  43. package/dist/oauth-provider.js +98 -110
  44. package/dist/oauth-provider.js.map +1 -1
  45. package/dist/output/build-authorize-data.d.ts +40 -0
  46. package/dist/output/build-authorize-data.d.ts.map +1 -0
  47. package/dist/output/build-authorize-data.js +22 -0
  48. package/dist/output/build-authorize-data.js.map +1 -0
  49. package/dist/output/build-error-payload.d.ts.map +1 -1
  50. package/dist/output/build-error-payload.js +4 -3
  51. package/dist/output/build-error-payload.js.map +1 -1
  52. package/dist/output/customization.d.ts +2 -12
  53. package/dist/output/customization.d.ts.map +1 -1
  54. package/dist/output/customization.js +59 -32
  55. package/dist/output/customization.js.map +1 -1
  56. package/dist/output/output-manager.d.ts +16 -0
  57. package/dist/output/output-manager.d.ts.map +1 -0
  58. package/dist/output/output-manager.js +69 -0
  59. package/dist/output/output-manager.js.map +1 -0
  60. package/dist/output/send-web-page.d.ts +1 -1
  61. package/dist/output/send-web-page.d.ts.map +1 -1
  62. package/dist/output/send-web-page.js +3 -2
  63. package/dist/output/send-web-page.js.map +1 -1
  64. package/package.json +7 -7
  65. package/src/account/account-manager.ts +2 -2
  66. package/src/account/account-store.ts +12 -6
  67. package/src/assets/app/components/accept-form.tsx +86 -83
  68. package/src/assets/app/components/account-picker.tsx +98 -79
  69. package/src/assets/app/components/button.tsx +34 -0
  70. package/src/assets/app/components/client-identifier.tsx +12 -13
  71. package/src/assets/app/components/fieldset.tsx +26 -0
  72. package/src/assets/app/components/form-card.tsx +47 -0
  73. package/src/assets/app/components/help-card.tsx +1 -1
  74. package/src/assets/app/components/icons/alert-icon.tsx +5 -0
  75. package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
  76. package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
  77. package/src/assets/app/components/icons/lock-icon.tsx +5 -0
  78. package/src/assets/app/components/icons/token-icon.tsx +5 -0
  79. package/src/assets/app/components/icons/util.tsx +17 -0
  80. package/src/assets/app/components/info-card.tsx +45 -0
  81. package/src/assets/app/components/input-checkbox.tsx +47 -0
  82. package/src/assets/app/components/input-container.tsx +37 -0
  83. package/src/assets/app/components/input-layout.tsx +47 -0
  84. package/src/assets/app/components/input-text.tsx +69 -0
  85. package/src/assets/app/components/layout-title-page.tsx +33 -16
  86. package/src/assets/app/components/layout-welcome.tsx +30 -14
  87. package/src/assets/app/components/sign-in-form.tsx +214 -196
  88. package/src/assets/app/components/sign-up-account-form.tsx +101 -117
  89. package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
  90. package/src/assets/app/hooks/use-api.ts +2 -0
  91. package/src/assets/app/lib/api.ts +49 -14
  92. package/src/assets/app/lib/clsx.ts +6 -1
  93. package/src/assets/app/lib/util.ts +3 -0
  94. package/src/assets/app/main.css +2 -1
  95. package/src/assets/app/views/accept-view.tsx +4 -3
  96. package/src/assets/app/views/authorize-view.tsx +8 -4
  97. package/src/assets/app/views/error-view.tsx +24 -15
  98. package/src/assets/app/views/sign-in-view.tsx +5 -15
  99. package/src/assets/app/views/sign-up-view.tsx +3 -10
  100. package/src/assets/app/views/welcome-view.tsx +11 -18
  101. package/src/client/client-auth.ts +3 -2
  102. package/src/client/client-manager.ts +2 -1
  103. package/src/client/client.ts +3 -1
  104. package/src/dpop/dpop-manager.ts +3 -2
  105. package/src/errors/invalid-token-error.ts +3 -1
  106. package/src/errors/second-authentication-factor-required-error.ts +25 -0
  107. package/src/lib/util/authorization-header.ts +1 -1
  108. package/src/metadata/build-metadata.ts +3 -0
  109. package/src/oauth-errors.ts +1 -0
  110. package/src/oauth-provider.ts +110 -99
  111. package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
  112. package/src/output/build-error-payload.ts +3 -1
  113. package/src/output/customization.ts +67 -45
  114. package/src/output/output-manager.ts +87 -0
  115. package/src/output/send-web-page.ts +4 -3
  116. package/tailwind.config.js +14 -1
  117. package/src/assets/app/components/error-card.tsx +0 -41
  118. package/src/output/send-error-page.ts +0 -41
@@ -1,22 +1,9 @@
1
1
  // Matches colors defined in tailwind.config.js
2
- const colorNames = ['primary', 'error'] as const
2
+ const colorNames = ['brand', 'error', 'warning'] as const
3
3
  type ColorName = (typeof colorNames)[number]
4
4
  const isColorName = (name: string): name is ColorName =>
5
5
  (colorNames as readonly string[]).includes(name)
6
6
 
7
- export type FieldDefinition = {
8
- label?: string
9
- placeholder?: string
10
- pattern?: string
11
- title?: string
12
- }
13
-
14
- export type ExtraFieldDefinition = FieldDefinition & {
15
- type: 'text' | 'password' | 'date' | 'captcha'
16
- required?: boolean
17
- [_: string]: unknown
18
- }
19
-
20
7
  export type Customization = {
21
8
  name?: string
22
9
  logo?: string
@@ -41,56 +28,91 @@ export function buildCustomizationData({
41
28
  }
42
29
 
43
30
  export function buildCustomizationCss(customization?: Customization) {
44
- if (!customization?.colors) return ''
31
+ const vars = Array.from(buildCustomizationVars(customization))
32
+ if (vars.length) return `:root { ${vars.join(' ')} }`
33
+
34
+ return ''
35
+ }
36
+
37
+ export function* buildCustomizationVars(customization?: Customization) {
38
+ if (customization?.colors) {
39
+ for (const [name, value] of Object.entries(customization.colors)) {
40
+ if (!isColorName(name)) {
41
+ throw new TypeError(`Invalid color name: ${name}`)
42
+ }
43
+
44
+ // Skip undefined values
45
+ if (value === undefined) continue
45
46
 
46
- const vars = Object.entries(customization.colors)
47
- .filter((e) => isColorName(e[0]) && e[1] != null)
48
- .map(([name, value]) => [name, parseColor(value)] as const)
49
- .filter((e): e is [ColorName, ParsedColor] => e[1] != null)
50
- // alpha not supported by tailwind (it does not work that way)
51
- .map(([name, { r, g, b }]) => `--color-${name}: ${r} ${g} ${b};`)
47
+ const { r, g, b, a } = parseColor(value)
52
48
 
53
- return `:root { ${vars.join(' ')} }`
49
+ // Tailwind does not apply alpha values to base colors
50
+ if (a !== undefined) throw new TypeError('Alpha not supported')
51
+
52
+ yield `--color-${name}: ${r} ${g} ${b};`
53
+ }
54
+ }
54
55
  }
55
56
 
56
- type ParsedColor = { r: number; g: number; b: number; a?: number }
57
- function parseColor(color: string): undefined | ParsedColor {
57
+ type RgbaColor = { r: number; g: number; b: number; a?: number }
58
+ function parseColor(color: unknown): RgbaColor {
59
+ if (typeof color !== 'string') {
60
+ throw new TypeError(`Invalid color value: ${typeof color}`)
61
+ }
62
+
58
63
  if (color.startsWith('#')) {
59
64
  if (color.length === 4 || color.length === 5) {
60
- const [r, g, b, a] = color
61
- .slice(1)
62
- .split('')
63
- .map((c) => parseInt(c + c, 16))
65
+ const r = parseUi8Hex(color.slice(1, 2))
66
+ const g = parseUi8Hex(color.slice(2, 3))
67
+ const b = parseUi8Hex(color.slice(3, 4))
68
+ const a = color.length > 4 ? parseUi8Hex(color.slice(4, 5)) : undefined
64
69
  return { r, g, b, a }
65
70
  }
66
71
 
67
72
  if (color.length === 7 || color.length === 9) {
68
- const r = parseInt(color.slice(1, 3), 16)
69
- const g = parseInt(color.slice(3, 5), 16)
70
- const b = parseInt(color.slice(5, 7), 16)
71
- const a = color.length > 8 ? parseInt(color.slice(7, 9), 16) : undefined
73
+ const r = parseUi8Hex(color.slice(1, 3))
74
+ const g = parseUi8Hex(color.slice(3, 5))
75
+ const b = parseUi8Hex(color.slice(5, 7))
76
+ const a = color.length > 8 ? parseUi8Hex(color.slice(7, 9)) : undefined
72
77
  return { r, g, b, a }
73
78
  }
74
79
 
75
- return undefined
80
+ throw new TypeError(`Invalid hex color: ${color}`)
76
81
  }
77
82
 
78
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
83
+ const rgbMatch = color.match(
84
+ /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
85
+ )
79
86
  if (rgbMatch) {
80
- const [, r, g, b] = rgbMatch
81
- return { r: parseInt(r, 10), g: parseInt(g, 10), b: parseInt(b, 10) }
87
+ const r = parseUi8Dec(rgbMatch[1])
88
+ const g = parseUi8Dec(rgbMatch[2])
89
+ const b = parseUi8Dec(rgbMatch[3])
90
+ return { r, g, b }
82
91
  }
83
92
 
84
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)/)
93
+ const rgbaMatch = color.match(
94
+ /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
95
+ )
85
96
  if (rgbaMatch) {
86
- const [, r, g, b, a] = rgbaMatch
87
- return {
88
- r: parseInt(r, 10),
89
- g: parseInt(g, 10),
90
- b: parseInt(b, 10),
91
- a: parseInt(a, 10),
92
- }
97
+ const r = parseUi8Dec(rgbaMatch[1])
98
+ const g = parseUi8Dec(rgbaMatch[2])
99
+ const b = parseUi8Dec(rgbaMatch[3])
100
+ const a = parseUi8Dec(rgbaMatch[4])
101
+ return { r, g, b, a }
93
102
  }
94
103
 
95
- return undefined
104
+ throw new TypeError(`Unsupported color format: ${color}`)
105
+ }
106
+
107
+ function parseUi8Hex(v: string) {
108
+ return asUi8(parseInt(v, 16))
109
+ }
110
+
111
+ function parseUi8Dec(v: string) {
112
+ return asUi8(parseInt(v, 10))
113
+ }
114
+
115
+ function asUi8(v: number) {
116
+ if (v >= 0 && v <= 255 && v === (v | 0)) return v
117
+ throw new TypeError(`Invalid color component: ${v}`)
96
118
  }
@@ -0,0 +1,87 @@
1
+ import { ServerResponse } from 'node:http'
2
+
3
+ import { Asset } from '../assets/asset.js'
4
+ import { getAsset } from '../assets/index.js'
5
+ import { cssCode, Html, html } from '../lib/html/index.js'
6
+ import {
7
+ AuthorizationResultAuthorize,
8
+ buildAuthorizeData,
9
+ } from './build-authorize-data.js'
10
+ import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
11
+ import {
12
+ buildCustomizationCss,
13
+ buildCustomizationData,
14
+ Customization,
15
+ } from './customization.js'
16
+ import { declareBackendData, sendWebPage } from './send-web-page.js'
17
+
18
+ export class OutputManager {
19
+ readonly customizationScript: Html
20
+ readonly customizationStyle: Html
21
+ readonly customizationLinks?: Customization['links']
22
+
23
+ // Could technically cause an "UnhandledPromiseRejection", which might cause
24
+ // the process to exit. This is intentional, as it's a critical error. It
25
+ // should never happen in practice, as the built assets are bundled with the
26
+ // package.
27
+ readonly assetsPromise: Promise<[js: Asset, css: Asset]> = Promise.all([
28
+ getAsset('main.js'),
29
+ getAsset('main.css'),
30
+ ] as const)
31
+
32
+ constructor(customization?: Customization) {
33
+ // Note: building this here for two reasons:
34
+ // 1. To avoid re-building it on every request
35
+ // 2. To throw during init if the customization is invalid
36
+ this.customizationScript = declareBackendData(
37
+ '__customizationData',
38
+ buildCustomizationData(customization),
39
+ )
40
+ this.customizationStyle = cssCode(buildCustomizationCss(customization))
41
+ this.customizationLinks = customization?.links
42
+ }
43
+
44
+ async sendAuthorizePage(
45
+ res: ServerResponse,
46
+ data: AuthorizationResultAuthorize,
47
+ ): Promise<void> {
48
+ const [jsAsset, cssAsset] = await this.assetsPromise
49
+
50
+ return sendWebPage(res, {
51
+ scripts: [
52
+ declareBackendData('__authorizeData', buildAuthorizeData(data)),
53
+ this.customizationScript,
54
+ jsAsset, // Last (to be able to read the "backend data" variables)
55
+ ],
56
+ styles: [
57
+ cssAsset, // First (to be overridden by customization)
58
+ this.customizationStyle,
59
+ ],
60
+ links: this.customizationLinks,
61
+ htmlAttrs: { lang: 'en' },
62
+ title: 'Authorize',
63
+ body: html`<div id="root"></div>`,
64
+ })
65
+ }
66
+
67
+ async sendErrorPage(res: ServerResponse, err: unknown): Promise<void> {
68
+ const [jsAsset, cssAsset] = await this.assetsPromise
69
+
70
+ return sendWebPage(res, {
71
+ status: buildErrorStatus(err),
72
+ scripts: [
73
+ declareBackendData('__errorData', buildErrorPayload(err)),
74
+ this.customizationScript,
75
+ jsAsset, // Last (to be able to read the "backend data" variables)
76
+ ],
77
+ styles: [
78
+ cssAsset, // First (to be overridden by customization)
79
+ this.customizationStyle,
80
+ ],
81
+ links: this.customizationLinks,
82
+ htmlAttrs: { lang: 'en' },
83
+ title: 'Error',
84
+ body: html`<div id="root"></div>`,
85
+ })
86
+ }
87
+ }
@@ -18,11 +18,12 @@ export function declareBackendData(name: string, data: unknown) {
18
18
  return js`window[${name}]=${data};document.currentScript.remove();`
19
19
  }
20
20
 
21
- export function sendWebPage(
21
+ export async function sendWebPage(
22
22
  res: ServerResponse,
23
23
  { status = 200, ...options }: BuildDocumentOptions & { status?: number },
24
- ): void {
24
+ ): Promise<void> {
25
25
  // @TODO: make these headers configurable (?)
26
+ res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
26
27
  res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless')
27
28
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
28
29
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
@@ -52,7 +53,7 @@ export function sendWebPage(
52
53
 
53
54
  const html = buildDocument(options)
54
55
 
55
- writeHtml(res, html.toString(), status)
56
+ return writeHtml(res, html.toString(), status)
56
57
  }
57
58
 
58
59
  function assetToHash(asset: Html | AssetRef): string {
@@ -2,10 +2,23 @@
2
2
  export default {
3
3
  content: ['src/assets/app/**/*.{js,ts,jsx,tsx}'],
4
4
  theme: {
5
+ fontFamily: {
6
+ sans: [
7
+ '-apple-system',
8
+ 'BlinkMacSystemFont',
9
+ '"Segoe UI"',
10
+ 'Roboto',
11
+ 'Helvetica',
12
+ 'Arial',
13
+ 'sans-serif',
14
+ ],
15
+ mono: ['Monaco', 'mono'],
16
+ },
5
17
  extend: {
6
18
  colors: {
7
- primary: 'rgb(var(--color-primary) / <alpha-value>)',
19
+ brand: 'rgb(var(--color-brand) / <alpha-value>)',
8
20
  error: 'rgb(var(--color-error) / <alpha-value>)',
21
+ warning: 'rgb(var(--color-warning) / <alpha-value>)',
9
22
  },
10
23
  },
11
24
  },
@@ -1,41 +0,0 @@
1
- import { HtmlHTMLAttributes } from 'react'
2
- import { clsx } from '../lib/clsx'
3
-
4
- export type ErrorCardProps = {
5
- message?: null | string
6
- role?: 'alert' | 'status'
7
- }
8
-
9
- export function ErrorCard({
10
- message,
11
-
12
- role = 'alert',
13
- className,
14
- ...attrs
15
- }: Partial<ErrorCardProps> &
16
- Omit<HtmlHTMLAttributes<HTMLDivElement>, keyof ErrorCardProps | 'children'>) {
17
- return (
18
- <div
19
- {...attrs}
20
- className={clsx(
21
- 'flex items-center rounded bg-error py-1 px-2 text-white dark:text-black shadow-md',
22
- className,
23
- )}
24
- role={role}
25
- >
26
- <svg
27
- className="fill-current h-4 w-4"
28
- xmlns="http://www.w3.org/2000/svg"
29
- viewBox="0 0 20 20"
30
- >
31
- <path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z" />
32
- </svg>
33
-
34
- <div className="ml-4">
35
- <p>
36
- {typeof message === 'string' ? message : 'An unknown error occurred'}
37
- </p>
38
- </div>
39
- </div>
40
- )
41
- }
@@ -1,41 +0,0 @@
1
- import { ServerResponse } from 'node:http'
2
-
3
- import { getAsset } from '../assets/index.js'
4
- import { cssCode, html } from '../lib/html/index.js'
5
- import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
6
- import {
7
- Customization,
8
- buildCustomizationCss,
9
- buildCustomizationData,
10
- } from './customization.js'
11
- import { declareBackendData, sendWebPage } from './send-web-page.js'
12
-
13
- export async function sendErrorPage(
14
- res: ServerResponse,
15
- err: unknown,
16
- customization?: Customization,
17
- ): Promise<void> {
18
- const [jsAsset, cssAsset] = await Promise.all([
19
- getAsset('main.js'),
20
- getAsset('main.css'),
21
- ])
22
-
23
- return sendWebPage(res, {
24
- status: buildErrorStatus(err),
25
- scripts: [
26
- declareBackendData(
27
- '__customizationData',
28
- buildCustomizationData(customization),
29
- ),
30
- declareBackendData('__errorData', buildErrorPayload(err)),
31
- jsAsset, // Last (to be able to read the global variables)
32
- ],
33
- styles: [
34
- cssAsset, // First (to be overridden by customization)
35
- cssCode(buildCustomizationCss(customization)),
36
- ],
37
- htmlAttrs: { lang: 'en' },
38
- title: 'Error',
39
- body: html`<div id="root"></div>`,
40
- })
41
- }