@atproto/pds 0.4.33 → 0.4.35

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 (227) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/account-manager/db/migrations/004-oauth.d.ts +4 -0
  3. package/dist/account-manager/db/migrations/004-oauth.d.ts.map +1 -0
  4. package/dist/account-manager/db/migrations/004-oauth.js +106 -0
  5. package/dist/account-manager/db/migrations/004-oauth.js.map +1 -0
  6. package/dist/account-manager/db/migrations/index.d.ts +2 -0
  7. package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
  8. package/dist/account-manager/db/migrations/index.js +2 -0
  9. package/dist/account-manager/db/migrations/index.js.map +1 -1
  10. package/dist/account-manager/db/schema/authorization-request.d.ts +19 -0
  11. package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -0
  12. package/dist/account-manager/db/schema/authorization-request.js +5 -0
  13. package/dist/account-manager/db/schema/authorization-request.js.map +1 -0
  14. package/dist/account-manager/db/schema/device-account.d.ts +14 -0
  15. package/dist/account-manager/db/schema/device-account.d.ts.map +1 -0
  16. package/dist/account-manager/db/schema/device-account.js +5 -0
  17. package/dist/account-manager/db/schema/device-account.js.map +1 -0
  18. package/dist/account-manager/db/schema/device.d.ts +16 -0
  19. package/dist/account-manager/db/schema/device.d.ts.map +1 -0
  20. package/dist/account-manager/db/schema/device.js +5 -0
  21. package/dist/account-manager/db/schema/device.js.map +1 -0
  22. package/dist/account-manager/db/schema/index.d.ts +11 -1
  23. package/dist/account-manager/db/schema/index.d.ts.map +1 -1
  24. package/dist/account-manager/db/schema/token.d.ts +24 -0
  25. package/dist/account-manager/db/schema/token.d.ts.map +1 -0
  26. package/dist/account-manager/db/schema/token.js +5 -0
  27. package/dist/account-manager/db/schema/token.js.map +1 -0
  28. package/dist/account-manager/db/schema/used-refresh-token.d.ts +12 -0
  29. package/dist/account-manager/db/schema/used-refresh-token.d.ts.map +1 -0
  30. package/dist/account-manager/db/schema/used-refresh-token.js +5 -0
  31. package/dist/account-manager/db/schema/used-refresh-token.js.map +1 -0
  32. package/dist/account-manager/helpers/account.d.ts +27 -5
  33. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  34. package/dist/account-manager/helpers/account.js +15 -14
  35. package/dist/account-manager/helpers/account.js.map +1 -1
  36. package/dist/account-manager/helpers/authorization-request.d.ts +12 -0
  37. package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -0
  38. package/dist/account-manager/helpers/authorization-request.js +59 -0
  39. package/dist/account-manager/helpers/authorization-request.js.map +1 -0
  40. package/dist/account-manager/helpers/device-account.d.ts +108 -0
  41. package/dist/account-manager/helpers/device-account.d.ts.map +1 -0
  42. package/dist/account-manager/helpers/device-account.js +82 -0
  43. package/dist/account-manager/helpers/device-account.js.map +1 -0
  44. package/dist/account-manager/helpers/device.d.ts +9 -0
  45. package/dist/account-manager/helpers/device.d.ts.map +1 -0
  46. package/dist/account-manager/helpers/device.js +32 -0
  47. package/dist/account-manager/helpers/device.js.map +1 -0
  48. package/dist/account-manager/helpers/token.d.ts +485 -0
  49. package/dist/account-manager/helpers/token.d.ts.map +1 -0
  50. package/dist/account-manager/helpers/token.js +123 -0
  51. package/dist/account-manager/helpers/token.js.map +1 -0
  52. package/dist/account-manager/helpers/used-refresh-token.d.ts +10 -0
  53. package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -0
  54. package/dist/account-manager/helpers/used-refresh-token.js +25 -0
  55. package/dist/account-manager/helpers/used-refresh-token.js.map +1 -0
  56. package/dist/account-manager/index.d.ts +36 -6
  57. package/dist/account-manager/index.d.ts.map +1 -1
  58. package/dist/account-manager/index.js +223 -22
  59. package/dist/account-manager/index.js.map +1 -1
  60. package/dist/actor-store/preference/reader.d.ts +2 -1
  61. package/dist/actor-store/preference/reader.d.ts.map +1 -1
  62. package/dist/actor-store/preference/reader.js +3 -1
  63. package/dist/actor-store/preference/reader.js.map +1 -1
  64. package/dist/actor-store/preference/transactor.d.ts +2 -1
  65. package/dist/actor-store/preference/transactor.d.ts.map +1 -1
  66. package/dist/actor-store/preference/transactor.js +7 -1
  67. package/dist/actor-store/preference/transactor.js.map +1 -1
  68. package/dist/actor-store/preference/util.d.ts +3 -0
  69. package/dist/actor-store/preference/util.d.ts.map +1 -0
  70. package/dist/actor-store/preference/util.js +12 -0
  71. package/dist/actor-store/preference/util.js.map +1 -0
  72. package/dist/actor-store/record/reader.d.ts +1 -1
  73. package/dist/api/app/bsky/actor/getPreferences.d.ts.map +1 -1
  74. package/dist/api/app/bsky/actor/getPreferences.js +1 -6
  75. package/dist/api/app/bsky/actor/getPreferences.js.map +1 -1
  76. package/dist/api/app/bsky/actor/putPreferences.d.ts.map +1 -1
  77. package/dist/api/app/bsky/actor/putPreferences.js +1 -1
  78. package/dist/api/app/bsky/actor/putPreferences.js.map +1 -1
  79. package/dist/api/app/bsky/util/resolver.d.ts +1 -1
  80. package/dist/api/com/atproto/server/createSession.d.ts.map +1 -1
  81. package/dist/api/com/atproto/server/createSession.js +7 -31
  82. package/dist/api/com/atproto/server/createSession.js.map +1 -1
  83. package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
  84. package/dist/api/com/atproto/server/deleteSession.js +14 -13
  85. package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
  86. package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
  87. package/dist/api/com/atproto/server/getSession.js +4 -2
  88. package/dist/api/com/atproto/server/getSession.js.map +1 -1
  89. package/dist/api/com/atproto/server/refreshSession.d.ts.map +1 -1
  90. package/dist/api/com/atproto/server/refreshSession.js +4 -2
  91. package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
  92. package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -1
  93. package/dist/api/com/atproto/sync/getRepoStatus.js +2 -1
  94. package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -1
  95. package/dist/api/com/atproto/sync/listRepos.js +2 -2
  96. package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
  97. package/dist/api/proxy.d.ts.map +1 -1
  98. package/dist/api/proxy.js +15 -2
  99. package/dist/api/proxy.js.map +1 -1
  100. package/dist/auth-routes.d.ts +4 -0
  101. package/dist/auth-routes.d.ts.map +1 -0
  102. package/dist/auth-routes.js +24 -0
  103. package/dist/auth-routes.js.map +1 -0
  104. package/dist/auth-verifier.d.ts +32 -11
  105. package/dist/auth-verifier.d.ts.map +1 -1
  106. package/dist/auth-verifier.js +238 -79
  107. package/dist/auth-verifier.js.map +1 -1
  108. package/dist/config/config.d.ts +12 -0
  109. package/dist/config/config.d.ts.map +1 -1
  110. package/dist/config/config.js +45 -0
  111. package/dist/config/config.js.map +1 -1
  112. package/dist/config/env.d.ts +8 -0
  113. package/dist/config/env.d.ts.map +1 -1
  114. package/dist/config/env.js +10 -0
  115. package/dist/config/env.js.map +1 -1
  116. package/dist/config/secrets.d.ts +1 -0
  117. package/dist/config/secrets.d.ts.map +1 -1
  118. package/dist/config/secrets.js +1 -0
  119. package/dist/config/secrets.js.map +1 -1
  120. package/dist/context.d.ts +6 -0
  121. package/dist/context.d.ts.map +1 -1
  122. package/dist/context.js +71 -13
  123. package/dist/context.js.map +1 -1
  124. package/dist/db/cast.d.ts +15 -0
  125. package/dist/db/cast.d.ts.map +1 -0
  126. package/dist/db/cast.js +66 -0
  127. package/dist/db/cast.js.map +1 -0
  128. package/dist/db/db.d.ts +2 -2
  129. package/dist/db/db.d.ts.map +1 -1
  130. package/dist/db/db.js +9 -7
  131. package/dist/db/db.js.map +1 -1
  132. package/dist/db/index.d.ts +1 -0
  133. package/dist/db/index.d.ts.map +1 -1
  134. package/dist/db/index.js +1 -0
  135. package/dist/db/index.js.map +1 -1
  136. package/dist/error.d.ts.map +1 -1
  137. package/dist/error.js +5 -0
  138. package/dist/error.js.map +1 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +2 -0
  141. package/dist/index.js.map +1 -1
  142. package/dist/lexicon/index.d.ts +4 -0
  143. package/dist/lexicon/index.d.ts.map +1 -1
  144. package/dist/lexicon/index.js +8 -0
  145. package/dist/lexicon/index.js.map +1 -1
  146. package/dist/lexicon/lexicons.d.ts +51 -0
  147. package/dist/lexicon/lexicons.d.ts.map +1 -1
  148. package/dist/lexicon/lexicons.js +51 -0
  149. package/dist/lexicon/lexicons.js.map +1 -1
  150. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  151. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  152. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  153. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts +29 -0
  154. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts.map +1 -0
  155. package/dist/lexicon/types/app/bsky/graph/muteThread.js +3 -0
  156. package/dist/lexicon/types/app/bsky/graph/muteThread.js.map +1 -0
  157. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts +29 -0
  158. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts.map +1 -0
  159. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js +3 -0
  160. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js.map +1 -0
  161. package/dist/logger.d.ts +13 -11
  162. package/dist/logger.d.ts.map +1 -1
  163. package/dist/logger.js +80 -64
  164. package/dist/logger.js.map +1 -1
  165. package/dist/oauth/detailed-account-store.d.ts +27 -0
  166. package/dist/oauth/detailed-account-store.d.ts.map +1 -0
  167. package/dist/oauth/detailed-account-store.js +76 -0
  168. package/dist/oauth/detailed-account-store.js.map +1 -0
  169. package/dist/oauth/provider.d.ts +16 -0
  170. package/dist/oauth/provider.d.ts.map +1 -0
  171. package/dist/oauth/provider.js +45 -0
  172. package/dist/oauth/provider.js.map +1 -0
  173. package/dist/pipethrough.d.ts.map +1 -1
  174. package/dist/pipethrough.js.map +1 -1
  175. package/dist/sequencer/events.d.ts +2 -2
  176. package/example.env +21 -3
  177. package/package.json +9 -7
  178. package/src/account-manager/db/migrations/004-oauth.ts +122 -0
  179. package/src/account-manager/db/migrations/index.ts +2 -0
  180. package/src/account-manager/db/schema/authorization-request.ts +26 -0
  181. package/src/account-manager/db/schema/device-account.ts +15 -0
  182. package/src/account-manager/db/schema/device.ts +18 -0
  183. package/src/account-manager/db/schema/index.ts +15 -0
  184. package/src/account-manager/db/schema/token.ts +34 -0
  185. package/src/account-manager/db/schema/used-refresh-token.ts +13 -0
  186. package/src/account-manager/helpers/account.ts +16 -21
  187. package/src/account-manager/helpers/authorization-request.ts +82 -0
  188. package/src/account-manager/helpers/device-account.ts +135 -0
  189. package/src/account-manager/helpers/device.ts +45 -0
  190. package/src/account-manager/helpers/token.ts +185 -0
  191. package/src/account-manager/helpers/used-refresh-token.ts +30 -0
  192. package/src/account-manager/index.ts +325 -20
  193. package/src/actor-store/preference/reader.ts +8 -2
  194. package/src/actor-store/preference/transactor.ts +10 -0
  195. package/src/actor-store/preference/util.ts +8 -0
  196. package/src/api/app/bsky/actor/getPreferences.ts +2 -9
  197. package/src/api/app/bsky/actor/putPreferences.ts +5 -1
  198. package/src/api/com/atproto/server/createSession.ts +8 -44
  199. package/src/api/com/atproto/server/deleteSession.ts +14 -20
  200. package/src/api/com/atproto/server/getSession.ts +7 -2
  201. package/src/api/com/atproto/server/refreshSession.ts +6 -2
  202. package/src/api/com/atproto/sync/getRepoStatus.ts +3 -1
  203. package/src/api/com/atproto/sync/listRepos.ts +1 -1
  204. package/src/api/proxy.ts +18 -2
  205. package/src/auth-routes.ts +27 -0
  206. package/src/auth-verifier.ts +312 -92
  207. package/src/config/config.ts +66 -0
  208. package/src/config/env.ts +24 -0
  209. package/src/config/secrets.ts +2 -0
  210. package/src/context.ts +80 -14
  211. package/src/db/cast.ts +59 -0
  212. package/src/db/db.ts +15 -12
  213. package/src/db/index.ts +1 -0
  214. package/src/error.ts +7 -0
  215. package/src/index.ts +2 -0
  216. package/src/lexicon/index.ts +24 -0
  217. package/src/lexicon/lexicons.ts +52 -0
  218. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  219. package/src/lexicon/types/app/bsky/graph/muteThread.ts +38 -0
  220. package/src/lexicon/types/app/bsky/graph/unmuteThread.ts +38 -0
  221. package/src/logger.ts +83 -38
  222. package/src/oauth/detailed-account-store.ts +96 -0
  223. package/src/oauth/provider.ts +77 -0
  224. package/src/pipethrough.ts +3 -2
  225. package/tests/preferences.test.ts +67 -1
  226. package/tests/proxied/__snapshots__/feedgen.test.ts.snap +4 -1
  227. package/tests/proxied/__snapshots__/views.test.ts.snap +116 -38
