@atproto/oauth-provider 0.18.4 → 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 (91) hide show
  1. package/CHANGELOG.md +16 -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 +4 -1
  9. package/dist/account/account-store.js.map +1 -1
  10. package/dist/account/sign-up-input.d.ts +2 -2
  11. package/dist/errors/invalid-credentials-error.d.ts +9 -9
  12. package/dist/errors/invalid-credentials-error.d.ts.map +1 -1
  13. package/dist/errors/invalid-credentials-error.js +8 -8
  14. package/dist/errors/invalid-credentials-error.js.map +1 -1
  15. package/dist/lib/util/function.js +1 -1
  16. package/dist/lib/util/function.js.map +1 -1
  17. package/dist/oauth-hooks.d.ts +77 -4
  18. package/dist/oauth-hooks.d.ts.map +1 -1
  19. package/dist/oauth-hooks.js.map +1 -1
  20. package/dist/oauth-provider.d.ts.map +1 -1
  21. package/dist/oauth-provider.js +15 -9
  22. package/dist/oauth-provider.js.map +1 -1
  23. package/dist/request/request-data.d.ts +4 -4
  24. package/dist/request/request-data.d.ts.map +1 -1
  25. package/dist/request/request-data.js +1 -1
  26. package/dist/request/request-data.js.map +1 -1
  27. package/dist/request/request-manager.d.ts.map +1 -1
  28. package/dist/request/request-manager.js +7 -4
  29. package/dist/request/request-manager.js.map +1 -1
  30. package/dist/request/request-store.d.ts +1 -1
  31. package/dist/request/request-store.js.map +1 -1
  32. package/dist/result/authorization-result-authorize-page.d.ts +1 -1
  33. package/dist/result/authorization-result-authorize-page.js.map +1 -1
  34. package/dist/router/assets/send-authorization-page.js +1 -1
  35. package/dist/router/assets/send-authorization-page.js.map +1 -1
  36. package/dist/router/create-api-middleware.d.ts.map +1 -1
  37. package/dist/router/create-api-middleware.js +87 -51
  38. package/dist/router/create-api-middleware.js.map +1 -1
  39. package/dist/signer/access-token-payload.d.ts +15 -15
  40. package/dist/signer/access-token-payload.js +2 -2
  41. package/dist/signer/access-token-payload.js.map +1 -1
  42. package/dist/signer/api-token-payload.d.ts +15 -15
  43. package/dist/signer/api-token-payload.js +2 -2
  44. package/dist/signer/api-token-payload.js.map +1 -1
  45. package/dist/signer/signer.d.ts +6 -187
  46. package/dist/signer/signer.d.ts.map +1 -1
  47. package/dist/signer/signer.js.map +1 -1
  48. package/dist/token/token-claims.d.ts +2 -1
  49. package/dist/token/token-claims.d.ts.map +1 -1
  50. package/dist/token/token-claims.js.map +1 -1
  51. package/dist/token/token-data.d.ts +3 -3
  52. package/dist/token/token-data.d.ts.map +1 -1
  53. package/dist/token/token-data.js.map +1 -1
  54. package/dist/token/token-manager.d.ts +3 -3
  55. package/dist/token/token-manager.d.ts.map +1 -1
  56. package/dist/token/token-manager.js +14 -14
  57. package/dist/token/token-manager.js.map +1 -1
  58. package/dist/token/token-store.d.ts +3 -3
  59. package/dist/token/token-store.d.ts.map +1 -1
  60. package/dist/token/token-store.js.map +1 -1
  61. package/dist/types/handle.d.ts +5 -1
  62. package/dist/types/handle.d.ts.map +1 -1
  63. package/dist/types/handle.js +9 -8
  64. package/dist/types/handle.js.map +1 -1
  65. package/package.json +10 -10
  66. package/src/account/account-manager.ts +123 -20
  67. package/src/account/account-store.ts +59 -11
  68. package/src/errors/invalid-credentials-error.ts +8 -8
  69. package/src/lib/util/function.ts +2 -2
  70. package/src/oauth-hooks.ts +85 -3
  71. package/src/oauth-provider.ts +17 -9
  72. package/src/request/request-data.ts +5 -5
  73. package/src/request/request-manager.ts +11 -4
  74. package/src/request/request-store.ts +1 -1
  75. package/src/result/authorization-result-authorize-page.ts +1 -1
  76. package/src/router/assets/send-authorization-page.ts +1 -1
  77. package/src/router/create-api-middleware.ts +128 -67
  78. package/src/signer/access-token-payload.ts +2 -2
  79. package/src/signer/api-token-payload.ts +2 -2
  80. package/src/signer/signer.ts +13 -4
  81. package/src/token/token-claims.ts +2 -1
  82. package/src/token/token-data.ts +3 -3
  83. package/src/token/token-manager.ts +15 -15
  84. package/src/token/token-store.ts +3 -3
  85. package/src/types/handle.ts +21 -16
  86. package/tsconfig.build.tsbuildinfo +1 -1
  87. package/dist/oidc/sub.d.ts +0 -4
  88. package/dist/oidc/sub.d.ts.map +0 -1
  89. package/dist/oidc/sub.js +0 -3
  90. package/dist/oidc/sub.js.map +0 -1
  91. package/src/oidc/sub.ts +0 -4
