@atproto/pds 0.4.34 → 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 (179) hide show
  1. package/CHANGELOG.md +10 -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.js.map +1 -1
  61. package/dist/actor-store/record/reader.d.ts +1 -1
  62. package/dist/api/app/bsky/util/resolver.d.ts +1 -1
  63. package/dist/api/com/atproto/server/createSession.d.ts.map +1 -1
  64. package/dist/api/com/atproto/server/createSession.js +7 -31
  65. package/dist/api/com/atproto/server/createSession.js.map +1 -1
  66. package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
  67. package/dist/api/com/atproto/server/deleteSession.js +14 -13
  68. package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
  69. package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
  70. package/dist/api/com/atproto/server/getSession.js +4 -2
  71. package/dist/api/com/atproto/server/getSession.js.map +1 -1
  72. package/dist/api/com/atproto/server/refreshSession.d.ts.map +1 -1
  73. package/dist/api/com/atproto/server/refreshSession.js +4 -2
  74. package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
  75. package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -1
  76. package/dist/api/com/atproto/sync/getRepoStatus.js +2 -1
  77. package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -1
  78. package/dist/api/com/atproto/sync/listRepos.js +2 -2
  79. package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
  80. package/dist/api/proxy.d.ts.map +1 -1
  81. package/dist/api/proxy.js +15 -2
  82. package/dist/api/proxy.js.map +1 -1
  83. package/dist/auth-routes.d.ts +4 -0
  84. package/dist/auth-routes.d.ts.map +1 -0
  85. package/dist/auth-routes.js +24 -0
  86. package/dist/auth-routes.js.map +1 -0
  87. package/dist/auth-verifier.d.ts +32 -11
  88. package/dist/auth-verifier.d.ts.map +1 -1
  89. package/dist/auth-verifier.js +238 -79
  90. package/dist/auth-verifier.js.map +1 -1
  91. package/dist/config/config.d.ts +12 -0
  92. package/dist/config/config.d.ts.map +1 -1
  93. package/dist/config/config.js +45 -0
  94. package/dist/config/config.js.map +1 -1
  95. package/dist/config/env.d.ts +8 -0
  96. package/dist/config/env.d.ts.map +1 -1
  97. package/dist/config/env.js +10 -0
  98. package/dist/config/env.js.map +1 -1
  99. package/dist/config/secrets.d.ts +1 -0
  100. package/dist/config/secrets.d.ts.map +1 -1
  101. package/dist/config/secrets.js +1 -0
  102. package/dist/config/secrets.js.map +1 -1
  103. package/dist/context.d.ts +6 -0
  104. package/dist/context.d.ts.map +1 -1
  105. package/dist/context.js +71 -13
  106. package/dist/context.js.map +1 -1
  107. package/dist/db/cast.d.ts +15 -0
  108. package/dist/db/cast.d.ts.map +1 -0
  109. package/dist/db/cast.js +66 -0
  110. package/dist/db/cast.js.map +1 -0
  111. package/dist/db/db.d.ts +2 -2
  112. package/dist/db/db.d.ts.map +1 -1
  113. package/dist/db/db.js +9 -7
  114. package/dist/db/db.js.map +1 -1
  115. package/dist/db/index.d.ts +1 -0
  116. package/dist/db/index.d.ts.map +1 -1
  117. package/dist/db/index.js +1 -0
  118. package/dist/db/index.js.map +1 -1
  119. package/dist/error.d.ts.map +1 -1
  120. package/dist/error.js +5 -0
  121. package/dist/error.js.map +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/index.js.map +1 -1
  125. package/dist/logger.d.ts +13 -11
  126. package/dist/logger.d.ts.map +1 -1
  127. package/dist/logger.js +80 -64
  128. package/dist/logger.js.map +1 -1
  129. package/dist/oauth/detailed-account-store.d.ts +27 -0
  130. package/dist/oauth/detailed-account-store.d.ts.map +1 -0
  131. package/dist/oauth/detailed-account-store.js +76 -0
  132. package/dist/oauth/detailed-account-store.js.map +1 -0
  133. package/dist/oauth/provider.d.ts +16 -0
  134. package/dist/oauth/provider.d.ts.map +1 -0
  135. package/dist/oauth/provider.js +45 -0
  136. package/dist/oauth/provider.js.map +1 -0
  137. package/dist/pipethrough.d.ts.map +1 -1
  138. package/dist/pipethrough.js.map +1 -1
  139. package/dist/sequencer/events.d.ts +2 -2
  140. package/example.env +21 -3
  141. package/package.json +6 -4
  142. package/src/account-manager/db/migrations/004-oauth.ts +122 -0
  143. package/src/account-manager/db/migrations/index.ts +2 -0
  144. package/src/account-manager/db/schema/authorization-request.ts +26 -0
  145. package/src/account-manager/db/schema/device-account.ts +15 -0
  146. package/src/account-manager/db/schema/device.ts +18 -0
  147. package/src/account-manager/db/schema/index.ts +15 -0
  148. package/src/account-manager/db/schema/token.ts +34 -0
  149. package/src/account-manager/db/schema/used-refresh-token.ts +13 -0
  150. package/src/account-manager/helpers/account.ts +16 -21
  151. package/src/account-manager/helpers/authorization-request.ts +82 -0
  152. package/src/account-manager/helpers/device-account.ts +135 -0
  153. package/src/account-manager/helpers/device.ts +45 -0
  154. package/src/account-manager/helpers/token.ts +185 -0
  155. package/src/account-manager/helpers/used-refresh-token.ts +30 -0
  156. package/src/account-manager/index.ts +325 -20
  157. package/src/actor-store/preference/reader.ts +1 -1
  158. package/src/api/com/atproto/server/createSession.ts +8 -44
  159. package/src/api/com/atproto/server/deleteSession.ts +14 -20
  160. package/src/api/com/atproto/server/getSession.ts +7 -2
  161. package/src/api/com/atproto/server/refreshSession.ts +6 -2
  162. package/src/api/com/atproto/sync/getRepoStatus.ts +3 -1
  163. package/src/api/com/atproto/sync/listRepos.ts +1 -1
  164. package/src/api/proxy.ts +18 -2
  165. package/src/auth-routes.ts +27 -0
  166. package/src/auth-verifier.ts +312 -92
  167. package/src/config/config.ts +66 -0
  168. package/src/config/env.ts +24 -0
  169. package/src/config/secrets.ts +2 -0
  170. package/src/context.ts +80 -14
  171. package/src/db/cast.ts +59 -0
  172. package/src/db/db.ts +15 -12
  173. package/src/db/index.ts +1 -0
  174. package/src/error.ts +7 -0
  175. package/src/index.ts +2 -0
  176. package/src/logger.ts +83 -38
  177. package/src/oauth/detailed-account-store.ts +96 -0
  178. package/src/oauth/provider.ts +77 -0
  179. package/src/pipethrough.ts +3 -2
