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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
- }