@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
@@ -0,0 +1,112 @@
1
+ import { Kysely } from 'kysely'
2
+ import { HOUR } from '@atproto/common'
3
+ import { ClientId, DeviceId } from '@atproto/oauth-provider'
4
+ import { DateISO, JsonEncoded, toDateISO } from '../../../db'
5
+
6
+ export async function up(
7
+ db: Kysely<{
8
+ device_account: {
9
+ did: string
10
+ deviceId: DeviceId
11
+
12
+ remember: 0 | 1
13
+ authenticatedAt: string
14
+ authorizedClients: JsonEncoded<ClientId[]>
15
+ }
16
+ account_device: {
17
+ did: string
18
+ deviceId: DeviceId
19
+
20
+ createdAt: DateISO
21
+ updatedAt: DateISO
22
+ }
23
+ }>,
24
+ ): Promise<void> {
25
+ // Security: Delete any leftover device accounts that are not remembered
26
+ await db
27
+ .deleteFrom('device_account')
28
+ .where('remember', '=', 0)
29
+ .where('authenticatedAt', '<', toDateISO(new Date(Date.now() - HOUR)))
30
+ .execute()
31
+
32
+ // replaces "device_account"
33
+ await db.schema
34
+ .createTable('account_device')
35
+ .addColumn('did', 'varchar', (col) => col.notNull())
36
+ .addColumn('deviceId', 'varchar', (col) => col.notNull())
37
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
38
+ .addColumn('updatedAt', 'varchar', (col) => col.notNull())
39
+ .addPrimaryKeyConstraint('account_device_pk', [
40
+ 'deviceId', // first because this table will be joined from the "device" table
41
+ 'did',
42
+ ])
43
+ .addForeignKeyConstraint(
44
+ 'account_device_did_fk',
45
+ ['did'],
46
+ 'account',
47
+ ['did'],
48
+ // cascade on delete, future-proofing on update (fk can't be altered)
49
+ (qb) => qb.onDelete('cascade').onUpdate('cascade'),
50
+ )
51
+ .addForeignKeyConstraint(
52
+ 'account_device_device_id_fk',
53
+ ['deviceId'],
54
+ 'device',
55
+ ['id'],
56
+ // cascade on delete, future-proofing on update (fk can't be altered)
57
+ (qb) => qb.onDelete('cascade').onUpdate('cascade'),
58
+ )
59
+ .execute()
60
+
61
+ // Migrate "device_account" to "account_device"
62
+ await db
63
+ .insertInto('account_device')
64
+ .columns(['did', 'deviceId', 'createdAt', 'updatedAt'])
65
+ .expression(
66
+ db
67
+ .selectFrom('device_account')
68
+ .select('did')
69
+ .select('deviceId')
70
+ .select('authenticatedAt as createdAt') // Best we can do
71
+ .select('authenticatedAt as updatedAt')
72
+ .where('remember', '=', 1),
73
+ )
74
+ .onConflict((oc) => oc.doNothing())
75
+ .execute()
76
+
77
+ // @NOTE No need to create an index on "deviceId" for "account_device" because
78
+ // it is the first column in the primary key constraint
79
+
80
+ await db.schema
81
+ .createIndex('account_device_did_idx')
82
+ .on('account_device')
83
+ .column('did')
84
+ .execute()
85
+
86
+ await db.schema
87
+ .createTable('authorized_client')
88
+ .addColumn('did', 'varchar', (col) => col.notNull())
89
+ .addColumn('clientId', 'varchar', (col) => col.notNull())
90
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
91
+ .addColumn('updatedAt', 'varchar', (col) => col.notNull())
92
+ .addColumn('data', 'varchar', (col) => col.notNull())
93
+ .addPrimaryKeyConstraint('authorized_client_pk', ['did', 'clientId'])
94
+ .addForeignKeyConstraint(
95
+ 'authorized_client_did_fk',
96
+ ['did'],
97
+ 'account',
98
+ ['did'],
99
+ // cascade on delete, future-proofing on update (fk can't be altered)
100
+ (qb) => qb.onDelete('cascade').onUpdate('cascade'),
101
+ )
102
+ .execute()
103
+
104
+ // We don't migrate the "device_account" authorized clients. Users will need
105
+ // to reauthorize the client during the next oauth flow (minor inconvenience
106
+ // for authenticated clients users).
107
+ }
108
+
109
+ export async function down(db: Kysely<unknown>): Promise<void> {
110
+ await db.schema.dropTable('authorized_client').execute()
111
+ await db.schema.dropTable('account_device').execute()
112
+ }
@@ -2,10 +2,12 @@ import * as mig001 from './001-init'
2
2
  import * as mig002 from './002-account-deactivation'