@@ -0,0 +1,185 @@
1
+ import {
2
+ Code,
3
+ NewTokenData,
4
+ OAuthAuthorizationDetail,
5
+ RefreshToken,
6
+ TokenData,
7
+ TokenId,
8
+ TokenInfo,
9
+ } from '@atproto/oauth-provider'
10
+ import { Selectable } from 'kysely'
11
+ import {
12
+ fromDateISO,
13
+ fromJsonArray,
14
+ fromJsonObject,
15
+ toDateISO,
16
+ toJsonArray,
17
+ toJsonObject,
18
+ } from '../../db'
19
+ import { AccountDb, Token } from '../db'
20
+ import { ActorAccount, selectAccountQB } from './account'
21
+ import {
22
+ SelectableDeviceAccount,
23
+ toAccount,
24
+ toDeviceAccountInfo,
25
+ } from './device-account'
26
+
27
+ type LeftJoined<T> = { [K in keyof T]: null | T[K] }
28
+
29
+ export type ActorAccountToken = Selectable<ActorAccount> &
30
+ Selectable<Omit<Token, 'id' | 'did'>> &
31
+ LeftJoined<SelectableDeviceAccount>
32
+
33
+ export const toTokenInfo = (
34
+ row: ActorAccountToken,
35
+ audience: string,
36
+ ): TokenInfo => ({
37
+ id: row.tokenId,
38
+ data: {
39
+ createdAt: fromDateISO(row.createdAt),
40
+ expiresAt: fromDateISO(row.expiresAt),
41
+ updatedAt: fromDateISO(row.updatedAt),
42
+ clientId: row.clientId,
43
+ clientAuth: fromJsonObject(row.clientAuth),
44
+ deviceId: row.deviceId,
45
+ sub: row.did,
46
+ parameters: fromJsonObject(row.parameters),
47
+ details: row.details
48
+ ? fromJsonArray<OAuthAuthorizationDetail>(row.details)
49
+ : null,
50
+ code: row.code,
51
+ },
52
+ account: toAccount(row, audience),
53
+ info:
54
+ row.authenticatedAt != null &&
55
+ row.authorizedClients != null &&
56
+ row.remember != null
57
+ ? toDeviceAccountInfo(row as SelectableDeviceAccount)
58
+ : undefined,
59
+ currentRefreshToken: row.currentRefreshToken,
60
+ })
61
+
62
+ const selectTokenInfoQB = (db: AccountDb) =>
63
+ selectAccountQB(db, { includeDeactivated: true })
64
+ // uses "token_did_idx" index (though unlikely in practice)
65
+ .innerJoin('token', 'token.did', 'actor.did')
66
+ .leftJoin('device_account', (join) =>
67
+ join
68
+ // uses "device_account_pk" index
69
+ .on('device_account.did', '=', 'token.did')
70
+ // @ts-expect-error "deviceId" is nullable in token
71
+ .on('device_account.deviceId', '=', 'token.deviceId'),
72
+ )
73
+ .select([
74
+ 'token.tokenId',
75
+ 'token.createdAt',
76
+ 'token.updatedAt',
77
+ 'token.expiresAt',
78
+ 'token.clientId',
79
+ 'token.clientAuth',
80
+ 'token.deviceId',
81
+ 'token.did',
82
+ 'token.parameters',
83
+ 'token.details',
84
+ 'token.code',
85
+ 'token.currentRefreshToken',
86
+ 'device_account.authenticatedAt',
87
+ 'device_account.authorizedClients',
88
+ 'device_account.remember',
89
+ ])
90
+
91
+ export const createQB = (
92
+ db: AccountDb,
93
+ tokenId: TokenId,
94
+ data: TokenData,
95
+ refreshToken?: RefreshToken,
96
+ ) =>
97
+ db.db.insertInto('token').values({
98
+ tokenId,
99
+ createdAt: toDateISO(data.createdAt),
100
+ expiresAt: toDateISO(data.expiresAt),
101
+ updatedAt: toDateISO(data.updatedAt),
102
+ clientId: data.clientId,
103
+ clientAuth: toJsonObject(data.clientAuth),
104
+ deviceId: data.deviceId,
105
+ did: data.sub,
106
+ parameters: toJsonObject(data.parameters),
107
+ details: data.details ? toJsonArray(data.details) : null,
108
+ code: data.code,
109
+ currentRefreshToken: refreshToken || null,
110
+ })
111
+
112
+ export const forRotateQB = (db: AccountDb, id: TokenId) =>
113
+ db.db
114
+ .selectFrom('token')
115
+ .where('tokenId', '=', id)
116
+ .where('currentRefreshToken', 'is not', null)
117
+ .select(['id', 'currentRefreshToken'])
118
+
119
+ export const findByQB = (
120
+ db: AccountDb,
121
+ search: {
122
+ id?: number
123
+ code?: Code
124
+ tokenId?: TokenId
125
+ currentRefreshToken?: RefreshToken
126
+ },
127
+ ) => {
128
+ if (
129
+ search.id === undefined &&
130
+ search.code === undefined &&
131
+ search.tokenId === undefined &&
132
+ search.currentRefreshToken === undefined
133
+ ) {
134
+ // Prevent accidental scan
135
+ throw new TypeError('At least one search parameter is required')
136
+ }
137
+
138
+ return selectTokenInfoQB(db)
139
+ .if(search.id !== undefined, (qb) =>
140
+ // uses primary key index
141
+ qb.where('token.id', '=', search.id!),
142
+ )
143
+ .if(search.code !== undefined, (qb) =>
144
+ // uses "token_code_idx" partial index (hence the null check)
145
+ qb
146
+ .where('token.code', '=', search.code!)
147
+ .where('token.code', 'is not', null),
148
+ )
149
+ .if(search.tokenId !== undefined, (qb) =>
150
+ // uses "token_token_id_idx"
151
+ qb.where('token.tokenId', '=', search.tokenId!),
152
+ )
153
+ .if(search.currentRefreshToken !== undefined, (qb) =>
154
+ // uses "token_refresh_token_unique_idx"
155
+ qb.where('token.currentRefreshToken', '=', search.currentRefreshToken!),
156
+ )
157
+ }
158
+
159
+ export const removeByDidQB = (db: AccountDb, did: string) =>
160
+ // uses "token_did_idx" index
161
+ db.db.deleteFrom('token').where('did', '=', did)
162
+
163
+ export const rotateQB = (
164
+ db: AccountDb,
165
+ id: number,
166
+ newTokenId: TokenId,
167
+ newRefreshToken: RefreshToken,
168
+ newData: NewTokenData,
169
+ ) =>
170
+ db.db
171
+ .updateTable('token')
172
+ .set({
173
+ tokenId: newTokenId,
174
+ currentRefreshToken: newRefreshToken,
175
+
176
+ expiresAt: toDateISO(newData.expiresAt),
177
+ updatedAt: toDateISO(newData.updatedAt),
178
+ clientAuth: toJsonObject(newData.clientAuth),
179
+ })
180
+ // uses primary key index
181
+ .where('id', '=', id)
182
+
183
+ export const removeQB = (db: AccountDb, tokenId: TokenId) =>
184
+ // uses "used_refresh_token_fk" to cascade delete
185
+ db.db.deleteFrom('token').where('tokenId', '=', tokenId)
@@ -0,0 +1,30 @@
1
+ import { RefreshToken } from '@atproto/oauth-provider'
2
+ import { AccountDb } from '../db'
3
+
4
+ /**
5
+ * Note that the used refresh tokens will be removed once the token is revoked.
6
+ * This is done through the foreign key constraint in the database.
7
+ */
8
+ export const insertQB = (
9
+ db: AccountDb,
10
+ tokenId: number,
11
+ refreshToken: RefreshToken,
12
+ ) =>
13
+ db.db
14
+ .insertInto('used_refresh_token')
15
+ .values({ tokenId, refreshToken })
16
+ .onConflict((oc) => oc.doNothing())
17
+
18
+ export const findByTokenQB = (db: AccountDb, refreshToken: RefreshToken) =>
19
+ db.db
20
+ .selectFrom('used_refresh_token')
21
+ // uses primary key index
22
+ .where('refreshToken', '=', refreshToken)
23
+ .select('tokenId')
24
+
25
+ export const countQB = (db: AccountDb, refreshToken: RefreshToken) =>
26
+ db.db
27
+ .selectFrom('used_refresh_token')
28
+ // uses primary key index
29
+ .where('refreshToken', '=', refreshToken)
30
+ .select((qb) => qb.fn.count<number>('refreshToken').as('count'))
@@ -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
  }
@@ -17,7 +17,7 @@ export class PreferenceReader {
17
17
  return prefsRes
18
18
  .filter((pref) => !namespace || prefMatchNamespace(namespace, pref.name))
19
19
  .filter((pref) => prefInScope(scope, pref.name))
20
- .map((pref) => JSON.parse(pref.valueJson))
20
+ .map((pref) => JSON.parse(pref.valueJson) as AccountPreference)
21
21
  }
22
22
  }
23
23
 
@@ -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
  },