@@ -1,25 +1,57 @@
1
- import { KeyObject } from 'node:crypto'
2
- import { HOUR } from '@atproto/common'
1
+ import { HOUR, wait } from '@atproto/common'
2
+ import {
3
+ AccountInfo,
4
+ AccountStore,
5
+ Code,
6
+ DeviceData,
7
+ DeviceId,
8
+ DeviceStore,
9
+ FoundRequestResult,
10
+ LoginCredentials,
11
+ NewTokenData,
12
+ RefreshToken,
13
+ RequestData,
14
+ RequestId,
15
+ RequestStore,
16
+ TokenData,
17
+ TokenId,
18
+ TokenInfo,
19
+ TokenStore,
20
+ UpdateRequestData,
21
+ } from '@atproto/oauth-provider'
22
+ import { AuthRequiredError } from '@atproto/xrpc-server'
3
23
  import { CID } from 'multiformats/cid'
24
+ import { KeyObject } from 'node:crypto'
25
+
26
+ import { AuthScope } from '../auth-verifier'
27
+ import { BackgroundQueue } from '../background'
28
+ import { softDeleted } from '../db'
29
+ import { StatusAttr } from '../lexicon/types/com/atproto/admin/defs'
4
30
  import { AccountDb, EmailTokenPurpose, getDb, getMigrator } from './db'
