@atproto/oauth-provider 0.18.3 → 0.19.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 (197) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/account/account-manager.d.ts +11 -7
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js +82 -19
  5. package/dist/account/account-manager.js.map +1 -1
  6. package/dist/account/account-store.d.ts +47 -13
  7. package/dist/account/account-store.d.ts.map +1 -1
  8. package/dist/account/account-store.js +12 -9
  9. package/dist/account/account-store.js.map +1 -1
  10. package/dist/account/sign-in-data.d.ts +2 -2
  11. package/dist/account/sign-up-input.d.ts +5 -5
  12. package/dist/client/client-manager.d.ts.map +1 -1
  13. package/dist/client/client-manager.js.map +1 -1
  14. package/dist/client/client.d.ts +1 -1
  15. package/dist/client/client.d.ts.map +1 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/customization/branding.d.ts +52 -52
  18. package/dist/customization/colors.d.ts +24 -24
  19. package/dist/customization/colors.d.ts.map +1 -1
  20. package/dist/customization/customization.d.ts +77 -77
  21. package/dist/customization/links.d.ts +4 -4
  22. package/dist/device/device-manager.d.ts +20 -20
  23. package/dist/device/device-manager.d.ts.map +1 -1
  24. package/dist/device/device-manager.js.map +1 -1
  25. package/dist/device/device-store.js +1 -1
  26. package/dist/device/device-store.js.map +1 -1
  27. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  28. package/dist/dpop/dpop-manager.js.map +1 -1
  29. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  30. package/dist/dpop/dpop-nonce.js +1 -1
  31. package/dist/dpop/dpop-nonce.js.map +1 -1
  32. package/dist/errors/access-denied-error.d.ts.map +1 -1
  33. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  34. package/dist/errors/authorization-error.d.ts.map +1 -1
  35. package/dist/errors/authorization-error.js.map +1 -1
  36. package/dist/errors/consent-required-error.d.ts.map +1 -1
  37. package/dist/errors/handle-unavailable-error.d.ts +1 -1
  38. package/dist/errors/handle-unavailable-error.d.ts.map +1 -1
  39. package/dist/errors/handle-unavailable-error.js.map +1 -1
  40. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  41. package/dist/errors/invalid-client-error.d.ts.map +1 -1
  42. package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
  43. package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
  44. package/dist/errors/invalid-credentials-error.d.ts +9 -9
  45. package/dist/errors/invalid-credentials-error.d.ts.map +1 -1
  46. package/dist/errors/invalid-credentials-error.js +8 -8
  47. package/dist/errors/invalid-credentials-error.js.map +1 -1
  48. package/dist/errors/invalid-dpop-key-binding-error.d.ts.map +1 -1
  49. package/dist/errors/invalid-dpop-proof-error.d.ts.map +1 -1
  50. package/dist/errors/invalid-grant-error.d.ts.map +1 -1
  51. package/dist/errors/invalid-invite-code-error.d.ts.map +1 -1
  52. package/dist/errors/invalid-redirect-uri-error.d.ts.map +1 -1
  53. package/dist/errors/invalid-request-error.d.ts.map +1 -1
  54. package/dist/errors/invalid-scope-error.d.ts.map +1 -1
  55. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  56. package/dist/errors/invalid-token-error.js.map +1 -1
  57. package/dist/errors/login-required-error.d.ts.map +1 -1
  58. package/dist/errors/oauth-error.d.ts.map +1 -1
  59. package/dist/errors/oauth-error.js.map +1 -1
  60. package/dist/errors/second-authentication-factor-required-error.d.ts +2 -2
  61. package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -1
  62. package/dist/errors/second-authentication-factor-required-error.js.map +1 -1
  63. package/dist/errors/unauthorized-client-error.d.ts.map +1 -1
  64. package/dist/errors/use-dpop-nonce-error.d.ts.map +1 -1
  65. package/dist/errors/www-authenticate-error.d.ts.map +1 -1
  66. package/dist/lexicon/lexicon-getter.d.ts.map +1 -1
  67. package/dist/lexicon/lexicon-manager.d.ts.map +1 -1
  68. package/dist/lexicon/lexicon-store.js +1 -1
  69. package/dist/lexicon/lexicon-store.js.map +1 -1
  70. package/dist/lib/csp/index.d.ts +3 -3
  71. package/dist/lib/csp/index.d.ts.map +1 -1
  72. package/dist/lib/hcaptcha.d.ts +3 -3
  73. package/dist/lib/hcaptcha.d.ts.map +1 -1
  74. package/dist/lib/hcaptcha.js.map +1 -1
  75. package/dist/lib/html/build-document.d.ts.map +1 -1
  76. package/dist/lib/html/html.d.ts.map +1 -1
  77. package/dist/lib/html/tags.d.ts.map +1 -1
  78. package/dist/lib/http/accept.js.map +1 -1
  79. package/dist/lib/http/context.js.map +1 -1
  80. package/dist/lib/http/middleware.js.map +1 -1
  81. package/dist/lib/http/parser.d.ts +5 -5
  82. package/dist/lib/http/parser.d.ts.map +1 -1
  83. package/dist/lib/http/response.js.map +1 -1
  84. package/dist/lib/http/router.d.ts.map +1 -1
  85. package/dist/lib/http/stream.d.ts +5 -5
  86. package/dist/lib/http/stream.d.ts.map +1 -1
  87. package/dist/lib/util/authorization-header.d.ts +1 -1
  88. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  89. package/dist/lib/util/color.js.map +1 -1
  90. package/dist/lib/util/crypto.d.ts +1 -1
  91. package/dist/lib/util/crypto.d.ts.map +1 -1
  92. package/dist/lib/util/date.js.map +1 -1
  93. package/dist/lib/util/function.js +1 -1
  94. package/dist/lib/util/function.js.map +1 -1
  95. package/dist/lib/util/type.d.ts.map +1 -1
  96. package/dist/oauth-hooks.d.ts +77 -4
  97. package/dist/oauth-hooks.d.ts.map +1 -1
  98. package/dist/oauth-hooks.js.map +1 -1
  99. package/dist/oauth-middleware.js.map +1 -1
  100. package/dist/oauth-provider.d.ts +31 -31
  101. package/dist/oauth-provider.d.ts.map +1 -1
  102. package/dist/oauth-provider.js +15 -9
  103. package/dist/oauth-provider.js.map +1 -1
  104. package/dist/oauth-verifier.d.ts.map +1 -1
  105. package/dist/replay/replay-manager.d.ts.map +1 -1
  106. package/dist/replay/replay-manager.js.map +1 -1
  107. package/dist/replay/replay-store-memory.d.ts.map +1 -1
  108. package/dist/replay/replay-store-redis.d.ts.map +1 -1
  109. package/dist/request/code.d.ts.map +1 -1
  110. package/dist/request/request-data.d.ts +4 -4
  111. package/dist/request/request-data.d.ts.map +1 -1
  112. package/dist/request/request-data.js +1 -1
  113. package/dist/request/request-data.js.map +1 -1
  114. package/dist/request/request-manager.d.ts +32 -32
  115. package/dist/request/request-manager.d.ts.map +1 -1
  116. package/dist/request/request-manager.js +7 -4
  117. package/dist/request/request-manager.js.map +1 -1
  118. package/dist/request/request-store.d.ts +1 -1
  119. package/dist/request/request-store.js +2 -2
  120. package/dist/request/request-store.js.map +1 -1
  121. package/dist/result/authorization-result-authorize-page.d.ts +1 -1
  122. package/dist/result/authorization-result-authorize-page.js.map +1 -1
  123. package/dist/router/assets/assets-manifest.js.map +1 -1
  124. package/dist/router/assets/assets.d.ts +1 -1
  125. package/dist/router/assets/assets.d.ts.map +1 -1
  126. package/dist/router/assets/assets.js.map +1 -1
  127. package/dist/router/assets/send-account-page.d.ts.map +1 -1
  128. package/dist/router/assets/send-authorization-page.d.ts.map +1 -1
  129. package/dist/router/assets/send-authorization-page.js +1 -1
  130. package/dist/router/assets/send-authorization-page.js.map +1 -1
  131. package/dist/router/assets/send-cookie-error-page.d.ts.map +1 -1
  132. package/dist/router/assets/send-error-page.d.ts.map +1 -1
  133. package/dist/router/assets/send-redirect.d.ts +2 -2
  134. package/dist/router/assets/send-redirect.d.ts.map +1 -1
  135. package/dist/router/create-api-middleware.d.ts.map +1 -1
  136. package/dist/router/create-api-middleware.js +87 -51
  137. package/dist/router/create-api-middleware.js.map +1 -1
  138. package/dist/signer/access-token-payload.d.ts +1762 -1762
  139. package/dist/signer/access-token-payload.d.ts.map +1 -1
  140. package/dist/signer/access-token-payload.js +2 -2
  141. package/dist/signer/access-token-payload.js.map +1 -1
  142. package/dist/signer/api-token-payload.d.ts +1738 -1738
  143. package/dist/signer/api-token-payload.d.ts.map +1 -1
  144. package/dist/signer/api-token-payload.js +2 -2
  145. package/dist/signer/api-token-payload.js.map +1 -1
  146. package/dist/signer/signer.d.ts +6 -187
  147. package/dist/signer/signer.d.ts.map +1 -1
  148. package/dist/signer/signer.js.map +1 -1
  149. package/dist/token/refresh-token.d.ts.map +1 -1
  150. package/dist/token/token-claims.d.ts +2 -1
  151. package/dist/token/token-claims.d.ts.map +1 -1
  152. package/dist/token/token-claims.js.map +1 -1
  153. package/dist/token/token-data.d.ts +3 -3
  154. package/dist/token/token-data.d.ts.map +1 -1
  155. package/dist/token/token-data.js.map +1 -1
  156. package/dist/token/token-id.d.ts.map +1 -1
  157. package/dist/token/token-manager.d.ts +3 -3
  158. package/dist/token/token-manager.d.ts.map +1 -1
  159. package/dist/token/token-manager.js +14 -14
  160. package/dist/token/token-manager.js.map +1 -1
  161. package/dist/token/token-store.d.ts +3 -3
  162. package/dist/token/token-store.d.ts.map +1 -1
  163. package/dist/token/token-store.js +3 -3
  164. package/dist/token/token-store.js.map +1 -1
  165. package/dist/types/handle.d.ts +5 -1
  166. package/dist/types/handle.d.ts.map +1 -1
  167. package/dist/types/handle.js +9 -8
  168. package/dist/types/handle.js.map +1 -1
  169. package/package.json +19 -19
  170. package/src/account/account-manager.ts +123 -20
  171. package/src/account/account-store.ts +67 -19
  172. package/src/device/device-store.ts +1 -1
  173. package/src/errors/invalid-credentials-error.ts +8 -8
  174. package/src/lexicon/lexicon-store.ts +1 -1
  175. package/src/lib/util/function.ts +2 -2
  176. package/src/oauth-hooks.ts +85 -3
  177. package/src/oauth-provider.ts +17 -9
  178. package/src/request/request-data.ts +5 -5
  179. package/src/request/request-manager.ts +11 -4
  180. package/src/request/request-store.ts +3 -3
  181. package/src/result/authorization-result-authorize-page.ts +1 -1
  182. package/src/router/assets/send-authorization-page.ts +1 -1
  183. package/src/router/create-api-middleware.ts +128 -67
  184. package/src/signer/access-token-payload.ts +2 -2
  185. package/src/signer/api-token-payload.ts +2 -2
  186. package/src/signer/signer.ts +13 -4
  187. package/src/token/token-claims.ts +2 -1
  188. package/src/token/token-data.ts +3 -3
  189. package/src/token/token-manager.ts +15 -15
  190. package/src/token/token-store.ts +6 -6
  191. package/src/types/handle.ts +21 -16
  192. package/tsconfig.build.tsbuildinfo +1 -1
  193. package/dist/oidc/sub.d.ts +0 -4
  194. package/dist/oidc/sub.d.ts.map +0 -1
  195. package/dist/oidc/sub.js +0 -3
  196. package/dist/oidc/sub.js.map +0 -1
  197. package/src/oidc/sub.ts +0 -4