@@ -1,3 +1,4 @@
1
+ import { Did } from '@atproto/did'
1
2
  import {
2
3
  OAuthIssuerIdentifier,
3
4
  isOAuthClientIdLoopback,
@@ -12,11 +13,12 @@ import { callAsync } from '../lib/util/function.js'
12
13
  import { constantTime } from '../lib/util/time.js'
13
14
  import { OAuthHooks, RequestMetadata } from '../oauth-hooks.js'
14
15
  import { Customization } from '../oauth-provider.js'
15
- import { Sub } from '../oidc/sub.js'
16
16
  import {
17
17
  Account,
18
18
  AccountStore,
19
19
  AuthorizedClientData,
20
+ DeleteAccountConfirmInput,
21
+ DeleteAccountRequestInput,
20
22
  DeviceAccount,
21
23
  ResetPasswordConfirmInput,
22
24
  ResetPasswordRequestInput,
@@ -152,7 +154,7 @@ export class AccountManager {
152
154
 
153
155
  return account
154
156
  } catch (err) {
155
- await this.removeDeviceAccount(deviceId, account.sub)
157
+ await this.removeDeviceAccount(deviceId, account.did)
156
158
 
157
159
  throw InvalidRequestError.from(
158
160
  err,
@@ -190,7 +192,7 @@ export class AccountManager {
190
192
  // This information is only exposed to the hook and is never
191
193
  // surfaced to the client.
192
194
  const isCredentialsError = err instanceof InvalidCredentialsError
193
- const sub = isCredentialsError ? err.sub ?? null : null
195
+ const did = isCredentialsError ? err.did ?? null : null
194
196
 
195
197
  // Swallow any error from the hook itself so that it does not mask
196
198
  // the underlying authentication failure being reported.
@@ -198,7 +200,7 @@ export class AccountManager {
198
200
  await this.hooks.onSignInFailed?.call(null, {
199
201
  data,
200
202
  error: err,
201
- sub,
203
+ did,
202
204
  deviceId,
203
205
  deviceMetadata,
204
206
  clientId,
@@ -235,16 +237,16 @@ export class AccountManager {
235
237
 
236
238
  public async upsertDeviceAccount(
237
239
  deviceId: DeviceId,
238
- sub: Sub,
240
+ did: Did,
239
241
  ): Promise<void> {
240
- await this.store.upsertDeviceAccount(deviceId, sub)
242
+ await this.store.upsertDeviceAccount(deviceId, did)
241
243
  }
242
244
 
243
245
  public async getDeviceAccount(
244
246
  deviceId: DeviceId,
245
- sub: Sub,
247
+ did: Did,
246
248
  ): Promise<DeviceAccount> {
247
- const deviceAccount = await this.store.getDeviceAccount(deviceId, sub)
249
+ const deviceAccount = await this.store.getDeviceAccount(deviceId, did)
248
250
  if (!deviceAccount) throw new InvalidRequestError(`Account not found`)
249
251
 
250
252
  return deviceAccount
@@ -258,15 +260,15 @@ export class AccountManager {
258
260
  // "Loopback" clients are not distinguishable from one another.
259
261
  if (isOAuthClientIdLoopback(client.id)) return
260
262
 
261
- await this.store.setAuthorizedClient(account.sub, client.id, data)
263
+ await this.store.setAuthorizedClient(account.did, client.id, data)
262
264
  }
263
265
 
264
- public async getAccount(sub: Sub) {
265
- return this.store.getAccount(sub)
266
+ public async getAccount(did: Did) {
267
+ return this.store.getAccount(did)
266
268
  }
267
269
 
268
- public async removeDeviceAccount(deviceId: DeviceId, sub: Sub) {
269
- return this.store.removeDeviceAccount(deviceId, sub)
270
+ public async removeDeviceAccount(deviceId: DeviceId, did: Did) {
271
+ return this.store.removeDeviceAccount(deviceId, did)
270
272
  }
271
273
 
272
274
  public async listDeviceAccounts(
@@ -280,13 +282,13 @@ export class AccountManager {
280
282
  .filter((deviceAccount) => deviceAccount.deviceId === deviceId)
281
283
  }
282
284
 
283
- public async listAccountDevices(sub: Sub): Promise<DeviceAccount[]> {
285
+ public async listAccountDevices(did: Did): Promise<DeviceAccount[]> {
284
286
  const deviceAccounts = await this.store.listDeviceAccounts({
285
- sub,
287
+ did,
286
288
  })
287
289
 
288
290
  return deviceAccounts // Fool proof
289
- .filter((deviceAccount) => deviceAccount.account.sub === sub)
291
+ .filter((deviceAccount) => deviceAccount.account.did === did)
290
292
  }
291
293
 
292
294
  public async resetPasswordRequest(
@@ -412,8 +414,8 @@ export class AccountManager {
412
414
  await this.hooks.onVerifyEmailRequest?.call(null, {
413
415
  deviceId,
414
416
  deviceMetadata,
415
- input,
416
417
  account,
418
+ input,
417
419
  })
418
420
 
419
421
  await this.store.verifyEmailRequest(input)
@@ -421,8 +423,8 @@ export class AccountManager {
421
423
  await this.hooks.onVerifyEmailRequested?.call(null, {
422
424
  deviceId,
423
425
  deviceMetadata,
424
- input,
425
426
  account,
427
+ input,
426
428
  })
427
429
  }
428
430
 
@@ -435,8 +437,8 @@ export class AccountManager {
435
437
  await this.hooks.onVerifyEmailConfirm?.call(null, {
436
438
  deviceId,
437
439
  deviceMetadata,
438
- input,
439
440
  account,
441
+ input,
440
442
  })
441
443
 
442
444
  const updatedAccount = await this.store.verifyEmailConfirm(input)
@@ -448,8 +450,8 @@ export class AccountManager {
448
450
  await this.hooks.onVerifyEmailConfirmed?.call(null, {
449
451
  deviceId,
450
452
  deviceMetadata,
451
- input,
452
453
  account: updatedAccount,
454
+ input,
453
455
  })
454
456
 
455
457
  return updatedAccount
@@ -479,4 +481,105 @@ export class AccountManager {
479
481
 
480
482
  return updatedAccount
481
483
  }
484
+
485
+ public async deactivateAccount(
486
+ deviceId: DeviceId,
487
+ deviceMetadata: RequestMetadata,
488
+ account: Account,
489
+ ): Promise<Account> {
490
+ await this.hooks.onDeactivateAccount?.call(null, {
491
+ deviceId,
492
+ deviceMetadata,
493
+ account,
494
+ })
495
+
496
+ const updatedAccount = await callAsync(() =>
497
+ this.store.deactivateAccount({ did: account.did }),
498
+ ).catch((err) => {
499
+ throw InvalidRequestError.from(err, 'Account deactivation failed')
500
+ })
501
+
502
+ await this.hooks.onDeactivatedAccount?.call(null, {
503
+ deviceId,
504
+ deviceMetadata,
505
+ account: updatedAccount,
506
+ })
507
+
508
+ return updatedAccount
509
+ }
510
+
511
+ public async reactivateAccount(
512
+ deviceId: DeviceId,
513
+ deviceMetadata: RequestMetadata,
514
+ account: Account,
515
+ ): Promise<Account> {
516
+ await this.hooks.onReactivateAccount?.call(null, {
517
+ deviceId,
518
+ deviceMetadata,
519
+ account,
520
+ })
521
+
522
+ const updatedAccount = await callAsync(() =>
523
+ this.store.reactivateAccount({ did: account.did }),
524
+ ).catch((err) => {
525
+ throw InvalidRequestError.from(err, 'Account reactivation failed')
526
+ })
527
+
528
+ await this.hooks.onReactivatedAccount?.call(null, {
529
+ deviceId,
530
+ deviceMetadata,
531
+ account: updatedAccount,
532
+ })
533
+
534
+ return updatedAccount
535
+ }
536
+
537
+ public async deleteAccountRequest(
538
+ deviceId: DeviceId,
539
+ deviceMetadata: RequestMetadata,
540
+ input: DeleteAccountRequestInput,
541
+ account: Account,
542
+ ): Promise<void> {
543
+ await this.hooks.onDeleteAccountRequest?.call(null, {
544
+ deviceId,
545
+ deviceMetadata,
546
+ account,
547
+ })
548
+
549
+ await this.store.deleteAccountRequest(input)
550
+
551
+ await this.hooks.onDeleteAccountRequested?.call(null, {
552
+ deviceId,
553
+ deviceMetadata,
554
+ account,
555
+ })
556
+ }
557
+
558
+ public async deleteAccountConfirm(
559
+ deviceId: DeviceId,
560
+ deviceMetadata: RequestMetadata,
561
+ input: DeleteAccountConfirmInput,
562
+ account: Account,
563
+ ): Promise<void> {
564
+ return constantTime(BRUTE_FORCE_MITIGATION_DELAY, async () => {
565
+ await this.hooks.onDeleteAccountConfirm?.call(null, {
566
+ deviceId,
567
+ deviceMetadata,
568
+ account,
569
+ })
570
+
571
+ await callAsync(() => this.store.deleteAccountConfirm(input)).catch(
572
+ (err) => {
573
+ throw InvalidRequestError.from(err, 'Account deletion failed')
574
+ },
575
+ )
576
+
577
+ await this.hooks.onDeleteAccountConfirmed?.call(null, {
578
+ deviceId,
579
+ deviceMetadata,
580
+ account,
581
+ input,
582
+ })
583
+ })
584
+ }
482
585
  }
@@ -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,14 +234,53 @@ 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
275
  'authenticateAccount',
232
276
  'createAccount',
277
+ 'deactivateAccount',
278
+ 'deleteAccountConfirm',
279
+ 'deleteAccountRequest',
233
280
  'getAccount',
234
281
  'getDeviceAccount',
235
282
  'listDeviceAccounts',
283
+ 'reactivateAccount',
236
284
  'removeDeviceAccount',
237
285
  'resetPasswordConfirm',
238
286
  'resetPasswordRequest',
@@ -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)
@@ -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
  }