@atproto/pds 0.4.34 → 0.4.35

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