@atproto/oauth-provider 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. package/CHANGELOG.md +13 -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/{send-authorize-page.d.ts → build-authorize-data.d.ts} +2 -5
  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 +6 -6
  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/dist/output/send-authorize-page.d.ts.map +0 -1
  118. package/dist/output/send-authorize-page.js +0 -49
  119. package/dist/output/send-authorize-page.js.map +0 -1
  120. package/dist/output/send-error-page.d.ts +0 -5
  121. package/dist/output/send-error-page.d.ts.map +0 -1
  122. package/dist/output/send-error-page.js +0 -31
  123. package/dist/output/send-error-page.js.map +0 -1
  124. 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