@atproto/pds 0.4.123 → 0.4.124

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 (184) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/account-manager/account-manager.js +17 -7
  3. package/dist/account-manager/account-manager.js.map +1 -1
  4. package/dist/account-manager/db/index.d.ts.map +1 -1
  5. package/dist/account-manager/db/migrations/005-oauth-account-management.d.ts +20 -0
  6. package/dist/account-manager/db/migrations/005-oauth-account-management.d.ts.map +1 -0
  7. package/dist/account-manager/db/migrations/005-oauth-account-management.js +72 -0
  8. package/dist/account-manager/db/migrations/005-oauth-account-management.js.map +1 -0
  9. package/dist/account-manager/db/migrations/index.d.ts +2 -0
  10. package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
  11. package/dist/account-manager/db/migrations/index.js +19 -7
  12. package/dist/account-manager/db/migrations/index.js.map +1 -1
  13. package/dist/account-manager/db/schema/account-device.d.ts +13 -0
  14. package/dist/account-manager/db/schema/account-device.d.ts.map +1 -0
  15. package/dist/account-manager/db/schema/{device-account.js → account-device.js} +2 -2
  16. package/dist/account-manager/db/schema/account-device.js.map +1 -0
  17. package/dist/account-manager/db/schema/authorization-request.d.ts +4 -4
  18. package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -1
  19. package/dist/account-manager/db/schema/authorization-request.js.map +1 -1
  20. package/dist/account-manager/db/schema/authorized-client.d.ts +16 -0
  21. package/dist/account-manager/db/schema/authorized-client.d.ts.map +1 -0
  22. package/dist/account-manager/db/schema/authorized-client.js +5 -0
  23. package/dist/account-manager/db/schema/authorized-client.js.map +1 -0
  24. package/dist/account-manager/db/schema/index.d.ts +4 -3
  25. package/dist/account-manager/db/schema/index.d.ts.map +1 -1
  26. package/dist/account-manager/db/schema/token.d.ts +5 -5
  27. package/dist/account-manager/db/schema/token.d.ts.map +1 -1
  28. package/dist/account-manager/db/schema/token.js.map +1 -1
  29. package/dist/account-manager/helpers/account-device.d.ts +204 -0
  30. package/dist/account-manager/helpers/account-device.d.ts.map +1 -0
  31. package/dist/account-manager/helpers/account-device.js +54 -0
  32. package/dist/account-manager/helpers/account-device.js.map +1 -0
  33. package/dist/account-manager/helpers/account.d.ts +2 -1
  34. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  35. package/dist/account-manager/helpers/auth.d.ts.map +1 -1
  36. package/dist/account-manager/helpers/auth.js +17 -7
  37. package/dist/account-manager/helpers/auth.js.map +1 -1
  38. package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -1
  39. package/dist/account-manager/helpers/authorization-request.js +4 -4
  40. package/dist/account-manager/helpers/authorization-request.js.map +1 -1
  41. package/dist/account-manager/helpers/authorized-client.d.ts +6 -0
  42. package/dist/account-manager/helpers/authorized-client.d.ts.map +1 -0
  43. package/dist/account-manager/helpers/authorized-client.js +47 -0
  44. package/dist/account-manager/helpers/authorized-client.js.map +1 -0
  45. package/dist/account-manager/helpers/device.d.ts +1 -1
  46. package/dist/account-manager/helpers/device.d.ts.map +1 -1
  47. package/dist/account-manager/helpers/device.js.map +1 -1
  48. package/dist/account-manager/helpers/email-token.d.ts.map +1 -1
  49. package/dist/account-manager/helpers/invite.d.ts.map +1 -1
  50. package/dist/account-manager/helpers/password.d.ts.map +1 -1
  51. package/dist/account-manager/helpers/password.js +17 -7
  52. package/dist/account-manager/helpers/password.js.map +1 -1
  53. package/dist/account-manager/helpers/repo.d.ts.map +1 -1
  54. package/dist/account-manager/helpers/scrypt.d.ts.map +1 -1
  55. package/dist/account-manager/helpers/scrypt.js +17 -7
  56. package/dist/account-manager/helpers/scrypt.js.map +1 -1
  57. package/dist/account-manager/helpers/token.d.ts +566 -59
  58. package/dist/account-manager/helpers/token.d.ts.map +1 -1
  59. package/dist/account-manager/helpers/token.js +17 -32
  60. package/dist/account-manager/helpers/token.js.map +1 -1
  61. package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -1
  62. package/dist/account-manager/oauth-store.d.ts +17 -7
  63. package/dist/account-manager/oauth-store.d.ts.map +1 -1
  64. package/dist/account-manager/oauth-store.js +138 -86
  65. package/dist/account-manager/oauth-store.js.map +1 -1
  66. package/dist/actor-store/actor-store.js +17 -7
  67. package/dist/actor-store/actor-store.js.map +1 -1
  68. package/dist/actor-store/blob/transactor.js +17 -7
  69. package/dist/actor-store/blob/transactor.js.map +1 -1
  70. package/dist/actor-store/db/index.d.ts.map +1 -1
  71. package/dist/actor-store/db/migrations/index.js +17 -7
  72. package/dist/actor-store/db/migrations/index.js.map +1 -1
  73. package/dist/actor-store/migrate.d.ts.map +1 -1
  74. package/dist/actor-store/preference/reader.d.ts.map +1 -1
  75. package/dist/actor-store/preference/util.d.ts.map +1 -1
  76. package/dist/actor-store/record/reader.d.ts.map +1 -1
  77. package/dist/actor-store/record/reader.js +17 -7
  78. package/dist/actor-store/record/reader.js.map +1 -1
  79. package/dist/actor-store/repo/sql-repo-reader.d.ts +1 -1
  80. package/dist/api/app/bsky/util/resolver.d.ts.map +1 -1
  81. package/dist/api/com/atproto/identity/signPlcOperation.js +17 -7
  82. package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
  83. package/dist/api/com/atproto/identity/submitPlcOperation.js +17 -7
  84. package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
  85. package/dist/api/com/atproto/repo/describeRepo.js +17 -7
  86. package/dist/api/com/atproto/repo/describeRepo.js.map +1 -1
  87. package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
  88. package/dist/api/com/atproto/server/createAccount.js +17 -7
  89. package/dist/api/com/atproto/server/createAccount.js.map +1 -1
  90. package/dist/api/com/atproto/server/util.d.ts.map +1 -1
  91. package/dist/api/com/atproto/server/util.js +17 -7
  92. package/dist/api/com/atproto/server/util.js.map +1 -1
  93. package/dist/api/com/atproto/sync/getRecord.js +17 -7
  94. package/dist/api/com/atproto/sync/getRecord.js.map +1 -1
  95. package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
  96. package/dist/api/com/atproto/sync/util.d.ts.map +1 -1
  97. package/dist/api/proxy.d.ts.map +1 -1
  98. package/dist/auth-routes.d.ts.map +1 -1
  99. package/dist/auth-routes.js +2 -3
  100. package/dist/auth-routes.js.map +1 -1
  101. package/dist/auth-verifier.d.ts.map +1 -1
  102. package/dist/auth-verifier.js +19 -13
  103. package/dist/auth-verifier.js.map +1 -1
  104. package/dist/basic-routes.d.ts.map +1 -1
  105. package/dist/config/config.d.ts.map +1 -1
  106. package/dist/config/config.js +1 -1
  107. package/dist/config/config.js.map +1 -1
  108. package/dist/config/env.d.ts +1 -1
  109. package/dist/config/env.d.ts.map +1 -1
  110. package/dist/config/env.js +1 -1
  111. package/dist/config/env.js.map +1 -1
  112. package/dist/config/secrets.d.ts.map +1 -1
  113. package/dist/context.js +18 -8
  114. package/dist/context.js.map +1 -1
  115. package/dist/db/cast.d.ts +17 -13
  116. package/dist/db/cast.d.ts.map +1 -1
  117. package/dist/db/cast.js +13 -52
  118. package/dist/db/cast.js.map +1 -1
  119. package/dist/db/pagination.d.ts.map +1 -1
  120. package/dist/db/util.d.ts.map +1 -1
  121. package/dist/did-cache/db/index.d.ts.map +1 -1
  122. package/dist/disk-blobstore.d.ts.map +1 -1
  123. package/dist/handle/explicit-slurs.d.ts.map +1 -1
  124. package/dist/handle/index.d.ts.map +1 -1
  125. package/dist/index.js +17 -7
  126. package/dist/index.js.map +1 -1
  127. package/dist/lexicon/util.d.ts.map +1 -1
  128. package/dist/mailer/index.js +17 -7
  129. package/dist/mailer/index.js.map +1 -1
  130. package/dist/pipethrough.d.ts.map +1 -1
  131. package/dist/read-after-write/util.d.ts.map +1 -1
  132. package/dist/redis.d.ts.map +1 -1
  133. package/dist/repo/prepare.d.ts.map +1 -1
  134. package/dist/repo/prepare.js +17 -7
  135. package/dist/repo/prepare.js.map +1 -1
  136. package/dist/scripts/publish-identity.d.ts.map +1 -1
  137. package/dist/scripts/rebuild-repo.d.ts.map +1 -1
  138. package/dist/scripts/rotate-keys.d.ts.map +1 -1
  139. package/dist/scripts/sequencer-recovery/index.d.ts.map +1 -1
  140. package/dist/scripts/sequencer-recovery/recoverer.d.ts.map +1 -1
  141. package/dist/scripts/sequencer-recovery/recovery-db.d.ts.map +1 -1
  142. package/dist/scripts/sequencer-recovery/repair-repos.d.ts.map +1 -1
  143. package/dist/scripts/util.d.ts.map +1 -1
  144. package/dist/sequencer/db/index.d.ts.map +1 -1
  145. package/dist/sequencer/db/migrations/index.js +17 -7
  146. package/dist/sequencer/db/migrations/index.js.map +1 -1
  147. package/dist/sequencer/events.d.ts +6 -6
  148. package/dist/sequencer/events.d.ts.map +1 -1
  149. package/dist/sequencer/sequencer.d.ts.map +1 -1
  150. package/dist/util/debug.d.ts.map +1 -1
  151. package/dist/util/params.d.ts.map +1 -1
  152. package/dist/well-known.d.ts.map +1 -1
  153. package/package.json +5 -4
  154. package/src/account-manager/db/migrations/005-oauth-account-management.ts +112 -0
  155. package/src/account-manager/db/migrations/index.ts +2 -0
  156. package/src/account-manager/db/schema/account-device.ts +14 -0
  157. package/src/account-manager/db/schema/authorization-request.ts +5 -3
  158. package/src/account-manager/db/schema/authorized-client.ts +19 -0
  159. package/src/account-manager/db/schema/index.ts +5 -3
  160. package/src/account-manager/db/schema/token.ts +7 -4
  161. package/src/account-manager/helpers/account-device.ts +66 -0
  162. package/src/account-manager/helpers/authorization-request.ts +5 -5
  163. package/src/account-manager/helpers/authorized-client.ts +69 -0
  164. package/src/account-manager/helpers/device.ts +3 -1
  165. package/src/account-manager/helpers/token.ts +19 -57
  166. package/src/account-manager/oauth-store.ts +182 -103
  167. package/src/auth-routes.ts +11 -7
  168. package/src/auth-verifier.ts +2 -7
  169. package/src/config/config.ts +1 -1
  170. package/src/config/env.ts +2 -2
  171. package/src/context.ts +2 -2
  172. package/src/db/cast.ts +43 -50
  173. package/tests/db.test.ts +2 -1
  174. package/tsconfig.build.tsbuildinfo +1 -1
  175. package/tsconfig.tests.tsbuildinfo +1 -1
  176. package/dist/account-manager/db/schema/device-account.d.ts +0 -14
  177. package/dist/account-manager/db/schema/device-account.d.ts.map +0 -1
  178. package/dist/account-manager/db/schema/device-account.js.map +0 -1
  179. package/dist/account-manager/helpers/device-account.d.ts +0 -108
  180. package/dist/account-manager/helpers/device-account.d.ts.map +0 -1
  181. package/dist/account-manager/helpers/device-account.js +0 -83
  182. package/dist/account-manager/helpers/device-account.js.map +0 -1
  183. package/src/account-manager/db/schema/device-account.ts +0 -15
  184. package/src/account-manager/helpers/device-account.ts +0 -135