5
- import * as scrypt from './helpers/scrypt'
6
31
  import * as account from './helpers/account'
7
- import { AccountStatus } from './helpers/account'
8
- import { ActorAccount } from './helpers/account'
9
- import * as repo from './helpers/repo'
32
+ import { AccountStatus, ActorAccount } from './helpers/account'
10
33
  import * as auth from './helpers/auth'
34
+ import * as authRequest from './helpers/authorization-request'
35
+ import * as deviceAccount from './helpers/device-account'
36
+ import * as device from './helpers/device'
37
+ import * as emailToken from './helpers/email-token'
11
38
  import * as invite from './helpers/invite'
12
39
  import * as password from './helpers/password'
13
- import * as emailToken from './helpers/email-token'
14
- import { AuthScope } from '../auth-verifier'
15
- import { StatusAttr } from '../lexicon/types/com/atproto/admin/defs'
40
+ import * as repo from './helpers/repo'
41
+ import * as scrypt from './helpers/scrypt'
42
+ import * as token from './helpers/token'
43
+ import * as usedRefreshToken from './helpers/used-refresh-token'
16
44
 
17
45
  export { AccountStatus } from './helpers/account'
46
+ export { formatAccountStatus } from './helpers/account'
18
47
 
19
- export class AccountManager {
48
+ export class AccountManager
49
+ implements AccountStore, RequestStore, DeviceStore, TokenStore
50
+ {
20
51
  db: AccountDb
21
52
 
22
53
  constructor(
54
+ private backgroundQueue: BackgroundQueue,
23
55
  dbLocation: string,
24
56
  private jwtKey: KeyObject,
25
57
  private serviceDid: string,
@@ -73,15 +105,9 @@ export class AccountManager {
73
105
  includeDeactivated: true,
74
106
  includeTakenDown: true,
75
107
  })
76
- if (!got) {
77
- return AccountStatus.Deleted
78
- } else if (got.takedownRef) {
79
- return AccountStatus.Takendown
80
- } else if (got.deactivatedAt) {
81
- return AccountStatus.Deactivated
82
- } else {
83
- return AccountStatus.Active
84
- }
108
+
109
+ const res = account.formatAccountStatus(got)
110
+ return res.active ? AccountStatus.Active : res.status
85
111
  }
86
112
 
87
113
  async createAccount(opts: {
@@ -148,10 +174,11 @@ export class AccountManager {
148
174
  }
149
175
 
150
176
  async takedownAccount(did: string, takedown: StatusAttr) {
151
- await this.db.transaction((dbTxn) =>
177
+ await this.db.transaction(async (dbTxn) =>
152
178
  Promise.all([
153
179
  account.updateAccountTakedownStatus(dbTxn, did, takedown),
154
180
  auth.revokeRefreshTokensByDid(dbTxn, did),
181
+ token.removeByDidQB(dbTxn, did).execute(),
155
182
  ]),
156
183
  )
157
184
  }
@@ -250,6 +277,63 @@ export class AccountManager {
250
277
  return auth.revokeRefreshToken(this.db, id)
251
278
  }
252
279
 
280
+ // Login
281
+ // ----------
282
+
283
+ async login({
284
+ identifier,
285
+ password,
286
+ }: {
287
+ identifier: string
288
+ password: string
289
+ }): Promise<{
290
+ user: ActorAccount
291
+ appPassword: password.AppPassDescript | null
292
+ }> {
293
+ const start = Date.now()
294
+ try {
295
+ const identifierNormalized = identifier.toLowerCase()
296
+
297
+ const user = identifierNormalized.includes('@')
298
+ ? await this.getAccountByEmail(identifierNormalized, {
299
+ includeDeactivated: true,
300
+ includeTakenDown: true,
301
+ })
302
+ : await this.getAccount(identifierNormalized, {
303
+ includeDeactivated: true,
304
+ includeTakenDown: true,
305
+ })
306
+
307
+ if (!user) {
308
+ throw new AuthRequiredError('Invalid identifier or password')
309
+ }
310
+
311
+ let appPassword: password.AppPassDescript | null = null
312
+ const validAccountPass = await this.verifyAccountPassword(
313
+ user.did,
314
+ password,
315
+ )
316
+ if (!validAccountPass) {
317
+ appPassword = await this.verifyAppPassword(user.did, password)
318
+ if (appPassword === null) {
319
+ throw new AuthRequiredError('Invalid identifier or password')
320
+ }
321
+ }
322
+
323
+ if (softDeleted(user)) {
324
+ throw new AuthRequiredError(
325
+ 'Account has been taken down',
326
+ 'AccountTakedown',
327
+ )
328
+ }
329
+
330
+ return { user, appPassword }
331
+ } finally {
332
+ // Mitigate timing attacks
333
+ await wait(350 - (Date.now() - start))
334
+ }
335
+ }
336
+
253
337
  // Passwords
254
338
  // ----------
255
339
 
@@ -399,4 +483,225 @@ export class AccountManager {
399
483
  ]),
400
484
  )
401
485
  }
486
+
487
+ // AccountStore
488
+
489
+ async authenticateAccount(
490
+ { username: identifier, password, remember = false }: LoginCredentials,
491
+ deviceId: DeviceId,
492
+ ): Promise<AccountInfo | null> {
493
+ try {
494
+ const { user, appPassword } = await this.login({ identifier, password })
495
+
496
+ if (appPassword) {
497
+ throw new AuthRequiredError('App passwords are not allowed')
498
+ }
499
+
500
+ await this.db.executeWithRetry(
501
+ deviceAccount.createOrUpdateQB(this.db, deviceId, user.did, remember),
502
+ )
503
+
504
+ return await this.getDeviceAccount(deviceId, user.did)
505
+ } catch (err) {
506
+ if (err instanceof AuthRequiredError) return null
507
+ throw err
508
+ }
509
+ }
510
+
511
+ async addAuthorizedClient(
512
+ deviceId: DeviceId,
513
+ sub: string,
514
+ clientId: string,
515
+ ): Promise<void> {
516
+ await this.db.transaction(async (dbTxn) => {
517
+ const row = await deviceAccount
518
+ .readQB(dbTxn, deviceId, sub)
519
+ .executeTakeFirstOrThrow()
520
+
521
+ const { authorizedClients } = deviceAccount.toDeviceAccountInfo(row)
522
+ if (!authorizedClients.includes(clientId)) {
523
+ await deviceAccount
524
+ .updateQB(dbTxn, deviceId, sub, {
525
+ authorizedClients: [...authorizedClients, clientId],
526
+ })
527
+ .execute()
528
+ }
529
+ })
530
+ }
531
+
532
+ async getDeviceAccount(
533
+ deviceId: DeviceId,
534
+ sub: string,
535
+ ): Promise<AccountInfo | null> {
536
+ const row = await deviceAccount
537
+ .getAccountInfoQB(this.db, deviceId, sub)
538
+ .executeTakeFirst()
539
+
540
+ if (!row) return null
541
+
542
+ return {
543
+ account: deviceAccount.toAccount(row, this.serviceDid),
544
+ info: deviceAccount.toDeviceAccountInfo(row),
545
+ }
546
+ }
547
+
548
+ async listDeviceAccounts(deviceId: DeviceId): Promise<AccountInfo[]> {
549
+ const rows = await deviceAccount
550
+ .listRememberedQB(this.db, deviceId)
551
+ .execute()
552
+
553
+ return rows.map((row) => ({
554
+ account: deviceAccount.toAccount(row, this.serviceDid),
555
+ info: deviceAccount.toDeviceAccountInfo(row),
556
+ }))
557
+ }
558
+
559
+ async removeDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {
560
+ await this.db.executeWithRetry(
561
+ deviceAccount.removeQB(this.db, deviceId, sub),
562
+ )
563
+ }
564
+
565
+ // RequestStore
566
+
567
+ async createRequest(id: RequestId, data: RequestData): Promise<void> {
568
+ await this.db.executeWithRetry(authRequest.createQB(this.db, id, data))
569
+ }
570
+
571
+ async readRequest(id: RequestId): Promise<RequestData | null> {
572
+ try {
573
+ const row = await authRequest.readQB(this.db, id).executeTakeFirst()
574
+ if (!row) return null
575
+ return authRequest.rowToRequestData(row)
576
+ } finally {
577
+ // Take the opportunity to clean up expired requests. Do this after we got
578
+ // the current (potentially expired) request data to allow the provider to
579
+ // handle expired requests.
580
+ this.backgroundQueue.add(async () => {
581
+ await this.db.executeWithRetry(authRequest.removeOldExpiredQB(this.db))
582
+ })
583
+ }
584
+ }
585
+
586
+ async updateRequest(id: RequestId, data: UpdateRequestData): Promise<void> {
587
+ await this.db.executeWithRetry(authRequest.updateQB(this.db, id, data))
588
+ }
589
+
590
+ async deleteRequest(id: RequestId): Promise<void> {
591
+ await this.db.executeWithRetry(authRequest.removeByIdQB(this.db, id))
592
+ }
593
+
594
+ async findRequestByCode(code: Code): Promise<FoundRequestResult | null> {
595
+ const row = await authRequest.findByCodeQB(this.db, code).executeTakeFirst()
596
+ return row ? authRequest.rowToFoundRequestResult(row) : null
597
+ }
598
+
599
+ // DeviceStore
600
+
601
+ async createDevice(deviceId: DeviceId, data: DeviceData): Promise<void> {
602
+ await this.db.executeWithRetry(device.createQB(this.db, deviceId, data))
603
+ }
604
+
605
+ async readDevice(deviceId: DeviceId): Promise<null | DeviceData> {
606
+ const row = await device.readQB(this.db, deviceId).executeTakeFirst()
607
+ return row ? device.rowToDeviceData(row) : null
608
+ }
609
+
610
+ async updateDevice(
611
+ deviceId: DeviceId,
612
+ data: Partial<DeviceData>,
613
+ ): Promise<void> {
614
+ await this.db.executeWithRetry(device.updateQB(this.db, deviceId, data))
615
+ }
616
+
617
+ async deleteDevice(deviceId: DeviceId): Promise<void> {
618
+ // Will cascade to device_account (device_account_device_id_fk)
619
+ await this.db.executeWithRetry(device.removeQB(this.db, deviceId))
620
+ }
621
+
622
+ // TokenStore
623
+
624
+ async createToken(
625
+ id: TokenId,
626
+ data: TokenData,
627
+ refreshToken?: RefreshToken,
628
+ ): Promise<void> {
629
+ await this.db.transaction(async (dbTxn) => {
630
+ if (refreshToken) {
631
+ const { count } = await usedRefreshToken
632
+ .countQB(dbTxn, refreshToken)
633
+ .executeTakeFirstOrThrow()
634
+
635
+ if (count > 0) {
636
+ throw new Error('Refresh token already in use')
637
+ }
638
+ }
639
+
640
+ return token.createQB(dbTxn, id, data, refreshToken).execute()
641
+ })
642
+ }
643
+
644
+ async readToken(tokenId: TokenId): Promise<TokenInfo | null> {
645
+ const row = await token.findByQB(this.db, { tokenId }).executeTakeFirst()
646
+ return row ? token.toTokenInfo(row, this.serviceDid) : null
647
+ }
648
+
649
+ async deleteToken(tokenId: TokenId): Promise<void> {
650
+ // Will cascade to used_refresh_token (used_refresh_token_fk)
651
+ await this.db.executeWithRetry(token.removeQB(this.db, tokenId))
652
+ }
653
+
654
+ async rotateToken(
655
+ tokenId: TokenId,
656
+ newTokenId: TokenId,
657
+ newRefreshToken: RefreshToken,
658
+ newData: NewTokenData,
659
+ ): Promise<void> {
660
+ const err = await this.db.transaction(async (dbTxn) => {
661
+ const { id, currentRefreshToken } = await token
662
+ .forRotateQB(dbTxn, tokenId)
663
+ .executeTakeFirstOrThrow()
664
+
665
+ if (currentRefreshToken) {
666
+ await usedRefreshToken
667
+ .insertQB(dbTxn, id, currentRefreshToken)
668
+ .execute()
669
+ }
670
+
671
+ const { count } = await usedRefreshToken
672
+ .countQB(dbTxn, newRefreshToken)
673
+ .executeTakeFirstOrThrow()
674
+
675
+ if (count > 0) {
676
+ // Do NOT throw (we don't want the transaction to be rolled back)
677
+ return new Error('New refresh token already in use')
678
+ }
679
+
680
+ await token
681
+ .rotateQB(dbTxn, id, newTokenId, newRefreshToken, newData)
682
+ .execute()
683
+ })
684
+
685
+ if (err) throw err
686
+ }
687
+
688
+ async findTokenByRefreshToken(
689
+ refreshToken: RefreshToken,
690
+ ): Promise<TokenInfo | null> {
691
+ const used = await usedRefreshToken
692
+ .findByTokenQB(this.db, refreshToken)
693
+ .executeTakeFirst()
694
+
695
+ const search = used
696
+ ? { id: used.tokenId }
697
+ : { currentRefreshToken: refreshToken }
698
+
699
+ const row = await token.findByQB(this.db, search).executeTakeFirst()
700
+ return row ? token.toTokenInfo(row, this.serviceDid) : null
701
+ }
702
+
703
+ async findTokenByCode(code: Code): Promise<TokenInfo | null> {
704
+ const row = await token.findByQB(this.db, { code }).executeTakeFirst()
705
+ return row ? token.toTokenInfo(row, this.serviceDid) : null
706
+ }
402
707
  }