3
3
  import * as mig003 from './003-privileged-app-passwords'
4
4
  import * as mig004 from './004-oauth'
5
+ import * as mig005 from './005-oauth-account-management'
5
6
 
6
7
  export default {
7
8
  '001': mig001,
8
9
  '002': mig002,
9
10
  '003': mig003,
10
11
  '004': mig004,
12
+ '005': mig005,
11
13
  }
@@ -0,0 +1,14 @@
1
+ import { DeviceId } from '@atproto/oauth-provider'
2
+ import { DateISO } from '../../../db'
3
+
4
+ export interface AccountDevice {
5
+ did: string
6
+ deviceId: DeviceId
7
+
8
+ createdAt: DateISO
9
+ updatedAt: DateISO
10
+ }
11
+
12
+ export const tableName = 'account_device'
13
+
14
+ export type PartialDB = { [tableName]: AccountDevice }
@@ -1,11 +1,13 @@
1
1
  import { Selectable } from 'kysely'
2
2
  import {
3
+ ClientAuth,
3
4
  Code,
4
5
  DeviceId,
6
+ OAuthAuthorizationRequestParameters,
5
7
  OAuthClientId,
6
8
  RequestId,
7
9
  } from '@atproto/oauth-provider'
8
- import { DateISO, JsonObject } from '../../../db'
10
+ import { DateISO, JsonEncoded } from '../../../db'
9
11
 
