@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
@@ -6,36 +6,41 @@ import {
6
6
  useState,
7
7
  } from 'react'
8
8
 
9
- import { clsx } from '../lib/clsx'
10
- import { ErrorCard } from './error-card'
9
+ import { Button } from './button'
10
+ import { FormCard, FormCardProps } from './form-card'
11
+ import { Override } from '../lib/util'
12
+ import { Fieldset } from './fieldset'
11
13
 
12
14
  export type SignUpAccountFormOutput = {
13
15
  username: string
14
16
  password: string
15
17
  }
16
18
 
17
- export type SignUpAccountFormProps = {
18
- onSubmit: (credentials: SignUpAccountFormOutput) => void | PromiseLike<void>
19
- submitLabel?: ReactNode
20
- submitAria?: string
21
-
22
- onCancel?: () => void
23
- cancelLabel?: ReactNode
24
- cancelAria?: string
25
-
26
- username?: string
27
- usernamePlaceholder?: string
28
- usernameLabel?: string
29
- usernameAria?: string
30
- usernamePattern?: string
31
- usernameTitle?: string
32
-
33
- passwordPlaceholder?: string
34
- passwordLabel?: string
35
- passwordAria?: string
36
- passwordPattern?: string
37
- passwordTitle?: string
38
- }
19
+ export type SignUpAccountFormProps = Override<
20
+ FormCardProps,
21
+ {
22
+ onSubmit: (credentials: SignUpAccountFormOutput) => void | PromiseLike<void>
23
+ submitLabel?: ReactNode
24
+ submitAria?: string
25
+
26
+ onCancel?: () => void
27
+ cancelLabel?: ReactNode
28
+ cancelAria?: string
29
+
30
+ username?: string
31
+ usernamePlaceholder?: string
32
+ usernameLabel?: string
33
+ usernameAria?: string
34
+ usernamePattern?: string
35
+ usernameTitle?: string
36
+
37
+ passwordPlaceholder?: string
38
+ passwordLabel?: string
39
+ passwordAria?: string
40
+ passwordPattern?: string
41
+ passwordTitle?: string
42
+ }
43
+ >
39
44
 