@@ -1,13 +1,16 @@
1
+ import assert from 'node:assert'
1
2
  import { Client, createOp as createPlcOp } from '@did-plc/lib'
2
3
  import { Selectable } from 'kysely'
3
4
  import { Keypair, Secp256k1Keypair } from '@atproto/crypto'
4
5
  import {
5
6
  Account,
6
- AccountInfo,
7
7
  AccountStore,
8
8
  AuthenticateAccountData,
9
+ AuthorizedClientData,
10
+ AuthorizedClients,
11
+ ClientId,
9
12
  Code,
10
- DeviceAccountInfo,
13
+ DeviceAccount,
11
14
  DeviceData,
12
15
  DeviceId,
13
16
  DeviceStore,
@@ -23,6 +26,7 @@ import {
23
26
  ResetPasswordConfirmData,
24
27
  ResetPasswordRequestData,
25
28
  SignUpData,
29
+ Sub,
26
30
  TokenData,
27
31
  TokenId,
28
32
  TokenInfo,
@@ -35,16 +39,21 @@ import {
35
39
  } from '@atproto/xrpc-server'
36
40
  import { ActorStore } from '../actor-store/actor-store'
37
41
  import { BackgroundQueue } from '../background'
42
+ import { fromDateISO } from '../db'
38
43
  import { ImageUrlBuilder } from '../image/image-url-builder'
44
+ import { dbLogger } from '../logger'
39
45
  import { ServerMailer } from '../mailer'
40
46
  import { Sequencer, syncEvtDataFromCommit } from '../sequencer'
41
47
  import { AccountManager } from './account-manager'
42
- import { AccountStatus, ActorAccount } from './helpers/account'
43
- import * as authRequest from './helpers/authorization-request'
44
- import * as device from './helpers/device'
45
- import * as deviceAccount from './helpers/device-account'
46
- import * as token from './helpers/token'
47
- import * as usedRefreshToken from './helpers/used-refresh-token'
48
+ import * as schemas from './db/schema'
49
+ import * as accountHelper from './helpers/account'
50
+ import { AccountStatus } from './helpers/account'
51
+ import * as accountDeviceHelper from './helpers/account-device'
52
+ import * as authRequestHelper from './helpers/authorization-request'
53
+ import * as authorizedClientHelper from './helpers/authorized-client'
54
+ import * as deviceHelper from './helpers/device'
55
+ import * as tokenHelper from './helpers/token'
56
+ import * as usedRefreshTokenHelper from './helpers/used-refresh-token'
48
57
 
49
58
  /**
50
59
  * This class' purpose is to implement the interface needed by the OAuthProvider
@@ -78,29 +87,6 @@ export class OAuthStore
78
87
  return this.accountManager.serviceDid
79
88
  }
80
89
 
81
- private async buildAccount(row: Selectable<ActorAccount>): Promise<Account> {
82
- const account = deviceAccount.toAccount(row, this.serviceDid)
83
-
84
- if (!account.name || !account.picture) {
85
- const did = account.sub
86
-
87
- const profile = await this.actorStore.read(did, async (store) => {
88
- return store.record.getProfileRecord()
89
- })
90
-
91
- if (profile) {
92
- const { avatar, displayName } = profile
93
-
94
- account.name ||= displayName
95
- account.picture ||= avatar
96
- ? this.imageUrlBuilder.build('avatar', did, avatar.ref.toString())
97
- : undefined
98
- }
99
- }
100
-
101
- return account
102
- }
103
-
104
90
  private async verifyEmailAvailability(email: string): Promise<void> {
105
91
  // @NOTE Email validity & disposability check performed by the OAuthProvider
106
92
 
@@ -244,72 +230,99 @@ export class OAuthStore
244
230
  }
245
231
  }
246
232
 
247
- async addDeviceAccount(
248
- deviceId: DeviceId,
249
- sub: string,
250
- remember: boolean,
251
- ): Promise<DeviceAccountInfo> {
252
- const [row] = await this.db.executeWithRetry(
253
- deviceAccount.createOrUpdateQB(this.db, deviceId, sub, remember),
254
- )
255
- if (!row) throw new Error('Failed to create device account')
256
- return deviceAccount.toDeviceAccountInfo(row)
257
- }
258
-
259
- async addAuthorizedClient(
260
- deviceId: DeviceId,
261
- sub: string,
262
- clientId: string,
233
+ async setAuthorizedClient(
234
+ sub: Sub,
235
+ clientId: ClientId,
236
+ data: AuthorizedClientData,
263
237
  ): Promise<void> {
264
- await this.db.transaction(async (dbTxn) => {
265
- const row = await deviceAccount
266
- .readQB(dbTxn, deviceId, sub)
267
- .executeTakeFirstOrThrow()
238
+ await authorizedClientHelper.upsert(this.db, sub, clientId, data)
239
+ }
268
240
 
269
- const { authorizedClients } = deviceAccount.toDeviceAccountInfo(row)
270
- if (!authorizedClients.includes(clientId)) {
271
- await deviceAccount
272
- .updateQB(dbTxn, deviceId, sub, {
273
- authorizedClients: [...authorizedClients, clientId],
274
- })
275
- .execute()
276
- }
241
+ async getAccount(sub: Sub): Promise<{
242
+ account: Account
243
+ authorizedClients: AuthorizedClients
244
+ }> {
245
+ const accountRow = await accountHelper.getAccount(this.db, sub, {
246
+ includeDeactivated: true,
277
247
  })
248
+
249
+ assert(accountRow, 'Account not found')
250
+
251
+ const account = await this.buildAccount(accountRow)
252
+ const authorizedClients = await authorizedClientHelper.getAuthorizedClients(
253
+ this.db,
254
+ sub,
255
+ )
256
+
257
+ return { account, authorizedClients }
258
+ }
259
+
260
+ async upsertDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {
261
+ await this.db.executeWithRetry(
262
+ accountDeviceHelper.upsertQB(this.db, deviceId, sub),
263
+ )
278
264
  }
279
265
 
280
266
  async getDeviceAccount(
281
267
  deviceId: DeviceId,
282
268
  sub: string,
283
- ): Promise<AccountInfo | null> {
284
- const row = await deviceAccount
285
- .getAccountInfoQB(this.db, deviceId, sub)
269
+ ): Promise<DeviceAccount | null> {
270
+ const row = await accountDeviceHelper
271
+ .selectQB(this.db, { deviceId, sub })
286
272
  .executeTakeFirst()
287
273
 
288
274
  if (!row) return null
289
275
 
290
276
  return {
277
+ deviceId,
278
+ deviceData: deviceHelper.rowToDeviceData(row),
291
279
  account: await this.buildAccount(row),
292
- info: deviceAccount.toDeviceAccountInfo(row),
280
+ authorizedClients: await authorizedClientHelper.getAuthorizedClients(
281
+ this.db,
282
+ sub,
283
+ ),
284
+ createdAt: fromDateISO(row.adCreatedAt),
285
+ updatedAt: fromDateISO(row.adUpdatedAt),
293
286
  }
294
287
  }
295
288
 
296
- async listDeviceAccounts(deviceId: DeviceId): Promise<AccountInfo[]> {
297
- const rows = await deviceAccount
298
- .listRememberedQB(this.db, deviceId)
299
- .execute()
300
-
301
- return Promise.all(
302
- rows.map(async (row) => ({
303
- account: await this.buildAccount(row),
304
- info: deviceAccount.toDeviceAccountInfo(row),
305
- })),
289
+ async removeDeviceAccount(deviceId: DeviceId, sub: Sub): Promise<void> {
290
+ await this.db.executeWithRetry(
291
+ accountDeviceHelper.removeQB(this.db, deviceId, sub),
306
292
  )
307
293
  }
308
294
 
309
- async removeDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {
310
- await this.db.executeWithRetry(
311
- deviceAccount.removeQB(this.db, deviceId, sub),
295
+ async listDeviceAccounts(
296
+ filter: { sub: Sub } | { deviceId: DeviceId },
297
+ ): Promise<DeviceAccount[]> {
298
+ const rows = await accountDeviceHelper.selectQB(this.db, filter).execute()
299
+
300
+ const uniqueDids = [...new Set(rows.map((row) => row.did))]
301
+
302
+ // Enrich all distinct account with their profile data
303
+ const accounts = new Map(
304
+ await Promise.all(
305
+ Array.from(uniqueDids, async (did): Promise<[Sub, Account]> => {
306
+ const row = rows.find((r) => r.did === did)!
307
+ return [did, await this.buildAccount(row)]
308
+ }),
309
+ ),
312
310
  )
311
+
312
+ const authorizedClientsMap =
313
+ await authorizedClientHelper.getAuthorizedClientsMulti(
314
+ this.db,
315
+ uniqueDids,
316
+ )
317
+
318
+ return rows.map((row) => ({
319
+ deviceId: row.deviceId,
320
+ deviceData: deviceHelper.rowToDeviceData(row),
321
+ account: accounts.get(row.did)!,
322
+ authorizedClients: authorizedClientsMap.get(row.did)!,
323
+ createdAt: fromDateISO(row.adCreatedAt),
324
+ updatedAt: fromDateISO(row.adUpdatedAt),
325
+ }))
313
326
  }
314
327
 
315
328
  async resetPasswordRequest({
@@ -383,58 +396,70 @@ export class OAuthStore
383
396
  // RequestStore
384
397
 
385
398
  async createRequest(id: RequestId, data: RequestData): Promise<void> {
386
- await this.db.executeWithRetry(authRequest.createQB(this.db, id, data))
399
+ await this.db.executeWithRetry(
400
+ authRequestHelper.createQB(this.db, id, data),
401
+ )
387
402
  }
388
403
 
389
404
  async readRequest(id: RequestId): Promise<RequestData | null> {
390
405
  try {
391
- const row = await authRequest.readQB(this.db, id).executeTakeFirst()
406
+ const row = await authRequestHelper.readQB(this.db, id).executeTakeFirst()
392
407
  if (!row) return null
393
- return authRequest.rowToRequestData(row)
408
+ return authRequestHelper.rowToRequestData(row)
394
409
  } finally {
395
410
  // Take the opportunity to clean up expired requests. Do this after we got
396
411
  // the current (potentially expired) request data to allow the provider to
397
412
  // handle expired requests.
398
413
  this.backgroundQueue.add(async () => {
399
- await this.db.executeWithRetry(authRequest.removeOldExpiredQB(this.db))
414
+ await this.db.executeWithRetry(
415
+ authRequestHelper.removeOldExpiredQB(this.db),
416
+ )
400
417
  })
401
418
  }
402
419
  }
403
420
 
404
421
  async updateRequest(id: RequestId, data: UpdateRequestData): Promise<void> {
405
- await this.db.executeWithRetry(authRequest.updateQB(this.db, id, data))
422
+ await this.db.executeWithRetry(
423
+ authRequestHelper.updateQB(this.db, id, data),
424
+ )
406
425
  }
407
426
 
408
427
  async deleteRequest(id: RequestId): Promise<void> {
409
- await this.db.executeWithRetry(authRequest.removeByIdQB(this.db, id))
428
+ await this.db.executeWithRetry(authRequestHelper.removeByIdQB(this.db, id))
410
429
  }
411
430
 
412
431
  async findRequestByCode(code: Code): Promise<FoundRequestResult | null> {
413
- const row = await authRequest.findByCodeQB(this.db, code).executeTakeFirst()
414
- return row ? authRequest.rowToFoundRequestResult(row) : null
432
+ const row = await authRequestHelper
433
+ .findByCodeQB(this.db, code)
434
+ .executeTakeFirst()
435
+ return row ? authRequestHelper.rowToFoundRequestResult(row) : null
415
436
  }
416
437
 
417
438
  // DeviceStore
418
439
 
419
440
  async createDevice(deviceId: DeviceId, data: DeviceData): Promise<void> {
420
- await this.db.executeWithRetry(device.createQB(this.db, deviceId, data))
441
+ await this.db.executeWithRetry(
442
+ deviceHelper.createQB(this.db, deviceId, data),
443
+ )
421
444
  }
422
445
 
423
446
  async readDevice(deviceId: DeviceId): Promise<null | DeviceData> {
424
- const row = await device.readQB(this.db, deviceId).executeTakeFirst()
425
- return row ? device.rowToDeviceData(row) : null
447
+ const row = await deviceHelper.readQB(this.db, deviceId).executeTakeFirst()
448
+ return row ? deviceHelper.rowToDeviceData(row) : null
426
449
  }
427
450
 
428
451
  async updateDevice(
429
452
  deviceId: DeviceId,
430
453
  data: Partial<DeviceData>,
431
454
  ): Promise<void> {
432
- await this.db.executeWithRetry(device.updateQB(this.db, deviceId, data))
455
+ await this.db.executeWithRetry(
456
+ deviceHelper.updateQB(this.db, deviceId, data),
457
+ )
433
458
  }
434
459
 
435
460
  async deleteDevice(deviceId: DeviceId): Promise<void> {
436
461
  // Will cascade to device_account (device_account_device_id_fk)
437
- await this.db.executeWithRetry(device.removeQB(this.db, deviceId))
462
+ await this.db.executeWithRetry(deviceHelper.removeQB(this.db, deviceId))
438
463
  }
439
464
 
440
465
  // TokenStore
@@ -446,7 +471,7 @@ export class OAuthStore
446
471
  ): Promise<void> {
447
472
  await this.db.transaction(async (dbTxn) => {
448
473
  if (refreshToken) {
449
- const { count } = await usedRefreshToken
474
+ const { count } = await usedRefreshTokenHelper
450
475
  .countQB(dbTxn, refreshToken)
451
476
  .executeTakeFirstOrThrow()
452
477
 
@@ -455,18 +480,25 @@ export class OAuthStore
455
480
  }
456
481
  }
457
482
 
458
- return token.createQB(dbTxn, id, data, refreshToken).execute()
483
+ return tokenHelper.createQB(dbTxn, id, data, refreshToken).execute()
459
484
  })
460
485
  }
461
486
 
487
+ async listAccountTokens(sub: Sub): Promise<TokenInfo[]> {
488
+ const rows = await tokenHelper.findByQB(this.db, { did: sub }).execute()
489
+ return Promise.all(rows.map((row) => this.toTokenInfo(row)))
490
+ }
491
+
462
492
  async readToken(tokenId: TokenId): Promise<TokenInfo | null> {
463
- const row = await token.findByQB(this.db, { tokenId }).executeTakeFirst()
464
- return row ? token.toTokenInfo(row, this.serviceDid) : null
493
+ const row = await tokenHelper
494
+ .findByQB(this.db, { tokenId })
495
+ .executeTakeFirst()
496
+ return row ? this.toTokenInfo(row) : null
465
497
  }
466
498
 
467
499
  async deleteToken(tokenId: TokenId): Promise<void> {
468
500
  // Will cascade to used_refresh_token (used_refresh_token_fk)
469
- await this.db.executeWithRetry(token.removeQB(this.db, tokenId))
501
+ await this.db.executeWithRetry(tokenHelper.removeQB(this.db, tokenId))
470
502
  }
471
503
 
472
504
  async rotateToken(
@@ -476,17 +508,17 @@ export class OAuthStore
476
508
  newData: NewTokenData,
477
509
  ): Promise<void> {
478
510
  const err = await this.db.transaction(async (dbTxn) => {
479
- const { id, currentRefreshToken } = await token
511
+ const { id, currentRefreshToken } = await tokenHelper
480
512
  .forRotateQB(dbTxn, tokenId)
481
513
  .executeTakeFirstOrThrow()
482
514
 
483
515
  if (currentRefreshToken) {
484
- await usedRefreshToken
516
+ await usedRefreshTokenHelper
485
517
  .insertQB(dbTxn, id, currentRefreshToken)
486
518
  .execute()
487
519
  }
488
520
 
489
- const { count } = await usedRefreshToken
521
+ const { count } = await usedRefreshTokenHelper
490
522
  .countQB(dbTxn, newRefreshToken)
491
523
  .executeTakeFirstOrThrow()
492
524
 
@@ -495,7 +527,7 @@ export class OAuthStore
495
527
  return new Error('New refresh token already in use')
496
528
  }
497
529
 
498
- await token
530
+ await tokenHelper
499
531
  .rotateQB(dbTxn, id, newTokenId, newRefreshToken, newData)
500
532
  .execute()
501
533
  })
@@ -506,7 +538,7 @@ export class OAuthStore
506
538
  async findTokenByRefreshToken(
507
539
  refreshToken: RefreshToken,
508
540
  ): Promise<TokenInfo | null> {
509
- const used = await usedRefreshToken
541
+ const used = await usedRefreshTokenHelper
510
542
  .findByTokenQB(this.db, refreshToken)
511
543
  .executeTakeFirst()
512
544
 
@@ -514,12 +546,59 @@ export class OAuthStore
514
546
  ? { id: used.tokenId }
515
547
  : { currentRefreshToken: refreshToken }
516
548
 
517
- const row = await token.findByQB(this.db, search).executeTakeFirst()
518
- return row ? token.toTokenInfo(row, this.serviceDid) : null
549
+ const row = await tokenHelper.findByQB(this.db, search).executeTakeFirst()
550
+ return row ? this.toTokenInfo(row) : null
519
551
  }
520
552
 
521
553
  async findTokenByCode(code: Code): Promise<TokenInfo | null> {
522
- const row = await token.findByQB(this.db, { code }).executeTakeFirst()
523
- return row ? token.toTokenInfo(row, this.serviceDid) : null
554
+ const row = await tokenHelper.findByQB(this.db, { code }).executeTakeFirst()
555
+ return row ? this.toTokenInfo(row) : null
556
+ }
557
+
558
+ private async toTokenInfo(
559
+ row: accountHelper.ActorAccount & Selectable<schemas.Token>,
560
+ ): Promise<TokenInfo> {
561
+ return {
562
+ id: row.tokenId,
563
+ data: tokenHelper.toTokenData(row),
564
+ account: await this.buildAccount(row),
565
+ currentRefreshToken: row.currentRefreshToken,
566
+ }
567
+ }
568
+
569
+ private async buildAccount(
570
+ row: accountHelper.ActorAccount,
571
+ ): Promise<Account> {
572
+ const account: Account = {
573
+ sub: row.did,
574
+ aud: this.serviceDid,
575
+ email: row.email || undefined,
576
+ email_verified: row.email ? row.emailConfirmedAt != null : undefined,
577
+ preferred_username: row.handle || undefined,
578
+ }
579
+
580
+ if (!account.name || !account.picture) {
581
+ const did = account.sub
582
+
583
+ const profile = await this.actorStore
584
+ .read(did, async (store) => {
585
+ return store.record.getProfileRecord()
586
+ })
587
+ .catch((err) => {
588
+ dbLogger.error({ err }, 'Failed to get profile record')
589
+ return null // No need to propagate
590
+ })
591
+
592
+ if (profile) {
593
+ const { avatar, displayName } = profile
594
+
595
+ account.name ||= displayName
596
+ account.picture ||= avatar
597
+ ? this.imageUrlBuilder.build('avatar', did, avatar.ref.toString())
598
+ : undefined
599
+ }
600
+ }
601
+
602
+ return account
524
603
  }
525
604
  }
@@ -1,5 +1,8 @@
1
1
  import { Router } from 'express'
2
- import { oauthProtectedResourceMetadataSchema } from '@atproto/oauth-provider'
2
+ import {
3
+ oauthMiddleware,
4
+ oauthProtectedResourceMetadataSchema,
5
+ } from '@atproto/oauth-provider'
3
6
  import { AppContext } from './context'
4
7
  import { oauthLogger } from './logger'
5
8
 
@@ -30,12 +33,13 @@ export const createRouter = ({ oauthProvider, cfg }: AppContext): Router => {
30
33
  })
31
34
 
32
35
  if (oauthProvider) {
33
- const oauthMiddleware = oauthProvider.httpHandler({
34
- onError: (req, res, err, message) => {
35
- oauthLogger.error({ err, req }, message)
36
- },
37
- })
38
- router.use(oauthMiddleware)
36
+ router.use(
37
+ oauthMiddleware(oauthProvider, {
38
+ onError: (req, res, err, message) => {
39
+ oauthLogger.error({ err, req }, message)
40
+ },
41
+ }),
42
+ )
39
43
  }
40
44
 
41
45
  return router
@@ -139,7 +139,7 @@ export class AuthVerifier {
139
139
 
140
140
  accessStandard =
141
141
  (opts: Partial<AccessOpts> = {}) =>
142
- (ctx: ReqCtx): Promise<AccessOutput> => {
142
+ async (ctx: ReqCtx): Promise<AccessOutput> => {
143
143
  return this.validateAccessToken(
144
144
  ctx,
145
145
  [
@@ -528,18 +528,13 @@ export class AuthVerifier {
528
528
  )
529
529
  }
530
530
 
531
- const isPrivileged = [
532
- AuthScope.Access,
533
- AuthScope.AppPassPrivileged,
534
- ].includes(scopeEquivalent)
535
-
536
531
  return {
537
532
  credentials: {
538
533
  type: 'access',
539
534
  did: result.claims.sub,
540
535
  scope: scopeEquivalent,
541
536
  audience: this.dids.pds,
542
- isPrivileged,
537
+ isPrivileged: scopeEquivalent === AuthScope.AppPassPrivileged,
543
538
  },
544
539
  artifacts: result.token,
545
540
  }
@@ -275,7 +275,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
275
275
  name: env.serviceName ?? 'Personal PDS',
276
276
  logo: env.logoUrl,
277
277
  colors: {
278
- brand: env.brandColor,
278
+ primary: env.primaryColor,
279
279
  error: env.errorColor,
280
280
  success: env.successColor,
281
281
  warning: env.warningColor,
package/src/config/env.ts CHANGED
@@ -24,7 +24,7 @@ export const readEnv = (): ServerEnvironment => {
24
24
  hcaptchaTokenSalt: envStr('PDS_HCAPTCHA_TOKEN_SALT'),
25
25
 
26
26
  // branding
27
- brandColor: envStr('PDS_PRIMARY_COLOR'),
27
+ primaryColor: envStr('PDS_PRIMARY_COLOR'),
28
28
  errorColor: envStr('PDS_ERROR_COLOR'),
29
29
  warningColor: envStr('PDS_WARNING_COLOR'),
30
30
  successColor: envStr('PDS_SUCCESS_COLOR'),
@@ -163,7 +163,7 @@ export type ServerEnvironment = {
163
163
  hcaptchaTokenSalt?: string
164
164
 
165
165
  // branding
166
- brandColor?: string
166
+ primaryColor?: string
167
167
  errorColor?: string
168
168
  warningColor?: string
169
169
  successColor?: string
package/src/context.ts CHANGED
@@ -10,7 +10,7 @@ import { KmsKeypair, S3BlobStore } from '@atproto/aws'
10
10
  import * as crypto from '@atproto/crypto'
11
11
  import { IdResolver } from '@atproto/identity'
12
12
  import {
13
- AccessTokenType,
13
+ AccessTokenMode,
14
14
  JoseKey,
15
15
  OAuthProvider,
16
16
  OAuthVerifier,
@@ -369,7 +369,7 @@ export class AppContext {
369
369
  // entryway), there is no need to use JWTs as access tokens. Instead,
370
370
  // the PDS can use tokenId as access tokens. This allows the PDS to
371
371
  // always use up-to-date token data from the token store.
372
- accessTokenType: AccessTokenType.id,
372
+ accessTokenMode: AccessTokenMode.light,
373
373
  })
374
374
  : undefined
375
375
 
package/src/db/cast.ts CHANGED
@@ -1,59 +1,52 @@
1
1
  export type DateISO = `${string}T${string}Z`
2
- export const toDateISO = (date: Date): DateISO => date.toISOString() as DateISO
3
- export const fromDateISO = (date: DateISO): Date => new Date(date)
2
+ export function toDateISO(date: Date) {
3
+ return date.toISOString() as DateISO
4
+ }
5
+ export function fromDateISO(dateStr: DateISO) {
6
+ return new Date(dateStr)
7
+ }
8
+
9
+ /**
10
+ * Allows to ensure that {@link JsonEncoded} is not used with non-JSON
11
+ * serializable values (e.g. {@link Date} or {@link Function}s).
12
+ */
13
+ export type Encodable =
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | readonly Encodable[]
19
+ | { readonly [_ in string]?: Encodable }
20
+
21
+ export type JsonString<T extends Encodable> = T extends readonly unknown[]
22
+ ? `[${string}]`
23
+ : T extends object
24
+ ? `{${string}}`
25
+ : T extends string
26
+ ? `"${string}"`
27
+ : T extends number
28
+ ? `${number}`
29
+ : T extends boolean
30
+ ? `true` | `false`
31
+ : T extends null
32
+ ? `null`
33
+ : never
4
34
 
5
- export type Json = string
6
- export const toJson = (obj: unknown): Json => {
7
- const json = JSON.stringify(obj)
35
+ declare const jsonEncodedType: unique symbol
36
+ export type JsonEncoded<T extends Encodable = Encodable> = JsonString<T> & {
37
+ [jsonEncodedType]: T
38
+ }
39
+
40
+ export function toJson<T extends Encodable>(value: T): JsonEncoded<T> {
41
+ const json = JSON.stringify(value)
8
42
  if (json === undefined) throw new TypeError('Input not JSONifyable')
9
- return json as Json
43
+ return json as JsonEncoded<T>
10
44
  }
11
- export const fromJson = <T>(json: Json): T => {
45
+
46
+ export function fromJson<T extends Encodable>(jsonStr: JsonEncoded<T>): T {
12
47
  try {
13
- return JSON.parse(json) as T
48
+ return JSON.parse(jsonStr) as T
14
49
  } catch (cause) {
15
50
  throw new TypeError('Database contains invalid JSON', { cause })
16
51
  }
17
52
  }
18
-
19
- export type JsonArray = `[${string}]`
20
- export const isJsonArray = (json: string): json is JsonArray =>
21
- // Although the JSON in the DB should have been encoded using toJson,
22
- // there should not be any leading or trailing whitespace. We will still trim
23
- // the string to protect against any manual editing of the DB.
24
- json.trimStart().startsWith('[') && json.trimEnd().endsWith(']')
25
- export function assertJsonArray(json: string): asserts json is JsonArray {
26
- if (!isJsonArray(json)) throw new TypeError('Not an Array')
27
- }
28
- export const toJsonArray = (obj: readonly unknown[]): JsonArray => {
29
- const json = toJson(obj)
30
- assertJsonArray(json)
31
- return json as JsonArray
32
- }
33
- export const fromJsonArray = <T>(json: JsonArray): T[] => {
34
- assertJsonArray(json)
35
- return fromJson(json) as T[]
36
- }
37
-
38
- export type JsonObject = `{${string}}`
39
- const isJsonObject = (json: string): json is JsonObject =>
40
- // Although the JSON in the DB should have been encoded using toJson,
41
- // there should not be any leading or trailing whitespace. We will still trim
42
- // the string to protect against any manual editing of the DB.
43
- json.trimStart().startsWith('{') && json.trimEnd().endsWith('}')
44
- function assertJsonObject(json: string): asserts json is JsonObject {
45
- if (!isJsonObject(json)) throw new TypeError('Not an Object')
46
- }
47
- export const toJsonObject = (
48
- obj: Readonly<Record<string, unknown>>,
49
- ): JsonObject => {
50
- const json = toJson(obj)
51
- assertJsonObject(json)
52
- return json as JsonObject
53
- }
54
- export const fromJsonObject = <T extends Record<string, unknown>>(
55
- json: JsonObject,
56
- ): T => {
57
- assertJsonObject(json)
58
- return fromJson(json) as T
59
- }
package/tests/db.test.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { TestNetworkNoAppView } from '@atproto/dev-env'
2
- import { AccountDb } from '../src/account-manager/db'
2
+ // Importing from `dist` to circumvent circular dependency typing issues
3
+ import { AccountDb } from '../dist/account-manager/db'
3
4
 
4
5
  describe('db', () => {
5
6
  let network: TestNetworkNoAppView