10
12
  export interface AuthorizationRequest {
11
13
  id: RequestId
@@ -13,8 +15,8 @@ export interface AuthorizationRequest {
13
15
  deviceId: DeviceId | null
14
16
 
15
17
  clientId: OAuthClientId
16
- clientAuth: JsonObject
17
- parameters: JsonObject
18
+ clientAuth: JsonEncoded<ClientAuth>
19
+ parameters: JsonEncoded<OAuthAuthorizationRequestParameters>
18
20
  expiresAt: DateISO
19
21
  code: Code | null
20
22
  }
@@ -0,0 +1,19 @@
1
+ import { Selectable } from 'kysely'
2
+ import { AuthorizedClientData, OAuthClientId } from '@atproto/oauth-provider'
3
+ import { DateISO, JsonEncoded } from '../../../db'
4
+
5
+ export interface AuthorizedClient {
6
+ did: string
7
+ clientId: OAuthClientId
8
+
9
+ createdAt: DateISO
10
+ updatedAt: DateISO
11
+
12
+ data: JsonEncoded<AuthorizedClientData>
13
+ }
14
+
15
+ export type AuthorizedClientEntry = Selectable<AuthorizedClient>
16
+
17
+ export const tableName = 'authorized_client'
18
+
19
+ export type PartialDB = { [tableName]: AuthorizedClient }
@@ -1,9 +1,10 @@
1
1
  import * as account from './account'
2
+ import * as accountDevice from './account-device'
2
3
  import * as actor from './actor'
3
4
  import * as appPassword from './app-password'
4
5
  import * as oauthRequest from './authorization-request'
6
+ import * as authorizedClient from './authorized-client'
5
7
  import * as device from './device'
6
- import * as deviceAccount from './device-account'
7
8
  import * as emailToken from './email-token'
8
9
  import * as inviteCode from './invite-code'
9
10
  import * as refreshToken from './refresh-token'
@@ -13,8 +14,9 @@ import * as usedRefreshToken from './used-refresh-token'
13
14
 
14
15
  export type DatabaseSchema = actor.PartialDB &
15
16
  account.PartialDB &
17
+ accountDevice.PartialDB &
18
+ authorizedClient.PartialDB &
16
19
  device.PartialDB &
17
- deviceAccount.PartialDB &
18
20
  oauthRequest.PartialDB &
19
21
  token.PartialDB &
20
22
  usedRefreshToken.PartialDB &
@@ -26,8 +28,8 @@ export type DatabaseSchema = actor.PartialDB &
26
28
 
27
29
  export type { Actor, ActorEntry } from './actor'
28
30
  export type { Account, AccountEntry } from './account'
31
+ export type { AccountDevice } from './account-device'
29
32
  export type { Device } from './device'
30
- export type { DeviceAccount } from './device-account'
31
33
  export type { AuthorizationRequest } from './authorization-request'
32
34
  export type { Token } from './token'
33
35
  export type { UsedRefreshToken } from './used-refresh-token'
@@ -1,13 +1,16 @@
1
1
  import { Generated, Selectable } from 'kysely'
2
2
  import {
3
+ ClientAuth,
3
4
  Code,
4
5
  DeviceId,
6
+ OAuthAuthorizationDetails,
7
+ OAuthAuthorizationRequestParameters,
5
8
  OAuthClientId,
6
9
  RefreshToken,
7
10
  Sub,
8
11
  TokenId,
9
12
  } from '@atproto/oauth-provider'
10
- import { DateISO, JsonArray, JsonObject } from '../../../db/cast'
13
+ import { DateISO, JsonEncoded } from '../../../db/cast'
11
14
 
12
15
  export interface Token {
13
16
  id: Generated<number>
@@ -18,10 +21,10 @@ export interface Token {
18
21
  updatedAt: DateISO
19
22
  expiresAt: DateISO
20
23
  clientId: OAuthClientId
21
- clientAuth: JsonObject
24
+ clientAuth: JsonEncoded<ClientAuth>
22
25
  deviceId: DeviceId | null
23
- parameters: JsonObject
24
- details: JsonArray | null
26
+ parameters: JsonEncoded<OAuthAuthorizationRequestParameters>
27
+ details: JsonEncoded<OAuthAuthorizationDetails> | null
25
28
  code: Code | null
26
29
  currentRefreshToken: RefreshToken | null
27
30
  }
@@ -0,0 +1,66 @@
1
+ import assert from 'node:assert'
2
+ import { DeviceId } from '@atproto/oauth-provider'
3
+ import { toDateISO } from '../../db'
4
+ import { AccountDb } from '../db'
5
+ import { selectAccountQB } from './account'
6
+
7
+ export function upsertQB(db: AccountDb, deviceId: DeviceId, did: string) {
8
+ const now = new Date()
9
+
10
+ return db.db
11
+ .insertInto('account_device')
12
+ .values({
13
+ did,
14
+ deviceId,
15
+ createdAt: toDateISO(now),
16
+ updatedAt: toDateISO(now),
17
+ })
18
+ .onConflict((oc) =>
19
+ // uses pk
20
+ oc.columns(['deviceId', 'did']).doUpdateSet({
21
+ updatedAt: toDateISO(now),
22
+ }),
23
+ )
24
+ }
25
+
26
+ export function selectQB(
27
+ db: AccountDb,
28
+ filter: {
29
+ sub?: string
30
+ deviceId?: DeviceId
31
+ },
32
+ ) {
33
+ assert(
34
+ filter.sub != null || filter.deviceId != null,
35
+ 'Either sub or deviceId must be provided',
36
+ )
37
+
38
+ return (
39
+ selectAccountQB(db, { includeDeactivated: true })
40
+ // note: query planner should use "account_device_pk" index
41
+ .innerJoin('account_device', 'account_device.did', 'actor.did')
42
+ .select([
43
+ 'account_device.deviceId',
44
+ 'account_device.createdAt as adCreatedAt',
45
+ 'account_device.updatedAt as adUpdatedAt',
46
+ ])
47
+ .innerJoin('device', 'device.id', 'account_device.deviceId')
48
+ .select([
49
+ 'device.sessionId',
50
+ 'device.userAgent',
51
+ 'device.ipAddress',
52
+ 'device.lastSeenAt',
53
+ ])
54
+ .if(filter.sub != null, (qb) => qb.where('actor.did', '=', filter.sub!))
55
+ .if(filter.deviceId != null, (qb) =>
56
+ qb.where('account_device.deviceId', '=', filter.deviceId!),
57
+ )
58
+ )
59
+ }
60
+
61
+ export function removeQB(db: AccountDb, deviceId: DeviceId, did: string) {
62
+ return db.db
63
+ .deleteFrom('account_device')
64
+ .where('deviceId', '=', deviceId)
65
+ .where('did', '=', did)
66
+ }
@@ -6,15 +6,15 @@ import {
6
6
  RequestId,
7
7
  UpdateRequestData,
8
8
  } from '@atproto/oauth-provider'
9
- import { fromDateISO, fromJsonObject, toDateISO, toJsonObject } from '../../db'
9
+ import { fromDateISO, fromJson, toDateISO, toJson } from '../../db'
10
10
  import { AccountDb, AuthorizationRequest } from '../db'
11
11
 
12
12
  export const rowToRequestData = (
13
13
  row: Selectable<AuthorizationRequest>,
14
14
  ): RequestData => ({
15
15
  clientId: row.clientId,
16
- clientAuth: fromJsonObject(row.clientAuth),
17
- parameters: fromJsonObject(row.parameters),
16
+ clientAuth: fromJson(row.clientAuth),
17
+ parameters: fromJson(row.parameters),
18
18
  expiresAt: fromDateISO(row.expiresAt),
19
19
  deviceId: row.deviceId,
20
20
  sub: row.did,
@@ -37,8 +37,8 @@ const requestDataToRow = (
37
37
  deviceId: data.deviceId,
38
38
 
39
39
  clientId: data.clientId,
40
- clientAuth: toJsonObject(data.clientAuth),
41
- parameters: toJsonObject(data.parameters),
40
+ clientAuth: toJson(data.clientAuth),
41
+ parameters: toJson(data.parameters),
42
42
  expiresAt: toDateISO(data.expiresAt),
43
43
  code: data.code,
44
44
  })
@@ -0,0 +1,69 @@
1
+ import {
2
+ AuthorizedClientData,
3
+ AuthorizedClients,
4
+ ClientId,
5
+ Sub,
6
+ } from '@atproto/oauth-provider'
7
+ import { fromJson, toDateISO, toJson } from '../../db'
8
+ import { AccountDb } from '../db'
9
+
10
+ export async function upsert(
11
+ db: AccountDb,
12
+ did: string,
13
+ clientId: ClientId,
14
+ data: AuthorizedClientData,
15
+ ) {
16
+ const now = new Date()
17
+
18
+ return db.db
19
+ .insertInto('authorized_client')
20
+ .values({
21
+ did,
22
+ clientId,
23
+ createdAt: toDateISO(now),
24
+ updatedAt: toDateISO(now),
25
+ data: toJson(data),
26
+ })
27
+ .onConflict((oc) =>
28
+ // uses "authorized_client_pk" idx
29
+ oc.columns(['did', 'clientId']).doUpdateSet({
30
+ updatedAt: toDateISO(now),
31
+ data: toJson(data),
32
+ }),
33
+ )
34
+ .executeTakeFirst()
35
+ }
36
+
37
+ export async function getAuthorizedClients(
38
+ db: AccountDb,
39
+ did: string,
40
+ ): Promise<AuthorizedClients> {
41
+ return (await getAuthorizedClientsMulti(db, [did])).get(did)!
42
+ }
43
+
44
+ export async function getAuthorizedClientsMulti(
45
+ db: AccountDb,
46
+ dids: Iterable<string>,
47
+ ): Promise<Map<Sub, AuthorizedClients>> {
48
+ // Using a Map will ensure unicity of dids (through unicity of keys)
49
+ const map = new Map<Sub, AuthorizedClients>(
50
+ Array.from(dids, (did) => [did, new Map()]),
51
+ )
52
+
53
+ if (map.size) {
54
+ const found = await db.db
55
+ .selectFrom('authorized_client')
56
+ .select('did')
57
+ .select('clientId')
58
+ .select('data')
59
+ // uses "authorized_client_pk"
60
+ .where('did', 'in', [...map.keys()])
61
+ .execute()
62
+
63
+ for (const { did, clientId, data } of found) {
64
+ map.get(did)!.set(clientId, fromJson(data))
65
+ }
66
+ }
67
+
68
+ return map
69
+ }
@@ -3,7 +3,9 @@ import { DeviceData, DeviceId } from '@atproto/oauth-provider'
3
3
  import { fromDateISO, toDateISO } from '../../db'
4
4
  import { AccountDb, Device } from '../db'
5
5
 
6
- export const rowToDeviceData = (row: Selectable<Device>): DeviceData => ({
6
+ export const rowToDeviceData = (
7
+ row: Omit<Selectable<Device>, 'id'>,
8
+ ): DeviceData => ({
7
9
  sessionId: row.sessionId,
8
10
  userAgent: row.userAgent,
9
11
  ipAddress: row.ipAddress,
@@ -2,75 +2,34 @@ import { Selectable } from 'kysely'
2
2
  import {
3
3
  Code,
4
4
  NewTokenData,
5
- OAuthAuthorizationDetail,
6
5
  RefreshToken,
7
6
  TokenData,
8
7
  TokenId,
9
- TokenInfo,
10
8
  } from '@atproto/oauth-provider'
11
- import {
12
- fromDateISO,
13
- fromJsonArray,
14
- fromJsonObject,
15
- toDateISO,
16
- toJsonArray,
17
- toJsonObject,
18
- } from '../../db'
9
+ import { fromDateISO, fromJson, toDateISO, toJson } from '../../db'
19
10
  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] }
11
+ import { selectAccountQB } from './account'
28
12
 
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: {
13
+ export function toTokenData(row: Selectable<Token>): TokenData {
14
+ return {
39
15
  createdAt: fromDateISO(row.createdAt),
40
16
  expiresAt: fromDateISO(row.expiresAt),
41
17
  updatedAt: fromDateISO(row.updatedAt),
42
18
  clientId: row.clientId,
43
- clientAuth: fromJsonObject(row.clientAuth),
19
+ clientAuth: fromJson(row.clientAuth),
44
20
  deviceId: row.deviceId,
45
21
  sub: row.did,
46
- parameters: fromJsonObject(row.parameters),
47
- details: row.details
48
- ? fromJsonArray<OAuthAuthorizationDetail>(row.details)
49
- : null,
22
+ parameters: fromJson(row.parameters),
50
23
  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
- })
24
+ }
25
+ }
61
26
 
62
27
  const selectTokenInfoQB = (db: AccountDb) =>
63
28
  selectAccountQB(db, { includeDeactivated: true })
64
29
  // uses "token_did_idx" index (though unlikely in practice)
65
30
  .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
31
  .select([
32
+ 'token.id',
74
33
  'token.tokenId',
75
34
  'token.createdAt',
76
35
  'token.updatedAt',
@@ -83,9 +42,6 @@ const selectTokenInfoQB = (db: AccountDb) =>
83
42
  'token.details',
84
43
  'token.code',
85
44
  'token.currentRefreshToken',
86
- 'device_account.authenticatedAt',
87
- 'device_account.authorizedClients',
88
- 'device_account.remember',
89
45
  ])
90
46
 
91
47
  export const createQB = (
@@ -100,11 +56,11 @@ export const createQB = (
100
56
  expiresAt: toDateISO(data.expiresAt),
101
57
  updatedAt: toDateISO(data.updatedAt),
102
58
  clientId: data.clientId,
103
- clientAuth: toJsonObject(data.clientAuth),
59
+ clientAuth: toJson(data.clientAuth),
104
60
  deviceId: data.deviceId,
105
61
  did: data.sub,
106
- parameters: toJsonObject(data.parameters),
107
- details: data.details ? toJsonArray(data.details) : null,
62
+ parameters: toJson(data.parameters),
63
+ details: data.details ? toJson(data.details) : null,
108
64
  code: data.code,
109
65
  currentRefreshToken: refreshToken || null,
110
66
  })
@@ -120,6 +76,7 @@ export const findByQB = (
120
76
  db: AccountDb,
121
77
  search: {
122
78
  id?: number
79
+ did?: string
123
80
  code?: Code
124
81
  tokenId?: TokenId
125
82
  currentRefreshToken?: RefreshToken
@@ -127,6 +84,7 @@ export const findByQB = (
127
84
  ) => {
128
85
  if (
129
86
  search.id === undefined &&
87
+ search.did === undefined &&
130
88
  search.code === undefined &&
131
89
  search.tokenId === undefined &&
132
90
  search.currentRefreshToken === undefined
@@ -140,6 +98,10 @@ export const findByQB = (
140
98
  // uses primary key index
141
99
  qb.where('token.id', '=', search.id!),
142
100
  )
101
+ .if(search.did !== undefined, (qb) =>
102
+ // uses "token_did_idx" index
103
+ qb.where('token.did', '=', search.did!),
104
+ )
143
105
  .if(search.code !== undefined, (qb) =>
144
106
  // uses "token_code_idx" partial index (hence the null check)
145
107
  qb
@@ -175,7 +137,7 @@ export const rotateQB = (
175
137
 
176
138
  expiresAt: toDateISO(newData.expiresAt),
177
139
  updatedAt: toDateISO(newData.updatedAt),
178
- clientAuth: toJsonObject(newData.clientAuth),
140
+ clientAuth: toJson(newData.clientAuth),
179
141
  })
180
142
  // uses primary key index
181
143
  .where('id', '=', id)