@@ -1,12 +1,17 @@
1
+ import { Did } from '@atproto/did'
1
2
  import type {
2
3
  Account,
4
+ ConfirmAccountDeletionInput,
3
5
  ConfirmEmailUpdateInput,
4
6
  ConfirmEmailVerificationInput,
5
7
  ConfirmResetPasswordInput,
8
+ DeactivateAccountInput,
9
+ InitiateAccountDeletionInput,
6
10
  InitiateEmailUpdateInput,
7
11
  InitiateEmailUpdateOutput,
8
12
  InitiateEmailVerificationInput,
9
13
  InitiatePasswordResetInput,
14
+ ReactivateAccountInput,
10
15
  UpdateHandleInput,
11
16
  } from '@atproto/oauth-provider-api'
12
17
  import { OAuthScope } from '@atproto/oauth-types'
@@ -21,7 +26,6 @@ import {
21
26
  InvalidRequestError,
22
27
  SecondAuthenticationFactorRequiredError,
23
28
  } from '../oauth-errors.js'
24
- import { Sub } from '../oidc/sub.js'
25
29
  import { InviteCode } from '../types/invite-code.js'
26
30
  import { SignUpInput } from './sign-up-input.js'
27
31
 
@@ -30,11 +34,11 @@ import { SignUpInput } from './sign-up-input.js'
30
34
  export * from '../client/client-id.js'