@@ -1,9 +1,14 @@
1
+ import { AuthScope } from '../../auth-verifier'
1
2
  import { ActorDb } from '../db'
3
+ import { prefInScope } from './util'
2
4
 
3
5
  export class PreferenceReader {
4
6
  constructor(public db: ActorDb) {}
5
7
 
6
- async getPreferences(namespace?: string): Promise<AccountPreference[]> {
8
+ async getPreferences(
9
+ namespace: string,
10
+ scope: AuthScope,
11
+ ): Promise<AccountPreference[]> {
7
12
  const prefsRes = await this.db.db
8
13
  .selectFrom('account_pref')
9
14
  .orderBy('id')
@@ -11,7 +16,8 @@ export class PreferenceReader {
11
16
  .execute()
12
17
  return prefsRes
13
18
  .filter((pref) => !namespace || prefMatchNamespace(namespace, pref.name))
14
- .map((pref) => JSON.parse(pref.valueJson))
19
+ .filter((pref) => prefInScope(scope, pref.name))
20
+ .map((pref) => JSON.parse(pref.valueJson) as AccountPreference)
15
21
  }
16
22
  }
17
23
 
@@ -4,11 +4,14 @@ import {
4
4
  AccountPreference,
5
5
  prefMatchNamespace,
6
6
  } from './reader'
7
+ import { AuthScope } from '../../auth-verifier'
8
+ import { prefInScope } from './util'
7
9
 
8
10
  export class PreferenceTransactor extends PreferenceReader {
9
11
  async putPreferences(
10
12
  values: AccountPreference[],
11
13
  namespace: string,
14
+ scope: AuthScope,
12
15
  ): Promise<void> {
13
16
  this.db.assertTransaction()
14
17
  if (!values.every((value) => prefMatchNamespace(namespace, value.$type))) {
@@ -16,6 +19,12 @@ export class PreferenceTransactor extends PreferenceReader {
16
19
  `Some preferences are not in the ${namespace} namespace`,
17
20
  )
18
21
  }
22
+ const notInScope = values.filter((val) => !prefInScope(scope, val.$type))
23
+ if (notInScope.length > 0) {
24
+ throw new InvalidRequestError(
25
+ `Do not have authorization to set preferences: ${notInScope.join(', ')}`,
26
+ )
27
+ }
19
28
  // get all current prefs for user and prep new pref rows
20
29
  const allPrefs = await this.db.db
21
30
  .selectFrom('account_pref')
@@ -29,6 +38,7 @@ export class PreferenceTransactor extends PreferenceReader {
29
38
  })
30
39
  const allPrefIdsInNamespace = allPrefs
31
40
  .filter((pref) => prefMatchNamespace(namespace, pref.name))
41
+ .filter((pref) => prefInScope(scope, pref.name))
32
42
  .map((pref) => pref.id)
33
43
  // replace all prefs in given namespace
34
44
  if (allPrefIdsInNamespace.length) {
@@ -0,0 +1,8 @@
1
+ import { AuthScope } from '../../auth-verifier'
2
+
3
+ const FULL_ACCESS_ONLY_PREFS = ['app.bsky.actor.defs#personalDetailsPref']
4
+
5
+ export const prefInScope = (scope: AuthScope, prefType: string) => {
6
+ if (scope === AuthScope.Access) return true
7
+ return !FULL_ACCESS_ONLY_PREFS.includes(prefType)
8
+ }
@@ -1,6 +1,5 @@
1
1
  import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
- import { AuthScope } from '../../../../auth-verifier'
4
3
 
5
4
  export default function (server: Server, ctx: AppContext) {
6
5
  if (!ctx.cfg.bskyAppView) return
@@ -8,15 +7,9 @@ export default function (server: Server, ctx: AppContext) {
8
7
  auth: ctx.authVerifier.accessStandard(),
9
8
  handler: async ({ auth }) => {
10
9
  const requester = auth.credentials.did
11
- let preferences = await ctx.actorStore.read(requester, (store) =>
12
- store.pref.getPreferences('app.bsky'),
10
+ const preferences = await ctx.actorStore.read(requester, (store) =>
11
+ store.pref.getPreferences('app.bsky', auth.credentials.scope),
13
12
  )
14
- if (auth.credentials.scope !== AuthScope.Access) {
15
- // filter out personal details for app passwords
16
- preferences = preferences.filter(
17
- (pref) => pref.$type !== 'app.bsky.actor.defs#personalDetailsPref',
18
- )
19
- }
20
13
  return {
21
14
  encoding: 'application/json',
22
15
  body: { preferences },
@@ -19,7 +19,11 @@ export default function (server: Server, ctx: AppContext) {
19
19
  }
20
20
  }
21
21
  await ctx.actorStore.transact(requester, async (actorTxn) => {
22
- await actorTxn.pref.putPreferences(checkedPreferences, 'app.bsky')
22
+ await actorTxn.pref.putPreferences(
23
+ checkedPreferences,
24
+ 'app.bsky',
25
+ auth.credentials.scope,
26
+ )
23
27
  })
24
28
  },
25
29
  })
@@ -1,12 +1,11 @@
1
1
  import { DAY, MINUTE } from '@atproto/common'
2
2
  import { INVALID_HANDLE } from '@atproto/syntax'
3
- import { AuthRequiredError } from '@atproto/xrpc-server'
3
+
4
+ import { formatAccountStatus } from '../../../../account-manager'
4
5
  import AppContext from '../../../../context'
5
- import { softDeleted } from '../../../../db/util'
6
6
  import { Server } from '../../../../lexicon'
7
- import { didDocForSession } from './util'
8
7
  import { authPassthru, resultPassthru } from '../../../proxy'
9
- import { AppPassDescript } from '../../../../account-manager/helpers/password'
8
+ import { didDocForSession } from './util'
10
9
 
11
10
  export default function (server: Server, ctx: AppContext) {
12
11
  server.com.atproto.server.createSession({
@@ -32,50 +31,15 @@ export default function (server: Server, ctx: AppContext) {
32
31
  )
33
32
  }
34
33
 
35
- const { password } = input.body
36
- const identifier = input.body.identifier.toLowerCase()
37
-
38
- const user = identifier.includes('@')
39
- ? await ctx.accountManager.getAccountByEmail(identifier, {
40
- includeDeactivated: true,
41
- includeTakenDown: true,
42
- })
43
- : await ctx.accountManager.getAccount(identifier, {
44
- includeDeactivated: true,
45
- includeTakenDown: true,
46
- })
47
-
48
- if (!user) {
49
- throw new AuthRequiredError('Invalid identifier or password')
50
- }
51
-
52
- let appPassword: AppPassDescript | null = null
53
- const validAccountPass = await ctx.accountManager.verifyAccountPassword(
54
- user.did,
55
- password,
56
- )
57
- if (!validAccountPass) {
58
- appPassword = await ctx.accountManager.verifyAppPassword(
59
- user.did,
60
- password,
61
- )
62
- if (appPassword === null) {
63
- throw new AuthRequiredError('Invalid identifier or password')
64
- }
65
- }
66
-
67
- if (softDeleted(user)) {
68
- throw new AuthRequiredError(
69
- 'Account has been taken down',
70
- 'AccountTakedown',
71
- )
72
- }
34
+ const { user, appPassword } = await ctx.accountManager.login(input.body)
73
35
 
74
36
  const [{ accessJwt, refreshJwt }, didDoc] = await Promise.all([
75
37
  ctx.accountManager.createSession(user.did, appPassword),
76
38
  didDocForSession(ctx, user.did),
77
39
  ])
78
40
 
41
+ const { status, active } = formatAccountStatus(user)
42
+
79
43
  return {
80
44
  encoding: 'application/json',
81
45
  body: {
@@ -86,8 +50,8 @@ export default function (server: Server, ctx: AppContext) {
86
50
  emailConfirmed: !!user.emailConfirmedAt,
87
51
  accessJwt,
88
52
  refreshJwt,
89
- active: user.active,
90
- status: user.status,
53
+ active,
54
+ status,
91
55
  },
92
56
  }
93
57
  },
@@ -1,28 +1,22 @@
1
- import { AuthScope } from '../../../../auth-verifier'
2
1
  import AppContext from '../../../../context'
3
2
  import { Server } from '../../../../lexicon'
4
3
  import { authPassthru } from '../../../proxy'
5
4
 
6
5
  export default function (server: Server, ctx: AppContext) {
7
- server.com.atproto.server.deleteSession(async ({ req }) => {
8
- if (ctx.entrywayAgent) {
9
- await ctx.entrywayAgent.com.atproto.server.deleteSession(
6
+ const { entrywayAgent } = ctx
7
+ if (entrywayAgent) {
8
+ server.com.atproto.server.deleteSession(async (reqCtx) => {
9
+ await entrywayAgent.com.atproto.server.deleteSession(
10
10
  undefined,
11
- authPassthru(req, true),
11
+ authPassthru(reqCtx.req, true),
12
12
  )
13
- return
14
- }
15
-
16
- const result = await ctx.authVerifier.validateBearerToken(
17
- req,
18
- [AuthScope.Refresh],
19
- { clockTolerance: Infinity }, // ignore expiration
20
- )
21
- const id = result.payload.jti
22
- if (!id) {
23
- throw new Error('Unexpected missing refresh token id')
24
- }
25
-
26
- await ctx.accountManager.revokeRefreshToken(id)
27
- })
13
+ })
14
+ } else {
15
+ server.com.atproto.server.deleteSession({
16
+ auth: ctx.authVerifier.refreshExpired,
17
+ handler: async ({ auth }) => {
18
+ await ctx.accountManager.revokeRefreshToken(auth.credentials.tokenId)
19
+ },
20
+ })
21
+ }
28
22
  }
@@ -1,5 +1,7 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { INVALID_HANDLE } from '@atproto/syntax'
3
+
4
+ import { formatAccountStatus } from '../../../../account-manager'
3
5
  import AppContext from '../../../../context'
4
6
  import { Server } from '../../../../lexicon'
5
7
  import { authPassthru, resultPassthru } from '../../../proxy'
@@ -31,6 +33,9 @@ export default function (server: Server, ctx: AppContext) {
31
33
  `Could not find user info for account: ${did}`,
32
34
  )
33
35
  }
36
+
37
+ const { status, active } = formatAccountStatus(user)
38
+
34
39
  return {
35
40
  encoding: 'application/json',
36
41
  body: {
@@ -39,8 +44,8 @@ export default function (server: Server, ctx: AppContext) {
39
44
  email: user.email ?? undefined,
40
45
  didDoc,
41
46
  emailConfirmed: !!user.emailConfirmedAt,
42
- active: user.active,
43
- status: user.status,
47
+ active,
48
+ status,
44
49
  },
45
50
  }
46
51
  },
@@ -1,5 +1,7 @@
1
1
  import { INVALID_HANDLE } from '@atproto/syntax'
2
2
  import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
3
+
4
+ import { formatAccountStatus } from '../../../../account-manager'
3
5
  import AppContext from '../../../../context'
4
6
  import { softDeleted } from '../../../../db/util'
5
7
  import { Server } from '../../../../lexicon'
@@ -44,6 +46,8 @@ export default function (server: Server, ctx: AppContext) {
44
46
  throw new InvalidRequestError('Token has been revoked', 'ExpiredToken')
45
47
  }
46
48
 
49
+ const { status, active } = formatAccountStatus(user)
50
+
47
51
  return {
48
52
  encoding: 'application/json',
49
53
  body: {
@@ -52,8 +56,8 @@ export default function (server: Server, ctx: AppContext) {
52
56
  handle: user.handle ?? INVALID_HANDLE,
53
57
  accessJwt: rotated.accessJwt,
54
58
  refreshJwt: rotated.refreshJwt,
55
- active: user.active,
56
- status: user.status,
59
+ active,
60
+ status,
57
61
  },
58
62
  }
59
63
  },