@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.
- package/CHANGELOG.md +9 -0
- package/dist/account-manager/account-manager.js +17 -7
- package/dist/account-manager/account-manager.js.map +1 -1
- package/dist/account-manager/db/index.d.ts.map +1 -1
- package/dist/account-manager/db/migrations/005-oauth-account-management.d.ts +20 -0
- package/dist/account-manager/db/migrations/005-oauth-account-management.d.ts.map +1 -0
- package/dist/account-manager/db/migrations/005-oauth-account-management.js +72 -0
- package/dist/account-manager/db/migrations/005-oauth-account-management.js.map +1 -0
- package/dist/account-manager/db/migrations/index.d.ts +2 -0
- package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
- package/dist/account-manager/db/migrations/index.js +19 -7
- package/dist/account-manager/db/migrations/index.js.map +1 -1
- package/dist/account-manager/db/schema/account-device.d.ts +13 -0
- package/dist/account-manager/db/schema/account-device.d.ts.map +1 -0
- package/dist/account-manager/db/schema/{device-account.js → account-device.js} +2 -2
- package/dist/account-manager/db/schema/account-device.js.map +1 -0
- package/dist/account-manager/db/schema/authorization-request.d.ts +4 -4
- package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -1
- package/dist/account-manager/db/schema/authorization-request.js.map +1 -1
- package/dist/account-manager/db/schema/authorized-client.d.ts +16 -0
- package/dist/account-manager/db/schema/authorized-client.d.ts.map +1 -0
- package/dist/account-manager/db/schema/authorized-client.js +5 -0
- package/dist/account-manager/db/schema/authorized-client.js.map +1 -0
- package/dist/account-manager/db/schema/index.d.ts +4 -3
- package/dist/account-manager/db/schema/index.d.ts.map +1 -1
- package/dist/account-manager/db/schema/token.d.ts +5 -5
- package/dist/account-manager/db/schema/token.d.ts.map +1 -1
- package/dist/account-manager/db/schema/token.js.map +1 -1
- package/dist/account-manager/helpers/account-device.d.ts +204 -0
- package/dist/account-manager/helpers/account-device.d.ts.map +1 -0
- package/dist/account-manager/helpers/account-device.js +54 -0
- package/dist/account-manager/helpers/account-device.js.map +1 -0
- package/dist/account-manager/helpers/account.d.ts +2 -1
- package/dist/account-manager/helpers/account.d.ts.map +1 -1
- package/dist/account-manager/helpers/auth.d.ts.map +1 -1
- package/dist/account-manager/helpers/auth.js +17 -7
- package/dist/account-manager/helpers/auth.js.map +1 -1
- package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -1
- package/dist/account-manager/helpers/authorization-request.js +4 -4
- package/dist/account-manager/helpers/authorization-request.js.map +1 -1
- package/dist/account-manager/helpers/authorized-client.d.ts +6 -0
- package/dist/account-manager/helpers/authorized-client.d.ts.map +1 -0
- package/dist/account-manager/helpers/authorized-client.js +47 -0
- package/dist/account-manager/helpers/authorized-client.js.map +1 -0
- package/dist/account-manager/helpers/device.d.ts +1 -1
- package/dist/account-manager/helpers/device.d.ts.map +1 -1
- package/dist/account-manager/helpers/device.js.map +1 -1
- package/dist/account-manager/helpers/email-token.d.ts.map +1 -1
- package/dist/account-manager/helpers/invite.d.ts.map +1 -1
- package/dist/account-manager/helpers/password.d.ts.map +1 -1
- package/dist/account-manager/helpers/password.js +17 -7
- package/dist/account-manager/helpers/password.js.map +1 -1
- package/dist/account-manager/helpers/repo.d.ts.map +1 -1
- package/dist/account-manager/helpers/scrypt.d.ts.map +1 -1
- package/dist/account-manager/helpers/scrypt.js +17 -7
- package/dist/account-manager/helpers/scrypt.js.map +1 -1
- package/dist/account-manager/helpers/token.d.ts +566 -59
- package/dist/account-manager/helpers/token.d.ts.map +1 -1
- package/dist/account-manager/helpers/token.js +17 -32
- package/dist/account-manager/helpers/token.js.map +1 -1
- package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -1
- package/dist/account-manager/oauth-store.d.ts +17 -7
- package/dist/account-manager/oauth-store.d.ts.map +1 -1
- package/dist/account-manager/oauth-store.js +138 -86
- package/dist/account-manager/oauth-store.js.map +1 -1
- package/dist/actor-store/actor-store.js +17 -7
- package/dist/actor-store/actor-store.js.map +1 -1
- package/dist/actor-store/blob/transactor.js +17 -7
- package/dist/actor-store/blob/transactor.js.map +1 -1
- package/dist/actor-store/db/index.d.ts.map +1 -1
- package/dist/actor-store/db/migrations/index.js +17 -7
- package/dist/actor-store/db/migrations/index.js.map +1 -1
- package/dist/actor-store/migrate.d.ts.map +1 -1
- package/dist/actor-store/preference/reader.d.ts.map +1 -1
- package/dist/actor-store/preference/util.d.ts.map +1 -1
- package/dist/actor-store/record/reader.d.ts.map +1 -1
- package/dist/actor-store/record/reader.js +17 -7
- package/dist/actor-store/record/reader.js.map +1 -1
- package/dist/actor-store/repo/sql-repo-reader.d.ts +1 -1
- package/dist/api/app/bsky/util/resolver.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.js +17 -7
- package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
- package/dist/api/com/atproto/identity/submitPlcOperation.js +17 -7
- package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
- package/dist/api/com/atproto/repo/describeRepo.js +17 -7
- package/dist/api/com/atproto/repo/describeRepo.js.map +1 -1
- package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
- package/dist/api/com/atproto/server/createAccount.js +17 -7
- package/dist/api/com/atproto/server/createAccount.js.map +1 -1
- package/dist/api/com/atproto/server/util.d.ts.map +1 -1
- package/dist/api/com/atproto/server/util.js +17 -7
- package/dist/api/com/atproto/server/util.js.map +1 -1
- package/dist/api/com/atproto/sync/getRecord.js +17 -7
- package/dist/api/com/atproto/sync/getRecord.js.map +1 -1
- package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/util.d.ts.map +1 -1
- package/dist/api/proxy.d.ts.map +1 -1
- package/dist/auth-routes.d.ts.map +1 -1
- package/dist/auth-routes.js +2 -3
- package/dist/auth-routes.js.map +1 -1
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +19 -13
- package/dist/auth-verifier.js.map +1 -1
- package/dist/basic-routes.d.ts.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +1 -1
- package/dist/config/env.js.map +1 -1
- package/dist/config/secrets.d.ts.map +1 -1
- package/dist/context.js +18 -8
- package/dist/context.js.map +1 -1
- package/dist/db/cast.d.ts +17 -13
- package/dist/db/cast.d.ts.map +1 -1
- package/dist/db/cast.js +13 -52
- package/dist/db/cast.js.map +1 -1
- package/dist/db/pagination.d.ts.map +1 -1
- package/dist/db/util.d.ts.map +1 -1
- package/dist/did-cache/db/index.d.ts.map +1 -1
- package/dist/disk-blobstore.d.ts.map +1 -1
- package/dist/handle/explicit-slurs.d.ts.map +1 -1
- package/dist/handle/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/lexicon/util.d.ts.map +1 -1
- package/dist/mailer/index.js +17 -7
- package/dist/mailer/index.js.map +1 -1
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/read-after-write/util.d.ts.map +1 -1
- package/dist/redis.d.ts.map +1 -1
- package/dist/repo/prepare.d.ts.map +1 -1
- package/dist/repo/prepare.js +17 -7
- package/dist/repo/prepare.js.map +1 -1
- package/dist/scripts/publish-identity.d.ts.map +1 -1
- package/dist/scripts/rebuild-repo.d.ts.map +1 -1
- package/dist/scripts/rotate-keys.d.ts.map +1 -1
- package/dist/scripts/sequencer-recovery/index.d.ts.map +1 -1
- package/dist/scripts/sequencer-recovery/recoverer.d.ts.map +1 -1
- package/dist/scripts/sequencer-recovery/recovery-db.d.ts.map +1 -1
- package/dist/scripts/sequencer-recovery/repair-repos.d.ts.map +1 -1
- package/dist/scripts/util.d.ts.map +1 -1
- package/dist/sequencer/db/index.d.ts.map +1 -1
- package/dist/sequencer/db/migrations/index.js +17 -7
- package/dist/sequencer/db/migrations/index.js.map +1 -1
- package/dist/sequencer/events.d.ts +6 -6
- package/dist/sequencer/events.d.ts.map +1 -1
- package/dist/sequencer/sequencer.d.ts.map +1 -1
- package/dist/util/debug.d.ts.map +1 -1
- package/dist/util/params.d.ts.map +1 -1
- package/dist/well-known.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/account-manager/db/migrations/005-oauth-account-management.ts +112 -0
- package/src/account-manager/db/migrations/index.ts +2 -0
- package/src/account-manager/db/schema/account-device.ts +14 -0
- package/src/account-manager/db/schema/authorization-request.ts +5 -3
- package/src/account-manager/db/schema/authorized-client.ts +19 -0
- package/src/account-manager/db/schema/index.ts +5 -3
- package/src/account-manager/db/schema/token.ts +7 -4
- package/src/account-manager/helpers/account-device.ts +66 -0
- package/src/account-manager/helpers/authorization-request.ts +5 -5
- package/src/account-manager/helpers/authorized-client.ts +69 -0
- package/src/account-manager/helpers/device.ts +3 -1
- package/src/account-manager/helpers/token.ts +19 -57
- package/src/account-manager/oauth-store.ts +182 -103
- package/src/auth-routes.ts +11 -7
- package/src/auth-verifier.ts +2 -7
- package/src/config/config.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/context.ts +2 -2
- package/src/db/cast.ts +43 -50
- package/tests/db.test.ts +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
- package/dist/account-manager/db/schema/device-account.d.ts +0 -14
- package/dist/account-manager/db/schema/device-account.d.ts.map +0 -1
- package/dist/account-manager/db/schema/device-account.js.map +0 -1
- package/dist/account-manager/helpers/device-account.d.ts +0 -108
- package/dist/account-manager/helpers/device-account.d.ts.map +0 -1
- package/dist/account-manager/helpers/device-account.js +0 -83
- package/dist/account-manager/helpers/device-account.js.map +0 -1
- package/src/account-manager/db/schema/device-account.ts +0 -15
- 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
|
-
|
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
|
43
|
-
import * as
|
44
|
-
import
|
45
|
-
import * as
|
46
|
-
import * as
|
47
|
-
import * as
|
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
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
265
|
-
|
266
|
-
.readQB(dbTxn, deviceId, sub)
|
267
|
-
.executeTakeFirstOrThrow()
|
238
|
+
await authorizedClientHelper.upsert(this.db, sub, clientId, data)
|
239
|
+
}
|
268
240
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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<
|
284
|
-
const row = await
|
285
|
-
.
|
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
|
-
|
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
|
297
|
-
|
298
|
-
.
|
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
|
310
|
-
|
311
|
-
|
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(
|
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
|
406
|
+
const row = await authRequestHelper.readQB(this.db, id).executeTakeFirst()
|
392
407
|
if (!row) return null
|
393
|
-
return
|
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(
|
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(
|
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(
|
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
|
414
|
-
|
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(
|
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
|
425
|
-
return row ?
|
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(
|
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(
|
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
|
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
|
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
|
464
|
-
|
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(
|
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
|
511
|
+
const { id, currentRefreshToken } = await tokenHelper
|
480
512
|
.forRotateQB(dbTxn, tokenId)
|
481
513
|
.executeTakeFirstOrThrow()
|
482
514
|
|
483
515
|
if (currentRefreshToken) {
|
484
|
-
await
|
516
|
+
await usedRefreshTokenHelper
|
485
517
|
.insertQB(dbTxn, id, currentRefreshToken)
|
486
518
|
.execute()
|
487
519
|
}
|
488
520
|
|
489
|
-
const { count } = await
|
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
|
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
|
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
|
518
|
-
return row ?
|
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
|
523
|
-
return row ?
|
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
|
}
|
package/src/auth-routes.ts
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
import { Router } from 'express'
|
2
|
-
import {
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
package/src/auth-verifier.ts
CHANGED
@@ -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
|
}
|
package/src/config/config.ts
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
3
|
-
|
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
|
-
|
6
|
-
export
|
7
|
-
|
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
|
43
|
+
return json as JsonEncoded<T>
|
10
44
|
}
|
11
|
-
|
45
|
+
|
46
|
+
export function fromJson<T extends Encodable>(jsonStr: JsonEncoded<T>): T {
|
12
47
|
try {
|
13
|
-
return JSON.parse(
|
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
|
-
|
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
|