31
35
  export * from '../device/device-data.js'
32
36
  export * from '../device/device-id.js'
33
- export * from '../oidc/sub.js'
34
37
  export * from '../request/request-id.js'
35
38
 
36
39
  export type {
37
40
  Account,
41
+ Did,
38
42
  HcaptchaVerifyResult,
39
43
  InviteCode,
40
44
  OAuthScope,
@@ -58,6 +62,11 @@ export type VerifyEmailRequestInput = InitiateEmailVerificationInput
58
62
  export type VerifyEmailConfirmInput = ConfirmEmailVerificationInput
59
63
  export type UpdateHandleData = UpdateHandleInput
60
64
 
65
+ export type DeactivateAccountData = DeactivateAccountInput
66
+ export type ReactivateAccountData = ReactivateAccountInput
67
+ export type DeleteAccountRequestInput = InitiateAccountDeletionInput
68
+ export type DeleteAccountConfirmInput = ConfirmAccountDeletionInput
69
+
61
70
  export type CreateAccountData = {
62
71
  locale: string
63
72
  email: string
@@ -124,7 +133,7 @@ export interface AccountStore {
124
133
 
125
134
  /**
126
135
  * @throws {InvalidCredentialsError} - When the credentials are not valid.
127
- * Populate {@link InvalidCredentialsError.sub} with the subject identifier
136
+ * Populate {@link InvalidCredentialsError.did} with the subject identifier
128
137
  * when the identifier matched an existing account (e.g. wrong password for
129
138
  * a known user); omit it when the identifier was not found. Throwing the
130
139
  * generic {@link InvalidRequestError} is also accepted for backward
@@ -138,7 +147,7 @@ export interface AccountStore {
138
147
  * Add a client & scopes to the list of authorized clients for the given account.
139
148
  */
140
149
  setAuthorizedClient(
141
- sub: Sub,
150
+ did: Did,
142
151
  clientId: ClientId,
143
152
  data: AuthorizedClientData,
144
153
  ): Awaitable<void>
@@ -146,7 +155,7 @@ export interface AccountStore {
146
155
  /**
147
156
  * @throws {InvalidRequestError} - When the credentials are not valid
148
157
  */
149
- getAccount(sub: Sub): Awaitable<{
158
+ getAccount(did: Did): Awaitable<{
150
159
  account: Account
151
160
  authorizedClients: AuthorizedClients
152
161
  }>
@@ -162,7 +171,7 @@ export interface AccountStore {
162
171
  * {@link RequestStore.deleteRequest}), all accounts bound to that request
163
172
  * should be deleted as well.
164
173
  */
165
- upsertDeviceAccount(deviceId: DeviceId, sub: Sub): Awaitable<void>
174
+ upsertDeviceAccount(deviceId: DeviceId, did: Did): Awaitable<void>
166
175
 
167
176
  /**
168
177
  * @param requestId - If provided, the result must either have the same
@@ -173,7 +182,7 @@ export interface AccountStore {
173
182
  */
174
183
  getDeviceAccount(
175
184
  deviceId: DeviceId,
176
- sub: Sub,
185
+ did: Did,
177
186
  ): Awaitable<DeviceAccount | null>
178
187
 
179
188
  /**
@@ -182,14 +191,14 @@ export interface AccountStore {
182
191
  *
183
192
  * @note Noop if the device-account is not found.
184
193
  */
185
- removeDeviceAccount(deviceId: DeviceId, sub: Sub): Awaitable<void>
194
+ removeDeviceAccount(deviceId: DeviceId, did: Did): Awaitable<void>
186
195
 
187
196
  /**
188
197
  * @returns **all** the device accounts that match the {@link requestId}
189
198
  * criteria and given {@link filter}.
190
199
  */
191
200
  listDeviceAccounts(
192
- filter: { sub: Sub } | { deviceId: DeviceId },
201
+ filter: { did: Did } | { deviceId: DeviceId },
193
202
  ): Awaitable<DeviceAccount[]>
194
203
 
195
204
  resetPasswordRequest(
@@ -206,8 +215,8 @@ export interface AccountStore {
206
215
  /**
207
216
  * Must trigger a verification email to be sent to the new email address, that
208
217
  * will then be confirmed through {@link updateEmailConfirm}. The account's
209
- * `email_verified` field is expected to become `false` until the new email is
210
- * confirmed.
218
+ * {@link Account['emailVerified'] emailVerified} field is expected to become
219
+ * `false` until the new email is confirmed.
211
220
  */
212
221
  updateEmailConfirm(data: UpdateEmailConfirmInput): Awaitable<Account | null>
213
222
 
@@ -225,25 +234,64 @@ export interface AccountStore {
225
234
  * cannot be used
226
235
  */
227
236
  updateHandle(data: UpdateHandleData): Awaitable<Account>
237
+
238
+ /**
239
+ * Mark the account as deactivated. The account remains recoverable. Should
240
+ * be a no-op when the account is already deactivated.
241
+ *
242
+ * @throws {InvalidRequestError} - When the account cannot be deactivated
243
+ * (e.g. unknown account)
244
+ */
245
+ deactivateAccount(data: DeactivateAccountData): Awaitable<Account>
246
+
247
+ /**
248
+ * Reactivate a previously-deactivated account. Should be a no-op when the
249
+ * account is already active.
250
+ *
251
+ * @throws {InvalidRequestError} - When the account cannot be reactivated
252
+ * (e.g. unknown account)
253
+ */
254
+ reactivateAccount(data: ReactivateAccountData): Awaitable<Account>
255
+
256
+ /**
257
+ * Initiate account deletion: typically sends a confirmation token to the
258
+ * account's email address. The account is NOT deleted until
259
+ * {@link deleteAccountConfirm} is called with the matching token and the
260
+ * user's current password.
261
+ */
262
+ deleteAccountRequest(data: DeleteAccountRequestInput): Awaitable<void>
263
+
264
+ /**
265
+ * Finalize account deletion. Implementations MUST verify both the email
266
+ * confirmation `token` issued by {@link deleteAccountRequest} and the user's
267
+ * current `password` before deleting any data. Deletion is irreversible.
268
+ *
269
+ * @throws {InvalidRequestError} - When the token or password is invalid.
270
+ */
271
+ deleteAccountConfirm(data: DeleteAccountConfirmInput): Awaitable<void>
228
272
  }
229
273
 
230
274
  export const isAccountStore = buildInterfaceChecker<AccountStore>([
231
- 'createAccount',
232
275
  'authenticateAccount',
233
- 'setAuthorizedClient',
276
+ 'createAccount',
277
+ 'deactivateAccount',
278
+ 'deleteAccountConfirm',
279
+ 'deleteAccountRequest',
234
280
  'getAccount',
235
- 'upsertDeviceAccount',
236
281
  'getDeviceAccount',
237
- 'removeDeviceAccount',
238
282
  'listDeviceAccounts',
239
- 'resetPasswordRequest',
283
+ 'reactivateAccount',
284
+ 'removeDeviceAccount',
240
285
  'resetPasswordConfirm',
241
- 'updateEmailRequest',
286
+ 'resetPasswordRequest',
287
+ 'setAuthorizedClient',
242
288
  'updateEmailConfirm',
243
- 'verifyEmailRequest',
289
+ 'updateEmailRequest',
290
+ 'updateHandle',
291
+ 'upsertDeviceAccount',
244
292
  'verifyEmailConfirm',
293
+ 'verifyEmailRequest',
245
294
  'verifyHandleAvailability',
246
- 'updateHandle',
247
295
  ])
248
296
 
249
297
  export function asAccountStore<V>(implementation: V): V & AccountStore {
@@ -18,9 +18,9 @@ export interface DeviceStore {
18
18
 
19
19
  export const isDeviceStore = buildInterfaceChecker<DeviceStore>([
20
20
  'createDevice',
21
+ 'deleteDevice',
21
22
  'readDevice',
22
23
  'updateDevice',
23
- 'deleteDevice',
24
24
  ])
25
25
 
26
26
  export function asDeviceStore<V>(implementation: V): V & DeviceStore {
@@ -1,4 +1,4 @@
1
- import { Sub } from '../oidc/sub.js'
1
+ import { Did } from '@atproto/did'
2
2
  import { InvalidRequestError } from './invalid-request-error.js'
3
3
 
4
4
  /**
@@ -6,22 +6,22 @@ import { InvalidRequestError } from './invalid-request-error.js'
6
6
  * that a sign-in attempt was rejected because the provided credentials did not
7
7
  * match a known account.
8
8
  *
9
- * Stores should populate {@link InvalidCredentialsError.sub} when the
10
- * identifier resolved to an existing account but e.g. the password or OTP was
11
- * incorrect. The identifier-unknown case should leave `sub` unset. This
12
- * information is surfaced to the `onSignInFailed` hook and never sent back to
13
- * the client, so populating it does not affect the client-visible response.
9
+ * Stores should populate {@link did}. when the identifier resolved to an
10
+ * existing account but e.g. the password or OTP was incorrect. The
11
+ * identifier-unknown case should leave {@link did} unset. This information is
12
+ * surfaced to the `onSignInFailed` hook and never sent back to the client, so
13
+ * populating it does not affect the client-visible response.
14
14
  *
15
15
  * Only the subject identifier (DID) is carried — not a full `Account` — to
16
16
  * avoid embedding PII (email, name, etc.) in an error that may be serialized
17
17
  * by loggers or monitoring tools walking the `.cause` chain. Hook consumers
18
18
  * that need richer profile info can resolve it from their own account store
19
- * using `sub`.
19
+ * using {@link did}.
20
20
  */
21
21
  export class InvalidCredentialsError extends InvalidRequestError {
22
22
  constructor(
23
23
  message = 'Invalid identifier or password',
24
- public readonly sub?: Sub,
24
+ public readonly did?: Did,
25
25
  cause?: unknown,
26
26
  ) {
27
27
  super(message, cause)
@@ -10,9 +10,9 @@ export interface LexiconStore {
10
10
  }
11
11
 
12
12
  export const isLexiconStore = buildInterfaceChecker<LexiconStore>([
13
+ 'deleteLexicon',
13
14
  'findLexicon',
14
15
  'storeLexicon',
15
- 'deleteLexicon',
16
16
  ])
17
17
 
18
18
  export function ifLexiconStore<V extends Partial<LexiconStore>>(
@@ -16,8 +16,8 @@ export async function callAsync<F extends (...args: any[]) => unknown>(
16
16
  export async function callAsync<F extends (...args: any[]) => unknown>(
17
17
  fn?: F,
18
18
  ...args: Parameters<F>
19
- ): Promise<Awaited<ReturnType<F>> | undefined> {
20
- return (await fn?.(...args)) as Awaited<ReturnType<F>> | undefined
19
+ ): Promise<unknown> {
20
+ return fn?.(...args)
21
21
  }
22
22
 
23
23
  export function invokeOnce<T extends (this: any, ...a: any[]) => any>(
@@ -1,3 +1,4 @@
1
+ import { Did } from '@atproto/did'
1
2
  import { Jwks } from '@atproto/jwk'
2
3
  import type { Account } from '@atproto/oauth-provider-api'
3
4
  import {
@@ -9,6 +10,7 @@ import {
9
10
  OAuthTokenType,
10
11
  } from '@atproto/oauth-types'
11
12
  import {
13
+ DeleteAccountConfirmInput,
12
14
  ResetPasswordConfirmInput,
13
15
  ResetPasswordRequestInput,
14
16
  SignUpData,
@@ -38,7 +40,6 @@ import {
38
40
  } from './lib/hcaptcha.js'
39
41
  import { RequestMetadata } from './lib/http/request.js'
40
42
  import { Awaitable, OmitKey } from './lib/util/type.js'
41
- import { Sub } from './oidc/sub.js'
42
43
  import { RequestId } from './request/request-id.js'
43
44
  import { AccessTokenPayload } from './signer/access-token-payload.js'
44
45
  import { TokenClaims } from './token/token-claims.js'
@@ -55,6 +56,7 @@ export {
55
56
  type ClientId,
56
57
  type ClientInfo,
57
58
  type DeviceId,
59
+ type Did,
58
60
  type DpopProof,
59
61
  type HcaptchaClientTokens,
60
62
  type HcaptchaConfig,
@@ -75,7 +77,6 @@ export {
75
77
  type SignInData,
76
78
  type SignUpData,
77
79
  type SignUpInput,
78
- type Sub,
79
80
  type TokenClaims,
80
81
  type UpdateHandleData,
81
82
  }
@@ -208,6 +209,87 @@ export type OAuthHooks = {
208
209
  account: Account
209
210
  }) => Awaitable<void>
210
211
 
212
+ /**
213
+ * This hook is called when a user requests account deactivation, before the
214
+ * account is deactivated on the account store.
215
+ */
216
+ onDeactivateAccount?: (data: {
217
+ deviceId: DeviceId
218
+ deviceMetadata: RequestMetadata
219
+ account: Account
220
+ }) => Awaitable<void>
221
+
222
+ /**
223
+ * This hook is called after a user successfully deactivated their account on
224
+ * the account store.
225
+ */
226
+ onDeactivatedAccount?: (data: {
227
+ deviceId: DeviceId
228
+ deviceMetadata: RequestMetadata
229
+ account: Account
230
+ }) => Awaitable<void>
231
+
232
+ /**
233
+ * This hook is called when a user requests account reactivation, before the
234
+ * account is reactivated on the account store.
235
+ */
236
+ onReactivateAccount?: (data: {
237
+ deviceId: DeviceId
238
+ deviceMetadata: RequestMetadata
239
+ account: Account
240
+ }) => Awaitable<void>
241
+
242
+ /**
243
+ * This hook is called after a user successfully reactivated their account on
244
+ * the account store.
245
+ */
246
+ onReactivatedAccount?: (data: {
247
+ deviceId: DeviceId
248
+ deviceMetadata: RequestMetadata
249
+ account: Account
250
+ }) => Awaitable<void>
251
+
252
+ /**
253
+ * This hook is called when a user requests account deletion, before the
254
+ * confirmation email is dispatched on the account store.
255
+ */
256
+ onDeleteAccountRequest?: (data: {
257
+ deviceId: DeviceId
258
+ deviceMetadata: RequestMetadata
259
+ account: Account
260
+ }) => Awaitable<void>
261
+
262
+ /**
263
+ * This hook is called after a user has requested account deletion and the
264
+ * confirmation email has been dispatched.
265
+ */
266
+ onDeleteAccountRequested?: (data: {
267
+ deviceId: DeviceId
268
+ deviceMetadata: RequestMetadata
269
+ account: Account
270
+ }) => Awaitable<void>
271
+
272
+ /**
273
+ * This hook is called when a user confirms account deletion, before the
274
+ * account is actually deleted on the account store.
275
+ */
276
+ onDeleteAccountConfirm?: (data: {
277
+ deviceId: DeviceId
278
+ deviceMetadata: RequestMetadata
279
+ account: Account
280
+ }) => Awaitable<void>
281
+
282
+ /**
283
+ * This hook is called after a user has successfully deleted their account
284
+ * on the account store.
285
+ */
286
+ onDeleteAccountConfirmed?: (data: {
287
+ deviceId: DeviceId
288
+ deviceMetadata: RequestMetadata
289
+ account: Account
290
+ input: DeleteAccountConfirmInput
291
+ }) => Awaitable<void>
292
+
211
293
  /**
212
294
  * This hook is called when a user attempts to sign up, after every validation
213
295
  * has passed (including hcaptcha).
@@ -336,7 +418,7 @@ export type OAuthHooks = {
336
418
  onSignInFailed?: (data: {
337
419
  data: SignInData
338
420
  error: InvalidRequestError
339
- sub: Sub | null
421
+ did: Did | null
340
422
  deviceId: DeviceId
341
423
  deviceMetadata: RequestMetadata
342
424
  clientId?: ClientId
@@ -642,6 +642,10 @@ export class OAuthProvider extends OAuthVerifier {
642
642
  throw new AccountSelectionRequiredError(parameters)
643
643
  }
644
644
 
645
+ const ssoSessions = sessions
646
+ .filter(hasActiveAccount)
647
+ .filter(matchesHint, parameters)
648
+
645
649
  // prompt=none
646
650
  //
647
651
  // > The Authorization Server MUST NOT display any authentication or
@@ -653,7 +657,6 @@ export class OAuthProvider extends OAuthVerifier {
653
657
  // > Section 3.1.2.6. This can be used as a method to check for existing
654
658
  // > authentication and/or consent.
655
659
  if (parameters.prompt === 'none') {
656
- const ssoSessions = sessions.filter(matchesHint, parameters)
657
660
  if (ssoSessions.length > 1) {
658
661
  throw new AccountSelectionRequiredError(parameters)
659
662
  }
@@ -682,7 +685,6 @@ export class OAuthProvider extends OAuthVerifier {
682
685
 
683
686
  // Automatic SSO when a hint was provided that matches a single session
684
687
  if (parameters.prompt == null && parameters.login_hint != null) {
685
- const ssoSessions = sessions.filter(matchesHint, parameters)
686
688
  if (ssoSessions.length === 1) {
687
689
  const ssoSession = ssoSessions[0]!
688
690
  if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
@@ -704,12 +706,14 @@ export class OAuthProvider extends OAuthVerifier {
704
706
  client,
705
707
  parameters,
706
708
  requestUri,
707
- sessions,
708
- selectedSub:
709
+ sessions: sessions
710
+ // Strip un-necessary information (like consentRequired)
711
+ .map(({ account, loginRequired }) => ({ account, loginRequired })),
712
+ selectedDid:
709
713
  parameters.prompt == null ||
710
714
  parameters.prompt === 'login' ||
711
715
  parameters.prompt === 'consent'
712
- ? sessions.find(matchesHint, parameters)?.account.sub
716
+ ? sessions.find(matchesHint, parameters)?.account.did
713
717
  : undefined,
714
718
  permissionSets: await this.lexiconManager
715
719
  .getPermissionSetsFromScope(parameters.scope)
@@ -884,9 +888,9 @@ export class OAuthProvider extends OAuthVerifier {
884
888
  // As an additional security measure, we also sign the device out,
885
889
  // so that the device cannot be used to access the account anymore
886
890
  // without a new authentication.
887
- const { deviceId, sub } = tokenInfo.data
891
+ const { deviceId, did } = tokenInfo.data
888
892
  if (deviceId) {
889
- await this.accountManager.removeDeviceAccount(deviceId, sub)
893
+ await this.accountManager.removeDeviceAccount(deviceId, did)
890
894
  }
891
895
  }
892
896
  }
@@ -912,7 +916,7 @@ export class OAuthProvider extends OAuthVerifier {
912
916
 
913
917
  await this.validateCodeGrant(parameters, input)
914
918
 
915
- const { account } = await this.accountManager.getAccount(data.sub)
919
+ const { account } = await this.accountManager.getAccount(data.did)
916
920
 
917
921
  return this.tokenManager.createToken(
918
922
  client,
@@ -1098,5 +1102,9 @@ function matchesHint(
1098
1102
  const hint = this.login_hint
1099
1103
  if (!hint) return false
1100
1104
 
1101
- return account.sub === hint || account.preferred_username === hint
1105
+ return account.did === hint || account.handle === hint
1106
+ }
1107
+
1108
+ function hasActiveAccount({ account }: { account: Account }): boolean {
1109
+ return !account.deactivated
1102
1110
  }
@@ -1,9 +1,9 @@
1
+ import { Did } from '@atproto/did'
1
2
  import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
3
  import { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'
3
4
  import { ClientId } from '../client/client-id.js'
4
5
  import { DeviceId } from '../device/device-id.js'
5
6
  import { NonNullableKeys } from '../lib/util/type.js'
6
- import { Sub } from '../oidc/sub.js'
7
7
  import { Code } from './code.js'
8
8
 
9
9
  export type {
@@ -12,8 +12,8 @@ export type {
12
12
  ClientId,
13
13
  Code,
14
14
  DeviceId,
15
+ Did,
15
16
  OAuthAuthorizationRequestParameters,
16
- Sub,
17
17
  }
18
18
 
19
19
  export type RequestData = {
@@ -22,15 +22,15 @@ export type RequestData = {
22
22
  parameters: Readonly<OAuthAuthorizationRequestParameters>
23
23
  expiresAt: Date
24
24
  deviceId: DeviceId | null
25
- sub: Sub | null
25
+ did: Did | null
26
26
  code: Code | null
27
27
  }
28
28
 
29
29
  export type RequestDataAuthorized = NonNullableKeys<
30
30
  RequestData,
31
- 'sub' | 'deviceId'
31
+ 'did' | 'deviceId'
32
32
  >
33
33
 
34
34
  export const isRequestDataAuthorized = (
35
35
  data: RequestData,
36
- ): data is RequestDataAuthorized => data.sub !== null && data.deviceId !== null
36
+ ): data is RequestDataAuthorized => data.did !== null && data.deviceId !== null
@@ -78,7 +78,7 @@ export class RequestManager {
78
78
  parameters,
79
79
  expiresAt,
80
80
  deviceId,
81
- sub: null,
81
+ did: null,
82
82
  code: null,
83
83
  })
84
84
 
@@ -337,7 +337,7 @@ export class RequestManager {
337
337
  const updates: UpdateRequestData = {}
338
338
 
339
339
  try {
340
- if (data.sub || data.code) {
340
+ if (data.did || data.code) {
341
341
  // If an account was linked to the request, the next step is to exchange
342
342
  // the code for a token.
343
343
  throw new AccessDeniedError(
@@ -404,6 +404,13 @@ export class RequestManager {
404
404
  let { parameters } = data
405
405
 
406
406
  try {
407
+ if (account.deactivated) {
408
+ throw new AccessDeniedError(
409
+ parameters,
410
+ 'This account has been deactivated',
411
+ )
412
+ }
413
+
407
414
  if (data.expiresAt < new Date()) {
408
415
  throw new AccessDeniedError(parameters, 'This request has expired')
409
416
  }
@@ -419,7 +426,7 @@ export class RequestManager {
419
426
  'This request was initiated from another device',
420
427
  )
421
428
  }
422
- if (data.sub || data.code) {
429
+ if (data.did || data.code) {
423
430
  throw new AccessDeniedError(
424
431
  parameters,
425
432
  'This request was already authorized',
@@ -453,7 +460,7 @@ export class RequestManager {
453
460
 
454
461
  // Bind the request to the account, preventing it from being used again.
455
462
  await this.store.updateRequest(requestId, {
456
- sub: account.sub,
463
+ did: account.did,
457
464
  code,
458
465
  // Allow the client to exchange the code for a token within the next 60 seconds.
459
466
  expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
@@ -12,7 +12,7 @@ export type { Awaitable }
12
12
 
13
13
  export type UpdateRequestData = Pick<
14
14
  Partial<RequestData>,
15
- 'sub' | 'code' | 'deviceId' | 'expiresAt' | 'parameters'
15
+ 'did' | 'code' | 'deviceId' | 'expiresAt' | 'parameters'
16
16
  >
17
17
 
18
18
  export type FoundRequestResult = {
@@ -43,11 +43,11 @@ export interface RequestStore {
43
43
  }
44
44
 
45
45
  export const isRequestStore = buildInterfaceChecker<RequestStore>([
46
+ 'consumeRequestCode',
46
47
  'createRequest',
48
+ 'deleteRequest',
47
49
  'readRequest',
48
50
  'updateRequest',
49
- 'deleteRequest',
50
- 'consumeRequestCode',
51
51
  ])
52
52
 
53
53
  export function asRequestStore<V extends Partial<RequestStore>>(
@@ -12,5 +12,5 @@ export type AuthorizationResultAuthorizePage = {
12
12
 
13
13
  requestUri: RequestUri
14
14
  sessions: readonly Session[]
15
- selectedSub?: Account['sub']
15
+ selectedDid?: Account['did']
16
16
  }
@@ -33,7 +33,7 @@ export function sendAuthorizePageFactory(
33
33
  loginHint: data.parameters.login_hint,
34
34
  promptMode: data.parameters.prompt,
35
35
  permissionSets: Object.fromEntries(data.permissionSets),
36
- selectedSub: data.selectedSub,
36
+ selectedDid: data.selectedDid,
37
37
  },
38
38
  __sessions: data.sessions,
39
39
  },