@atproto/pds 0.4.33 → 0.4.35

Sign up to get free protection for your applications and to get access to all the features.
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
  },