40
45
  export function SignUpAccountForm({
41
46
  onSubmit,
@@ -59,9 +64,8 @@ export function SignUpAccountForm({
59
64
  passwordPattern,
60
65
  passwordTitle,
61
66
 
62
- className,
63
67
  children,
64
- ...attrs
68
+ ...props
65
69
  }: SignUpAccountFormProps &
66
70
  Omit<FormHTMLAttributes<HTMLFormElement>, keyof SignUpAccountFormProps>) {
67
71
  const [loading, setLoading] = useState(false)
@@ -98,104 +102,84 @@ export function SignUpAccountForm({
98
102
  )
99
103
 
100
104
  return (
101
- <form
102
- {...attrs}
103
- className={clsx('flex flex-col', className)}
105
+ <FormCard
106
+ append={children}
107
+ error={errorMessage}
104
108
  onSubmit={doSubmit}
105
- >
106
- <fieldset disabled={loading}>
107
- <label className="text-sm font-medium" htmlFor="username">
108
- {usernameLabel}
109
- </label>
110
-
111
- <div
112
- id="username"
113
- className="mb-4 relative flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100"
114
- >
115
- <span className="w-6 ml-1 text-center text-base">@</span>
116
- <input
117
- name="username"
118
- type="text"
119
- onChange={() => setErrorMessage(null)}
120
- className="relative m-1 block w-[1px] min-w-0 flex-auto leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100 disabled:text-gray-500"
121
- placeholder={usernamePlaceholder}
122
- aria-label={usernameAria}
123
- autoCapitalize="none"
124
- autoCorrect="off"
125
- autoComplete="username"
126
- spellCheck="false"
127
- dir="auto"
128
- enterKeyHint="next"
129
- required
130
- defaultValue={defaultUsername}
131
- pattern={usernamePattern}
132
- title={usernameTitle}
133
- />
134
- </div>
135
-
136
- <label className="text-sm font-medium" htmlFor="password">
137
- {passwordLabel}
138
- </label>
139
-
140
- <div
141
- id="password"
142
- className="mb-4 relative flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100"
143
- >
144
- <span className="w-6 ml-1 text-center text-2xl font-light -mb-2">
145
- *
146
- </span>
147
- <input
148
- name="password"
149
- type="password"
150
- onChange={() => setErrorMessage(null)}
151
- className="relative m-1 block w-[1px] min-w-0 flex-auto leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
152
- placeholder={passwordPlaceholder}
153
- aria-label={passwordAria}
154
- autoCapitalize="none"
155
- autoCorrect="off"
156
- autoComplete="new-password"
157
- dir="auto"
158
- enterKeyHint="done"
159
- spellCheck="false"
160
- required
161
- pattern={passwordPattern}
162
- title={passwordTitle}
163
- />
164
- </div>
165
- </fieldset>
166
-
167
- {children && <div className="mt-4">{children}</div>}
168
-
169
- {errorMessage && <ErrorCard className="mt-2" message={errorMessage} />}
170
-
171
- <div className="flex-auto"></div>
172
-
173
- <div className="p-4 flex flex-wrap items-center justify-start">
174
- <button
175
- className="py-2 bg-transparent text-primary rounded-md font-semibold order-last"
109
+ cancel={
110
+ onCancel && (
111
+ <Button aria-label={cancelAria} onClick={onCancel}>
112
+ {cancelLabel}
113
+ </Button>
114
+ )
115
+ }
116
+ actions={
117
+ <Button
118
+ color="brand"
176
119
  type="submit"
177
- role="Button"
178
120
  aria-label={submitAria}
179
121
  disabled={loading}
180
122
  >
181
123
  {submitLabel}
182
- </button>
183
-
184
- {onCancel && (
185
- <button
186
- className="py-2 bg-transparent text-primary rounded-md font-light"
187
- type="button"
188
- role="Button"
189
- aria-label={cancelAria}
190
- onClick={onCancel}
191
- >
192
- {cancelLabel}
193
- </button>
194
- )}
124
+ </Button>
125
+ }
126
+ {...props}
127
+ >
128
+ <Fieldset disabled={loading}>
129
+ <label className="text-sm font-medium block">
130
+ <p>{usernameLabel}</p>
131
+
132
+ <div className="relative flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100">
133
+ <span className="w-6 ml-1 text-center text-base">@</span>
134
+ <input
135
+ name="username"
136
+ type="text"
137
+ onChange={() => setErrorMessage(null)}
138
+ className="relative m-1 block w-[1px] min-w-0 flex-auto leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100 disabled:text-gray-500"
139
+ placeholder={usernamePlaceholder}
140
+ aria-label={usernameAria}
141
+ autoCapitalize="none"
142
+ autoCorrect="off"
143
+ autoComplete="username"
144
+ spellCheck="false"
145
+ dir="auto"
146
+ enterKeyHint="next"
147
+ required
148
+ defaultValue={defaultUsername}
149
+ pattern={usernamePattern}
150
+ title={usernameTitle}
151
+ />
152
+ </div>
153
+ </label>
195
154
 
196
- <div className="flex-auto" />
197
- </div>
198
- </form>
155
+ <label className="text-sm font-medium block">
156
+ <p>{passwordLabel}</p>
157
+
158
+ <div className="relative flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100">
159
+ <span className="w-6 ml-1 text-center text-2xl font-light -mb-2">
160
+ *
161
+ </span>
162
+ <input
163
+ name="password"
164
+ type="password"
165
+ onChange={() => setErrorMessage(null)}
166
+ className="relative m-1 block w-[1px] min-w-0 flex-auto leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
167
+ placeholder={passwordPlaceholder}
168
+ aria-label={passwordAria}
169
+ autoCapitalize="none"
170
+ autoCorrect="off"
171
+ autoComplete="new-password"
172
+ dir="auto"
173
+ enterKeyHint="done"
174
+ spellCheck="false"
175
+ required
176
+ pattern={passwordPattern}
177
+ title={passwordTitle}
178
+ />
179
+ </div>
180
+ </label>
181
+ </Fieldset>
182
+ </FormCard>
199
183
  )
200
184
  }
201
185
 
@@ -31,7 +31,7 @@ export function SignUpDisclaimer({
31
31
  href={l.href}
32
32
  rel={l.rel}
33
33
  target="_blank"
34
- className="text-primary underline"
34
+ className="text-brand underline"
35
35
  >
36
36
  {l.title}
37
37
  </a>
@@ -8,6 +8,8 @@ import { useCsrfToken } from './use-csrf-token'
8
8
  export type SignInCredentials = {
9
9
  username: string
10
10
  password: string
11
+ emailOtp?: string
12
+
11
13
  remember?: boolean
12
14
  }
13
15
 
@@ -1,4 +1,4 @@
1
- import { fetchJsonProcessor, fetchOkProcessor } from '@atproto-labs/fetch'
1
+ import { FetchResponseError, Json } from '@atproto-labs/fetch'
2
2
 
3
3
  import { Account, Session } from '../backend-data'
4
4
 
@@ -15,7 +15,7 @@ export class Api {
15
15
  password: string
16
16
  remember?: boolean
17
17
  }): Promise<Session> {
18
- const { json } = await fetch('/oauth/authorize/sign-in', {
18
+ const response = await fetch('/oauth/authorize/sign-in', {
19
19
  method: 'POST',
20
20
  headers: { 'Content-Type': 'application/json' },
21
21
  mode: 'same-origin',
@@ -26,20 +26,40 @@ export class Api {
26
26
  credentials,
27
27
  }),
28
28
  })
29
- .then(fetchOkProcessor())
30
- .then(
31
- fetchJsonProcessor<{
32
- account: Account
33
- consentRequired: boolean
34
- }>(),
35
- )
36
29
 
37
- return {
38
- account: json.account,
30
+ const json: Json = await response.json()
39
31
 
40
- selected: true,
41
- loginRequired: false,
42
- consentRequired: this.newSessionsRequireConsent || json.consentRequired,
32
+ if (response.ok) {
33
+ const data = json as {
34
+ account: Account
35
+ consentRequired: boolean
36
+ }
37
+
38
+ return {
39
+ account: data.account,
40
+
41
+ selected: true,
42
+ loginRequired: false,
43
+ consentRequired: this.newSessionsRequireConsent || data.consentRequired,
44
+ }
45
+ } else if (
46
+ response.status === 400 &&
47
+ json?.['error'] === 'invalid_request' &&
48
+ json?.['error_description'] === 'Invalid credentials'
49
+ ) {
50
+ throw new InvalidCredentialsError()
51
+ } else if (
52
+ response.status === 401 &&
53
+ json?.['error'] === 'second_authentication_factor_required'
54
+ ) {
55
+ const data = json as {
56
+ type: 'emailOtp'
57
+ hint: string
58
+ }
59
+
60
+ throw new SecondAuthenticationFactorRequiredError(data.type, data.hint)
61
+ } else {
62
+ throw new FetchResponseError(response)
43
63
  }
44
64
  }
45
65
 
@@ -62,3 +82,18 @@ export class Api {
62
82
  return url
63
83
  }
64
84
  }
85
+
86
+ export class InvalidCredentialsError extends Error {
87
+ constructor() {
88
+ super('Invalid credentials')
89
+ }
90
+ }
91
+
92
+ export class SecondAuthenticationFactorRequiredError extends Error {
93
+ constructor(
94
+ public type: 'emailOtp',
95
+ public hint: string,
96
+ ) {
97
+ super(`${type} authentication factor required (hint: ${hint})`)
98
+ }
99
+ }
@@ -1,4 +1,9 @@
1
- export function clsx(a: string | undefined, b?: string) {
1
+ export function clsx(
2
+ a?: string,
3
+ ...args: readonly (string | undefined)[]
4
+ ): string | undefined {
5
+ if (args.length === 0) return a
6
+ const b = clsx(...args)
2
7
  if (a && b) return `${a} ${b}`
3
8
  return a || b
4
9
  }
@@ -8,3 +8,6 @@ export function upsert<T>(
8
8
  ? [...arr, item]
9
9
  : [...arr.slice(0, idx), item, ...arr.slice(idx + 1)]
10
10
  }
11
+
12
+ export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>
13
+ export type Override<T, U> = Simplify<Omit<T, keyof U> & U>
@@ -5,7 +5,8 @@
5
5
  /* Matches colors defined in tailwind.config.js */
6
6
  @layer base {
7
7
  :root {
8
- --color-primary: 255 115 179;
8
+ --color-brand: 255 115 179;
9
9
  --color-error: 235 65 49;
10
+ --color-warning: 255 201 73;
10
11
  }
11
12
  }
@@ -31,13 +31,14 @@ export function AcceptView({
31
31
  subtitle={
32
32
  <>
33
33
  Grant access to your{' '}
34
- <b>{account.preferred_username || account.email || account.sub}</b>{' '}
35
- account.
34
+ <b className="text-black dark:text-white">
35
+ {account.preferred_username || account.email || account.sub}
36
+ </b>{' '}
37
+ account
36
38
  </>
37
39
  }
38
40
  >
39
41
  <AcceptForm
40
- className="max-w-lg w-full"
41
42
  clientId={clientId}
42
43
  clientMetadata={clientMetadata}
43
44
  clientTrusted={clientTrusted}
@@ -85,10 +85,14 @@ export function AuthorizeView({
85
85
  clientTrusted={authorizeData.clientTrusted}
86
86
  onAccept={() => doAccept(session.account)}
87
87
  onReject={doReject}
88
- onBack={() => {
89
- setSession(null)
90
- setView(sessions.length ? 'sign-in' : 'welcome')
91
- }}
88
+ onBack={
89
+ forceSignIn
90
+ ? undefined
91
+ : () => {
92
+ setSession(null)
93
+ setView(sessions.length ? 'sign-in' : 'welcome')
94
+ }
95
+ }
92
96
  />
93
97
  )
94
98
  }
@@ -1,27 +1,36 @@
1
1
  import { CustomizationData, ErrorData } from '../backend-data'
2
- import { ErrorCard } from '../components/error-card'
3
- import { LayoutWelcome } from '../components/layout-welcome'
2
+ import { InfoCard } from '../components/info-card'
3
+ import { LayoutWelcome, LayoutWelcomeProps } from '../components/layout-welcome'
4
+ import { Override } from '../lib/util'
4
5
 
5
- export type ErrorViewProps = {
6
- customizationData?: CustomizationData
7
- errorData?: ErrorData
8
- }
6
+ export type ErrorViewProps = Override<
7
+ Omit<LayoutWelcomeProps, keyof CustomizationData>,
8
+ {
9
+ customizationData?: CustomizationData
10
+ errorData?: ErrorData
11
+ }
12
+ >
9
13
 
10
- export function ErrorView({ errorData, customizationData }: ErrorViewProps) {
14
+ export function ErrorView({
15
+ errorData,
16
+ customizationData,
17
+ ...props
18
+ }: ErrorViewProps) {
11
19
  return (
12
- <LayoutWelcome {...customizationData}>
13
- <ErrorCard message={getUserFriendlyMessage(errorData)} />
20
+ <LayoutWelcome {...customizationData} {...props}>
21
+ <InfoCard role="alert">{getUserFriendlyMessage(errorData)}</InfoCard>
14
22
  </LayoutWelcome>
15
23
  )
16
24
  }
17
25
 
18
26
  function getUserFriendlyMessage(errorData?: ErrorData) {
19
27
  const desc = errorData?.error_description
20
- switch (desc) {
21
- case 'Unknown request_uri': // Request was removed from database
22
- case 'This request has expired':
23
- return 'This sign-in session has expired'
24
- default:
25
- return desc || 'An unknown error occurred'
28
+ if (
29
+ desc === 'This request has expired' ||
30
+ desc?.startsWith('Unknown request_uri') // Request was removed from database
31
+ ) {
32
+ return 'This sign-in session has expired'
33
+ } else {
34
+ return desc || 'An unknown error occurred'
26
35
  }
27
36
  }
@@ -44,11 +44,12 @@ export function SignInView({
44
44
  subtitle="Confirm your password to continue"
45
45
  >
46
46
  <SignInForm
47
- className="max-w-lg w-full"
48
47
  onSubmit={onSignIn}
49
48
  onCancel={clearSession}
50
49
  cancelAria="Back" // to account picker
51
- usernameDefault={session.account.preferred_username}
50
+ usernameDefault={
51
+ session.account.preferred_username || session.account.sub
52
+ }
52
53
  usernameReadonly={true}
53
54
  rememberDefault={true}
54
55
  />
@@ -60,7 +61,6 @@ export function SignInView({
60
61
  return (
61
62
  <LayoutTitlePage title="Sign in" subtitle="Enter your password">
62
63
  <SignInForm
63
- className="max-w-lg w-full"
64
64
  onSubmit={onSignIn}
65
65
  onCancel={onBack}
66
66
  cancelAria="Back"
@@ -77,12 +77,7 @@ export function SignInView({
77
77
  title="Sign in"
78
78
  subtitle="Enter your username and password"
79
79
  >
80
- <SignInForm
81
- className="max-w-lg w-full"
82
- onSubmit={onSignIn}
83
- onCancel={onBack}
84
- cancelAria="Back"
85
- />
80
+ <SignInForm onSubmit={onSignIn} onCancel={onBack} cancelAria="Back" />
86
81
  </LayoutTitlePage>
87
82
  )
88
83
  }
@@ -94,7 +89,6 @@ export function SignInView({
94
89
  subtitle="Enter your username and password"
95
90
  >
96
91
  <SignInForm
97
- className="max-w-lg w-full"
98
92
  onSubmit={onSignIn}
99
93
  onCancel={() => setShowSignInForm(false)}
100
94
  cancelAria="Back" // to account picker
@@ -104,12 +98,8 @@ export function SignInView({
104
98
  }
105
99
 
106
100
  return (
107
- <LayoutTitlePage
108
- title="Sign in as..."
109
- subtitle="Select an account to continue."
110
- >
101
+ <LayoutTitlePage title="Sign in" subtitle="Select from an existing account">
111
102
  <AccountPicker
112
- className="max-w-lg w-full"
113
103
  accounts={accounts}
114
104
  onAccount={(a) => setSession(a.sub)}
115
105
  onOther={() => setShowSignInForm(true)}
@@ -8,6 +8,7 @@ import {
8
8
  SignUpAccountFormOutput,
9
9
  } from '../components/sign-up-account-form'
10
10
  import { SignUpDisclaimer } from '../components/sign-up-disclaimer'
11
+ import { Button } from '../components/button'
11
12
 
12
13
  export type SignUpViewProps = {
13
14
  stepName?: (step: number, total: number) => ReactNode
@@ -57,7 +58,7 @@ export function SignUpView({
57
58
  title="Create Account"
58
59
  subtitle="We're so excited to have you join us!"
59
60
  >
60
- <div className="max-w-lg w-full flex flex-col">
61
+ <div className="flex flex-col">
61
62
  <p className="mt-4 text-slate-400 dark:text-slate-600">
62
63
  {stepName(step, stepCount)}
63
64
  </p>
@@ -76,15 +77,7 @@ export function SignUpView({
76
77
  </SignUpAccountForm>
77
78
  )}
78
79
 
79
- {step === 2 && (
80
- <button
81
- type="button"
82
- className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-slate-700 bg-slate-100 hover:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500"
83
- onClick={() => setStep(1)}
84
- >
85
- Back
86
- </button>
87
- )}
80
+ {step === 2 && <Button onClick={() => setStep(1)}>Back</Button>}
88
81
 
89
82
  <HelpCard className="mb-4" links={links} />
90
83
  </div>
@@ -1,5 +1,5 @@
1
+ import { Button } from '../components/button'
1
2
  import { LayoutWelcome, LayoutWelcomeProps } from '../components/layout-welcome'
2
- import { clsx } from '../lib/clsx'
3
3
 
4
4
  export type WelcomeViewParams = LayoutWelcomeProps & {
5
5
  onSignIn?: () => void
@@ -25,36 +25,29 @@ export function WelcomeView({
25
25
  return (
26
26
  <LayoutWelcome {...props}>
27
27
  {onSignUp && (
28
- <button
29
- className={clsx(
30
- 'm-1 w-60 max-w-full text-white py-2 px-4 rounded-full truncate',
31
- onSignIn ? 'bg-primary' : 'bg-slate-400',
32
- )}
28
+ <Button
29
+ className={'m-1 w-60 max-w-full'}
30
+ color={onSignIn ? 'brand' : undefined}
33
31
  onClick={onSignUp}
34
32
  >
35
33
  {signUpLabel}
36
- </button>
34
+ </Button>
37
35
  )}
38
36
 
39
37
  {onSignIn && (
40
- <button
41
- className={clsx(
42
- 'm-1 w-60 max-w-full py-2 px-4 rounded-full truncate',
43
- onSignUp ? 'bg-slate-100 dark:bg-slate-700' : 'bg-primary',
44
- )}
38
+ <Button
39
+ className={'m-1 w-60 max-w-full'}
40
+ color={onSignUp ? undefined : 'brand'}
45
41
  onClick={onSignIn}
46
42
  >
47
43
  {signInLabel}
48
- </button>
44
+ </Button>
49
45
  )}
50
46
 
51
47
  {onCancel && (
52
- <button
53
- className="m-1 w-60 max-w-full bg-transparent text-primary py-2 px-4 rounded-full truncate font-light"
54
- onClick={onCancel}
55
- >
48
+ <Button className="m-1 w-60 max-w-full" onClick={onCancel}>
56
49
  {cancelLabel}
57
- </button>
50
+ </Button>
58
51
  )}
59
52
  </LayoutWelcome>
60
53
  )
@@ -1,9 +1,10 @@
1
1
  import { CLIENT_ASSERTION_TYPE_JWT_BEARER } from '@atproto/oauth-types'
2
- import { KeyLike, calculateJwkThumbprint, exportJWK } from 'jose'
3
- import { JOSEError } from 'jose/errors'
2
+ import { KeyLike, calculateJwkThumbprint, errors, exportJWK } from 'jose'
4
3
 
5
4
  import { InvalidClientError } from '../errors/invalid-client-error.js'
6
5
 
6
+ const { JOSEError } = errors
7
+
7
8
  export type ClientAuth =
8
9
  | { method: 'none' }
9
10
  | {
@@ -39,7 +39,8 @@ import { Client } from './client.js'
39
39
 
40
40
  const fetchMetadataHandler = pipe(
41
41
  fetchOkProcessor(),
42
- fetchJsonProcessor('application/json', false),
42
+ // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html#section-4.1
43
+ fetchJsonProcessor('application/json', true),
43
44
  fetchJsonZodProcessor(oauthClientMetadataSchema),
44
45
  )
45
46