@atproto/pds 0.5.5 → 0.5.7

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 (256) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/account-manager/account-manager.d.ts +51 -13
  3. package/dist/account-manager/account-manager.d.ts.map +1 -1
  4. package/dist/account-manager/account-manager.js +86 -26
  5. package/dist/account-manager/account-manager.js.map +1 -1
  6. package/dist/account-manager/db/migrations/005-oauth-account-management.d.ts.map +1 -1
  7. package/dist/account-manager/db/migrations/005-oauth-account-management.js +3 -4
  8. package/dist/account-manager/db/migrations/005-oauth-account-management.js.map +1 -1
  9. package/dist/account-manager/db/schema/authorization-request.d.ts +2 -2
  10. package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -1
  11. package/dist/account-manager/db/schema/authorization-request.js.map +1 -1
  12. package/dist/account-manager/db/schema/authorized-client.d.ts +2 -2
  13. package/dist/account-manager/db/schema/authorized-client.d.ts.map +1 -1
  14. package/dist/account-manager/db/schema/authorized-client.js.map +1 -1
  15. package/dist/account-manager/db/schema/token.d.ts +2 -2
  16. package/dist/account-manager/db/schema/token.d.ts.map +1 -1
  17. package/dist/account-manager/db/schema/token.js.map +1 -1
  18. package/dist/account-manager/helpers/account-device.d.ts +23 -196
  19. package/dist/account-manager/helpers/account-device.d.ts.map +1 -1
  20. package/dist/account-manager/helpers/account-device.js +3 -3
  21. package/dist/account-manager/helpers/account-device.js.map +1 -1
  22. package/dist/account-manager/helpers/account.d.ts +14 -4
  23. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  24. package/dist/account-manager/helpers/account.js +17 -11
  25. package/dist/account-manager/helpers/account.js.map +1 -1
  26. package/dist/account-manager/helpers/auth.js +1 -1
  27. package/dist/account-manager/helpers/auth.js.map +1 -1
  28. package/dist/account-manager/helpers/authorization-request.d.ts +83 -5
  29. package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -1
  30. package/dist/account-manager/helpers/authorization-request.js +8 -8
  31. package/dist/account-manager/helpers/authorization-request.js.map +1 -1
  32. package/dist/account-manager/helpers/authorized-client.d.ts +5 -4
  33. package/dist/account-manager/helpers/authorized-client.d.ts.map +1 -1
  34. package/dist/account-manager/helpers/authorized-client.js +3 -0
  35. package/dist/account-manager/helpers/authorized-client.js.map +1 -1
  36. package/dist/account-manager/helpers/device.d.ts +9 -3
  37. package/dist/account-manager/helpers/device.d.ts.map +1 -1
  38. package/dist/account-manager/helpers/device.js +4 -4
  39. package/dist/account-manager/helpers/device.js.map +1 -1
  40. package/dist/account-manager/helpers/invite.d.ts +35 -2
  41. package/dist/account-manager/helpers/invite.d.ts.map +1 -1
  42. package/dist/account-manager/helpers/lexicon.d.ts.map +1 -1
  43. package/dist/account-manager/helpers/lexicon.js +6 -0
  44. package/dist/account-manager/helpers/lexicon.js.map +1 -1
  45. package/dist/account-manager/helpers/password.d.ts +1 -0
  46. package/dist/account-manager/helpers/password.d.ts.map +1 -1
  47. package/dist/account-manager/helpers/password.js +3 -0
  48. package/dist/account-manager/helpers/password.js.map +1 -1
  49. package/dist/account-manager/helpers/token.d.ts +72 -1031
  50. package/dist/account-manager/helpers/token.d.ts.map +1 -1
  51. package/dist/account-manager/helpers/token.js +13 -10
  52. package/dist/account-manager/helpers/token.js.map +1 -1
  53. package/dist/account-manager/helpers/used-refresh-token.d.ts +6 -2
  54. package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -1
  55. package/dist/account-manager/oauth-store.d.ts +17 -13
  56. package/dist/account-manager/oauth-store.d.ts.map +1 -1
  57. package/dist/account-manager/oauth-store.js +89 -39
  58. package/dist/account-manager/oauth-store.js.map +1 -1
  59. package/dist/actor-store/blob/reader.d.ts.map +1 -1
  60. package/dist/actor-store/blob/reader.js +2 -3
  61. package/dist/actor-store/blob/reader.js.map +1 -1
  62. package/dist/actor-store/record/reader.js +3 -3
  63. package/dist/actor-store/record/reader.js.map +1 -1
  64. package/dist/actor-store/repo/sql-repo-reader.d.ts +5 -1
  65. package/dist/actor-store/repo/sql-repo-reader.d.ts.map +1 -1
  66. package/dist/actor-store/repo/sql-repo-reader.js.map +1 -1
  67. package/dist/api/com/atproto/admin/updateSubjectStatus.d.ts.map +1 -1
  68. package/dist/api/com/atproto/admin/updateSubjectStatus.js +12 -4
  69. package/dist/api/com/atproto/admin/updateSubjectStatus.js.map +1 -1
  70. package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
  71. package/dist/api/com/atproto/server/activateAccount.js +25 -31
  72. package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
  73. package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
  74. package/dist/api/com/atproto/server/deactivateAccount.js +3 -4
  75. package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
  76. package/dist/api/com/atproto/server/deleteAccount.d.ts.map +1 -1
  77. package/dist/api/com/atproto/server/deleteAccount.js +51 -37
  78. package/dist/api/com/atproto/server/deleteAccount.js.map +1 -1
  79. package/dist/api/com/atproto/server/util.d.ts +25 -6
  80. package/dist/api/com/atproto/server/util.d.ts.map +1 -1
  81. package/dist/api/com/atproto/server/util.js.map +1 -1
  82. package/dist/background.d.ts +12 -6
  83. package/dist/background.d.ts.map +1 -1
  84. package/dist/background.js +32 -13
  85. package/dist/background.js.map +1 -1
  86. package/dist/config/config.d.ts +2 -1
  87. package/dist/config/config.d.ts.map +1 -1
  88. package/dist/config/config.js +4 -1
  89. package/dist/config/config.js.map +1 -1
  90. package/dist/context.d.ts.map +1 -1
  91. package/dist/context.js +5 -32
  92. package/dist/context.js.map +1 -1
  93. package/dist/db/migrator.d.ts +4 -3
  94. package/dist/db/migrator.d.ts.map +1 -1
  95. package/dist/db/migrator.js +1 -1
  96. package/dist/db/migrator.js.map +1 -1
  97. package/dist/db/pagination.d.ts +2 -1
  98. package/dist/db/pagination.d.ts.map +1 -1
  99. package/dist/db/pagination.js +2 -2
  100. package/dist/db/pagination.js.map +1 -1
  101. package/dist/db/util.d.ts +3 -3
  102. package/dist/db/util.d.ts.map +1 -1
  103. package/dist/db/util.js.map +1 -1
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +33 -11
  107. package/dist/index.js.map +1 -1
  108. package/dist/lexicons/app/bsky/notification/defs.defs.d.ts +5 -0
  109. package/dist/lexicons/app/bsky/notification/defs.defs.d.ts.map +1 -1
  110. package/dist/lexicons/app/bsky/notification/defs.defs.js +1 -0
  111. package/dist/lexicons/app/bsky/notification/defs.defs.js.map +1 -1
  112. package/dist/lexicons/chat/bsky/notification/defs.d.ts +2 -0
  113. package/dist/lexicons/chat/bsky/notification/defs.d.ts.map +1 -0
  114. package/dist/lexicons/chat/bsky/notification/defs.defs.d.ts +20 -0
  115. package/dist/lexicons/chat/bsky/notification/defs.defs.d.ts.map +1 -0
  116. package/dist/lexicons/chat/bsky/notification/defs.defs.js +19 -0
  117. package/dist/lexicons/chat/bsky/notification/defs.defs.js.map +1 -0
  118. package/dist/lexicons/chat/bsky/notification/defs.js +5 -0
  119. package/dist/lexicons/chat/bsky/notification/defs.js.map +1 -0
  120. package/dist/lexicons/chat/bsky/notification/getPreferences.d.ts +3 -0
  121. package/dist/lexicons/chat/bsky/notification/getPreferences.d.ts.map +1 -0
  122. package/dist/lexicons/chat/bsky/notification/getPreferences.defs.d.ts +18 -0
  123. package/dist/lexicons/chat/bsky/notification/getPreferences.defs.d.ts.map +1 -0
  124. package/dist/lexicons/chat/bsky/notification/getPreferences.defs.js +16 -0
  125. package/dist/lexicons/chat/bsky/notification/getPreferences.defs.js.map +1 -0
  126. package/dist/lexicons/chat/bsky/notification/getPreferences.js +6 -0
  127. package/dist/lexicons/chat/bsky/notification/getPreferences.js.map +1 -0
  128. package/dist/lexicons/chat/bsky/notification/putPreferences.d.ts +3 -0
  129. package/dist/lexicons/chat/bsky/notification/putPreferences.d.ts.map +1 -0
  130. package/dist/lexicons/chat/bsky/notification/putPreferences.defs.d.ts +27 -0
  131. package/dist/lexicons/chat/bsky/notification/putPreferences.defs.d.ts.map +1 -0
  132. package/dist/lexicons/chat/bsky/notification/putPreferences.defs.js +22 -0
  133. package/dist/lexicons/chat/bsky/notification/putPreferences.defs.js.map +1 -0
  134. package/dist/lexicons/chat/bsky/notification/putPreferences.js +6 -0
  135. package/dist/lexicons/chat/bsky/notification/putPreferences.js.map +1 -0
  136. package/dist/lexicons/chat/bsky/notification.d.ts +4 -0
  137. package/dist/lexicons/chat/bsky/notification.d.ts.map +1 -0
  138. package/dist/lexicons/chat/bsky/notification.js +7 -0
  139. package/dist/lexicons/chat/bsky/notification.js.map +1 -0
  140. package/dist/lexicons/chat/bsky.d.ts +1 -0
  141. package/dist/lexicons/chat/bsky.d.ts.map +1 -1
  142. package/dist/lexicons/chat/bsky.js +1 -0
  143. package/dist/lexicons/chat/bsky.js.map +1 -1
  144. package/dist/lexicons/tools/ozone/report/defs.defs.d.ts +4 -0
  145. package/dist/lexicons/tools/ozone/report/defs.defs.d.ts.map +1 -1
  146. package/dist/lexicons/tools/ozone/report/defs.defs.js +2 -0
  147. package/dist/lexicons/tools/ozone/report/defs.defs.js.map +1 -1
  148. package/dist/lexicons/tools/ozone/report/queryActivities.d.ts +3 -0
  149. package/dist/lexicons/tools/ozone/report/queryActivities.d.ts.map +1 -0
  150. package/dist/lexicons/tools/ozone/report/queryActivities.defs.d.ts +42 -0
  151. package/dist/lexicons/tools/ozone/report/queryActivities.defs.d.ts.map +1 -0
  152. package/dist/lexicons/tools/ozone/report/queryActivities.defs.js +31 -0
  153. package/dist/lexicons/tools/ozone/report/queryActivities.defs.js.map +1 -0
  154. package/dist/lexicons/tools/ozone/report/queryActivities.js +6 -0
  155. package/dist/lexicons/tools/ozone/report/queryActivities.js.map +1 -0
  156. package/dist/lexicons/tools/ozone/report.d.ts +1 -0
  157. package/dist/lexicons/tools/ozone/report.d.ts.map +1 -1
  158. package/dist/lexicons/tools/ozone/report.js +1 -0
  159. package/dist/lexicons/tools/ozone/report.js.map +1 -1
  160. package/dist/mailer/index.d.ts +2 -0
  161. package/dist/mailer/index.d.ts.map +1 -1
  162. package/dist/mailer/index.js +2 -0
  163. package/dist/mailer/index.js.map +1 -1
  164. package/dist/pipethrough.d.ts +3 -0
  165. package/dist/pipethrough.d.ts.map +1 -1
  166. package/dist/pipethrough.js +32 -0
  167. package/dist/pipethrough.js.map +1 -1
  168. package/dist/scripts/sequencer-recovery/recovery-db.js.map +1 -1
  169. package/dist/sequencer/events.d.ts.map +1 -1
  170. package/dist/sequencer/events.js +6 -13
  171. package/dist/sequencer/events.js.map +1 -1
  172. package/dist/sequencer/sequencer.d.ts +1 -1
  173. package/dist/sequencer/sequencer.d.ts.map +1 -1
  174. package/dist/sequencer/sequencer.js.map +1 -1
  175. package/package.json +14 -14
  176. package/src/account-manager/account-manager.ts +135 -36
  177. package/src/account-manager/db/migrations/005-oauth-account-management.ts +6 -5
  178. package/src/account-manager/db/schema/authorization-request.ts +2 -1
  179. package/src/account-manager/db/schema/authorized-client.ts +6 -2
  180. package/src/account-manager/db/schema/token.ts +2 -2
  181. package/src/account-manager/helpers/account-device.ts +5 -11
  182. package/src/account-manager/helpers/account.ts +39 -16
  183. package/src/account-manager/helpers/auth.ts +2 -2
  184. package/src/account-manager/helpers/authorization-request.ts +12 -8
  185. package/src/account-manager/helpers/authorized-client.ts +12 -6
  186. package/src/account-manager/helpers/device.ts +4 -4
  187. package/src/account-manager/helpers/lexicon.ts +8 -3
  188. package/src/account-manager/helpers/password.ts +6 -0
  189. package/src/account-manager/helpers/token.ts +17 -11
  190. package/src/account-manager/oauth-store.ts +129 -61
  191. package/src/actor-store/blob/reader.ts +8 -5
  192. package/src/actor-store/record/reader.ts +3 -3
  193. package/src/actor-store/repo/sql-repo-reader.ts +1 -1
  194. package/src/api/com/atproto/admin/updateSubjectStatus.ts +16 -4
  195. package/src/api/com/atproto/server/activateAccount.ts +27 -49
  196. package/src/api/com/atproto/server/deactivateAccount.ts +3 -7
  197. package/src/api/com/atproto/server/deleteAccount.ts +60 -43
  198. package/src/api/com/atproto/server/util.ts +24 -7
  199. package/src/background.ts +49 -18
  200. package/src/config/config.ts +7 -2
  201. package/src/context.ts +6 -40
  202. package/src/db/migrator.ts +2 -1
  203. package/src/db/pagination.ts +7 -7
  204. package/src/db/util.ts +5 -2
  205. package/src/index.ts +31 -13
  206. package/src/mailer/index.ts +4 -2
  207. package/src/pipethrough.ts +38 -1
  208. package/src/scripts/sequencer-recovery/recovery-db.ts +1 -1
  209. package/src/sequencer/events.ts +8 -13
  210. package/src/sequencer/sequencer.ts +1 -3
  211. package/tests/_util.ts +8 -25
  212. package/tests/account-deactivation.test.ts +1 -1
  213. package/tests/account-deletion.test.ts +1 -1
  214. package/tests/account-manager.test.ts +79 -0
  215. package/tests/account-migration.test.ts +2 -2
  216. package/tests/account-status.test.ts +206 -0
  217. package/tests/account.test.ts +1 -1
  218. package/tests/app-passwords.test.ts +1 -1
  219. package/tests/auth.test.ts +1 -1
  220. package/tests/blob-deletes.test.ts +1 -1
  221. package/tests/create-post.test.ts +1 -1
  222. package/tests/crud.test.ts +1 -1
  223. package/tests/db.test.ts +4 -4
  224. package/tests/email-confirmation.test.ts +1 -1
  225. package/tests/file-uploads.test.ts +2 -2
  226. package/tests/get-service-auth.test.ts +1 -1
  227. package/tests/handles.test.ts +1 -1
  228. package/tests/invite-codes.test.ts +1 -1
  229. package/tests/invites-admin.test.ts +1 -1
  230. package/tests/moderation.test.ts +1 -1
  231. package/tests/moderator-auth.test.ts +1 -1
  232. package/tests/oauth.test.ts +91 -0
  233. package/tests/plc-operations.test.ts +1 -1
  234. package/tests/preferences.test.ts +1 -1
  235. package/tests/proxied/admin.test.ts +1 -1
  236. package/tests/proxied/feedgen.test.ts +1 -1
  237. package/tests/proxied/notif.test.ts +6 -13
  238. package/tests/proxied/procedures.test.ts +1 -1
  239. package/tests/proxied/proxy-catchall.test.ts +9 -8
  240. package/tests/proxied/proxy-header.test.ts +12 -8
  241. package/tests/proxied/proxy-oauth-aud.test.ts +17 -10
  242. package/tests/proxied/read-after-write.test.ts +4 -5
  243. package/tests/proxied/views.test.ts +1 -1
  244. package/tests/races.test.ts +1 -1
  245. package/tests/rate-limits.test.ts +1 -1
  246. package/tests/recovery.test.ts +1 -1
  247. package/tests/seeds/basic.ts +2 -2
  248. package/tests/seeds/users.ts +4 -1
  249. package/tests/sequencer.test.ts +1 -1
  250. package/tests/server.test.ts +9 -12
  251. package/tests/sync/invertible-ops.test.ts +1 -1
  252. package/tests/sync/list.test.ts +1 -1
  253. package/tests/sync/subscribe-repos.test.ts +1 -1
  254. package/tests/sync/sync.test.ts +1 -1
  255. package/tests/takedown-appeal.test.ts +1 -1
  256. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import { MINUTE } from '@atproto/common'
2
2
  import {
3
3
  AuthRequiredError,
4
4
  InvalidRequestError,
5
+ MethodRateLimit,
5
6
  Server,
6
7
  } from '@atproto/xrpc-server'
7
8
  import { OLD_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt.js'
@@ -11,58 +12,74 @@ import { com } from '../../../../lexicons/index.js'
11
12
  export default function (server: Server, ctx: AppContext) {
12
13
  const { entrywayClient } = ctx
13
14
 
14
- server.add(com.atproto.server.deleteAccount, {
15
- rateLimit: {
16
- durationMs: 5 * MINUTE,
17
- points: 50,
18
- },
19
- handler: async ({ input: { body }, req }) => {
20
- const { did, password, token } = body
15
+ const rateLimit: MethodRateLimit = {
16
+ durationMs: 5 * MINUTE,
17
+ points: 50,
18
+ }
21
19
 
22
- const account = await ctx.accountManager.getAccount(did, {
23
- includeDeactivated: true,
24
- includeTakenDown: true,
25
- })
26
- if (!account) {
27
- throw new InvalidRequestError('account not found')
28
- }
20
+ if (entrywayClient) {
21
+ server.add(com.atproto.server.deleteAccount, {
22
+ rateLimit,
23
+ handler: async ({ input: { body }, req }) => {
24
+ const account = await ctx.accountManager.getAccount(body.did, {
25
+ includeDeactivated: true,
26
+ includeTakenDown: true,
27
+ })
28
+ if (!account) {
29
+ throw new InvalidRequestError('account not found')
30
+ }
29
31
 
30
- if (entrywayClient) {
31
32
  const { headers } = ctx.entrywayPassthruHeaders(req)
32
33
  await entrywayClient.xrpc(com.atproto.server.deleteAccount, {
33
34
  body,
34
35
  headers,
35
36
  })
36
- return
37
- }
37
+ },
38
+ })
39
+ } else {
40
+ server.add(com.atproto.server.deleteAccount, {
41
+ rateLimit,
42
+ handler: async ({ input: { body } }) => {
43
+ const { did, password, token } = body
44
+
45
+ if (password.length > OLD_PASSWORD_MAX_LENGTH) {
46
+ throw new AuthRequiredError(
47
+ 'Password too long. Consider resetting your password.',
48
+ )
49
+ }
38
50
 
39
- if (password.length > OLD_PASSWORD_MAX_LENGTH) {
40
- throw new InvalidRequestError('Invalid password length.')
41
- }
51
+ const account = await ctx.accountManager.getAccount(did, {
52
+ includeDeactivated: true,
53
+ includeTakenDown: true,
54
+ })
55
+ if (!account) {
56
+ throw new InvalidRequestError('account not found')
57
+ }
42
58
 
43
- const validPass = await ctx.accountManager.verifyAccountPassword(
44
- did,
45
- password,
46
- )
47
- if (!validPass) {
48
- throw new AuthRequiredError('Invalid did or password')
49
- }
59
+ const validPass = await ctx.accountManager.verifyAccountPassword(
60
+ did,
61
+ password,
62
+ )
63
+ if (!validPass) {
64
+ throw new AuthRequiredError('Invalid did or password')
65
+ }
50
66
 
51
- await ctx.accountManager.assertValidEmailToken(
52
- did,
53
- 'delete_account',
54
- token,
55
- )
67
+ await ctx.accountManager.assertValidEmailToken(
68
+ did,
69
+ 'delete_account',
70
+ token,
71
+ )
56
72
 
57
- // @NOTE Order matters here: first "unlink" the account by removing it
58
- // from the account manager database ("source of truth"), then notify the
59
- // sequencer, and finally cleanup files from the file system.
60
- await ctx.accountManager.deleteAccount(did)
61
- try {
62
- await ctx.sequencer.sequenceAccountDeletion(did)
63
- } finally {
64
- await ctx.actorStore.destroy(did)
65
- }
66
- },
67
- })
73
+ // @NOTE Order matters here: first "unlink" the account by removing it
74
+ // from the account manager database ("source of truth"), then notify the
75
+ // sequencer, and finally cleanup files from the file system.
76
+ await ctx.accountManager.deleteAccount(did)
77
+ try {
78
+ await ctx.sequencer.sequenceAccountDeletion(did)
79
+ } finally {
80
+ await ctx.actorStore.destroy(did)
81
+ }
82
+ },
83
+ })
84
+ }
68
85
  }
@@ -1,9 +1,10 @@
1
+ import * as plc from '@did-plc/lib'
1
2
  import { getPdsEndpoint, getSigningDidKey } from '@atproto/common'
2
3
  import * as crypto from '@atproto/crypto'
3
- import { DidDocument } from '@atproto/identity'
4
+ import { DidDocument, IdResolver } from '@atproto/identity'
4
5
  import { InvalidRequestError } from '@atproto/xrpc-server'
6
+ import { ActorStore } from '../../../../actor-store/actor-store.js'
5
7
  import { ServerConfig } from '../../../../config/index.js'
6
- import { AppContext } from '../../../../context.js'
7
8
  import { httpLogger } from '../../../../logger.js'
8
9
 
9
10
  // generate an invite code preceded by the hostname
@@ -29,7 +30,7 @@ export const getRandomToken = () => {
29
30
  }
30
31
 
31
32
  export const safeResolveDidDoc = async (
32
- ctx: AppContext,
33
+ ctx: { idResolver: IdResolver },
33
34
  did: string,
34
35
  forceRefresh?: boolean,
35
36
  ): Promise<DidDocument | undefined> => {
@@ -42,7 +43,7 @@ export const safeResolveDidDoc = async (
42
43
  }
43
44
 
44
45
  export const didDocForSession = async (
45
- ctx: AppContext,
46
+ ctx: { idResolver: IdResolver; cfg: ServerConfig },
46
47
  did: string,
47
48
  forceRefresh?: boolean,
48
49
  ): Promise<DidDocument | undefined> => {
@@ -51,7 +52,13 @@ export const didDocForSession = async (
51
52
  }
52
53
 
53
54
  export const isValidDidDocForService = async (
54
- ctx: AppContext,
55
+ ctx: {
56
+ plcClient: plc.Client
57
+ idResolver: IdResolver
58
+ cfg: ServerConfig
59
+ plcRotationKey: crypto.Keypair
60
+ actorStore: ActorStore
61
+ },
55
62
  did: string,
56
63
  ): Promise<boolean> => {
57
64
  try {
@@ -63,7 +70,13 @@ export const isValidDidDocForService = async (
63
70
  }
64
71
 
65
72
  export const assertValidDidDocumentForService = async (
66
- ctx: AppContext,
73
+ ctx: {
74
+ plcClient: plc.Client
75
+ idResolver: IdResolver
76
+ cfg: ServerConfig
77
+ plcRotationKey: crypto.Keypair
78
+ actorStore: ActorStore
79
+ },
67
80
  did: string,
68
81
  ) => {
69
82
  if (did.startsWith('did:plc')) {
@@ -86,7 +99,11 @@ export const assertValidDidDocumentForService = async (
86
99
  }
87
100
 
88
101
  const assertValidDocContents = async (
89
- ctx: AppContext,
102
+ ctx: {
103
+ cfg: ServerConfig
104
+ plcRotationKey: crypto.Keypair
105
+ actorStore: ActorStore
106
+ },
90
107
  did: string,
91
108
  contents: {
92
109
  signingKey?: string
package/src/background.ts CHANGED
@@ -1,34 +1,65 @@
1
1
  import PQueue from 'p-queue'
2
2
  import { dbLogger } from './logger.js'
3
-
4
3
  // A simple queue for in-process, out-of-band/backgrounded work
5
4
 
6
- export class BackgroundQueue {
7
- queue: InstanceType<typeof PQueue> = new PQueue({ concurrency: 5 })
8
- destroyed = false
9
- constructor() {}
5
+ type Task<TContext> = (ctx: TContext, signal: AbortSignal) => Promise<void>
10
6
 
11
- add(task: Task) {
12
- if (this.destroyed) {
13
- return
14
- }
15
- this.queue
16
- .add(() => task())
17
- .catch((err) => {
7
+ export type BackgroundQueueOptions = NonNullable<
8
+ ConstructorParameters<typeof PQueue>[0]
9
+ > & {
10
+ concurrency: number
11
+ }
12
+
13
+ // @NOTE Keep this in sync with the BackgroundQueue in
14
+ // - packages/bsky/src/data-plane/server/background.ts
15
+ // - packages/ozone/src/background.ts
16
+ // - packages/pds/src/background.ts
17
+ export class BackgroundQueue<TContext = unknown> {
18
+ private abortController = new AbortController()
19
+ private queue: PQueue
20
+
21
+ public get signal(): AbortSignal {
22
+ return this.abortController.signal
23
+ }
24
+
25
+ public get destroyed() {
26
+ return this.signal.aborted
27
+ }
28
+
29
+ constructor(
30
+ private readonly context: TContext,
31
+ options?: BackgroundQueueOptions,
32
+ ) {
33
+ this.queue = new PQueue(options)
34
+ }
35
+
36
+ add(task: Task<TContext>) {
37
+ if (this.destroyed) return
38
+
39
+ this.queue.add<void>(async () => {
40
+ try {
41
+ // The task will receive a signal allowing it to abort if the
42
+ // backgroundQueue is destroyed.
43
+ await task(this.context, this.signal)
44
+ } catch (err) {
18
45
  dbLogger.error({ err }, 'background queue task failed')
19
- })
46
+ }
47
+ })
20
48
  }
21
49
 
22
50
  async processAll() {
23
- await this.queue.onIdle()
51
+ const { queue } = this
52
+ while (queue.size || queue.pending) await queue.onIdle()
24
53
  }
25
54
 
26
55
  // On destroy we stop accepting new tasks, but complete all pending/in-progress tasks.
27
56
  // The application calls this only once http connections have drained (tasks no longer being added).
28
57
  async destroy() {
29
- this.destroyed = true
30
- await this.queue.onIdle()
58
+ if (this.destroyed) {
59
+ dbLogger.warn('BackgroundQueue.destroy() called multiple times')
60
+ }
61
+
62
+ this.abortController.abort()
63
+ return this.processAll()
31
64
  }
32
65
  }
33
-
34
- type Task = () => Promise<void>
@@ -5,7 +5,7 @@ import {
5
5
  BrandingInput as BrandingConfig,
6
6
  HcaptchaConfig,
7
7
  } from '@atproto/oauth-provider'
8
- import { ensureValidDid } from '@atproto/syntax'
8
+ import { DidString, ensureValidDid, isValidDid } from '@atproto/syntax'
9
9
  import { ServerEnvironment } from './env.js'
10
10
 
11
11
  export type { BrandingConfig }
@@ -21,6 +21,11 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
21
21
  ? `http://localhost:${port}`
22
22
  : `https://${hostname}`
23
23
  const did = env.serviceDid ?? `did:web:${hostname}`
24
+
25
+ if (!isValidDid(did)) {
26
+ throw new Error(`Invalid service DID: ${did}`)
27
+ }
28
+
24
29
  const serviceCfg: ServerConfig['service'] = {
25
30
  port,
26
31
  hostname,
@@ -400,7 +405,7 @@ export type ServiceConfig = {
400
405
  port: number
401
406
  hostname: string
402
407
  publicUrl: string
403
- did: string
408
+ did: DidString
404
409
  version?: string
405
410
  privacyPolicyUrl?: string
406
411
  termsOfServiceUrl?: string
package/src/context.ts CHANGED
@@ -21,12 +21,7 @@ import {
21
21
  createServiceAuthHeaders,
22
22
  createServiceJwt,
23
23
  } from '@atproto/xrpc-server'
24
- import {
25
- Fetch,
26
- isUnicastIp,
27
- safeFetchWrap,
28
- unicastLookup,
29
- } from '@atproto-labs/fetch-node'
24
+ import { Fetch, safeFetchWrap } from '@atproto-labs/fetch-node'
30
25
  import { AccountManager } from './account-manager/account-manager.js'
31
26
  import { OAuthStore } from './account-manager/oauth-store.js'
32
27
  import { ScopeReferenceGetter } from './account-manager/scope-reference-getter.js'
@@ -47,6 +42,7 @@ import { ImageUrlBuilder } from './image/image-url-builder.js'
47
42
  import { fetchLogger, lexiconResolverLogger, oauthLogger } from './logger.js'
48
43
  import { ServerMailer } from './mailer/index.js'
49
44
  import { ModerationMailer } from './mailer/moderation.js'
45
+ import { buildProxyAgent } from './pipethrough.js'
50
46
  import { LocalViewer, LocalViewerCreator } from './read-after-write/viewer.js'
51
47
  import { getRedisClient } from './redis.js'
52
48
  import { Sequencer } from './sequencer/index.js'
@@ -181,7 +177,7 @@ export class AppContext {
181
177
  })
182
178
  const plcClient = new plc.Client(cfg.identity.plcUrl)
183
179
 
184
- const backgroundQueue = new BackgroundQueue()
180
+ const backgroundQueue = new BackgroundQueue(undefined, { concurrency: 5 })
185
181
  const crawlers = new Crawlers(
186
182
  backgroundQueue,
187
183
  cfg.service.hostname,
@@ -277,15 +273,14 @@ export class AppContext {
277
273
  )
278
274
 
279
275
  const accountManager = new AccountManager(
276
+ cfg,
277
+ actorStore,
280
278
  idResolver,
281
279
  jwtSecretKey,
282
280
  mailer,
283
281
  sequencer,
284
282
  plcClient,
285
283
  plcRotationKey,
286
- cfg.service.did,
287
- cfg.identity.serviceHandleDomains,
288
- cfg.db,
289
284
  )
290
285
  await accountManager.migrateOrThrow()
291
286
 
@@ -296,36 +291,7 @@ export class AppContext {
296
291
  )
297
292
 
298
293
  // An agent for performing HTTP requests based on user provided URLs.
299
- const proxyAgentBase = new undici.Agent({
300
- allowH2: cfg.proxy.allowHTTP2, // This is experimental
301
- headersTimeout: cfg.proxy.headersTimeout,
302
- maxResponseSize: cfg.proxy.maxResponseSize,
303
- bodyTimeout: cfg.proxy.bodyTimeout,
304
- factory: cfg.proxy.disableSsrfProtection
305
- ? undefined
306
- : (origin, opts) => {
307
- const { protocol, hostname } =
308
- origin instanceof URL ? origin : new URL(origin)
309
- if (protocol !== 'https:') {
310
- throw new Error(`Forbidden protocol "${protocol}"`)
311
- }
312
- if (isUnicastIp(hostname) === false) {
313
- throw new Error('Hostname resolved to non-unicast address')
314
- }
315
- return new undici.Pool(origin, opts)
316
- },
317
- connect: {
318
- lookup: cfg.proxy.disableSsrfProtection ? undefined : unicastLookup,
319
- },
320
- })
321
- const proxyAgent =
322
- cfg.proxy.maxRetries > 0
323
- ? new undici.RetryAgent(proxyAgentBase, {
324
- statusCodes: [], // Only retry on socket errors
325
- methods: ['GET', 'HEAD'],
326
- maxRetries: cfg.proxy.maxRetries,
327
- })
328
- : proxyAgentBase
294
+ const proxyAgent = buildProxyAgent(cfg.proxy)
329
295
 
330
296
  /**
331
297
  * A fetch() function that protects against SSRF attacks, large responses &
@@ -1,4 +1,5 @@
1
- import { Kysely, Migration, Migrator as KyselyMigrator } from 'kysely'
1
+ import { Kysely } from 'kysely'
2
+ import { Migration, Migrator as KyselyMigrator } from 'kysely/migration'
2
3
 
3
4
  export class Migrator<T> extends KyselyMigrator {
4
5
  constructor(
@@ -1,4 +1,4 @@
1
- import { sql } from 'kysely'
1
+ import { SqlBool, sql } from 'kysely'
2
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
3
3
  import { AnyQb, DbRef } from './util.js'
4
4
 
@@ -66,16 +66,16 @@ export abstract class GenericKeyset<R, LR extends LabeledResult> {
66
66
  if (tryIndex) {
67
67
  // The tryIndex param will likely disappear and become the default implementation: here for now for gradual rollout query-by-query.
68
68
  if (direction === 'asc') {
69
- return sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))`
69
+ return sql<SqlBool>`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))`
70
70
  } else {
71
- return sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))`
71
+ return sql<SqlBool>`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))`
72
72
  }
73
73
  } else {
74
74
  // @NOTE this implementation can struggle to use an index on (primary, secondary) for pagination due to the "or" usage.
75
75
  if (direction === 'asc') {
76
- return sql`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))`
76
+ return sql<SqlBool>`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))`
77
77
  } else {
78
- return sql`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))`
78
+ return sql<SqlBool>`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))`
79
79
  }
80
80
  }
81
81
  }
@@ -125,8 +125,8 @@ export const paginate = <
125
125
  const { limit, cursor, keyset, direction = 'desc', tryIndex } = opts
126
126
  const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)
127
127
  return qb
128
- .if(!!limit, (q) => q.limit(limit as number))
128
+ .$if(!!limit, (q) => q.limit(limit as number))
129
129
  .orderBy(keyset.primary, direction)
130
130
  .orderBy(keyset.secondary, direction)
131
- .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
131
+ .$if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
132
132
  }
package/src/db/util.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  RawBuilder,
6
6
  ReferenceExpression,
7
7
  SelectQueryBuilder,
8
+ SqlBool,
8
9
  SqliteAdapter,
9
10
  SqliteIntrospector,
10
11
  SqliteQueryCompiler,
@@ -14,7 +15,7 @@ import { retry } from '@atproto/common'
14
15
 
15
16
  // Applies to repo_root or record table
16
17
  export const notSoftDeletedClause = (alias: DbRef) => {
17
- return sql`${alias}."takedownRef" is null`
18
+ return sql<SqlBool>`${alias}."takedownRef" is null`
18
19
  }
19
20
 
20
21
  export const softDeleted = (repoOrRecord: { takedownRef: string | null }) => {
@@ -92,7 +93,9 @@ const RETRY_ERRORS = new Set([
92
93
 
93
94
  export type Ref = ReferenceExpression<any, any>
94
95
 
95
- export type DbRef = RawBuilder | ReturnType<DynamicModule['ref']>
96
+ export type DbRef =
97
+ | RawBuilder<unknown>
98
+ | ReturnType<DynamicModule<unknown>['ref']>
96
99
 
97
100
  export type AnyQb = SelectQueryBuilder<any, any, any>
98
101
 
package/src/index.ts CHANGED
@@ -9,11 +9,8 @@ import http from 'node:http'
9
9
  import { PlcClientError } from '@did-plc/lib'
10
10
  import cors from 'cors'
11
11
  import express from 'express'
12
- // eslint-disable-next-line import/default, import/no-named-as-default-member
12
+ // eslint-disable-next-line import/default
13
13
  import httpTerminator from 'http-terminator'
14
- // eslint-disable-next-line import/no-named-as-default-member
15
- const { createHttpTerminator } = httpTerminator
16
- type HttpTerminator = ReturnType<typeof createHttpTerminator>
17
14
  import { DAY, SECOND } from '@atproto/common'
18
15
  import {
19
16
  MethodHandler,
@@ -64,7 +61,7 @@ export class PDS {
64
61
  public ctx: AppContext
65
62
  public app: express.Application
66
63
  public server?: http.Server
67
- private terminator?: HttpTerminator
64
+ private terminator?: httpTerminator.HttpTerminator
68
65
  private dbStatsInterval?: NodeJS.Timeout
69
66
  private sequencerStatsInterval?: NodeJS.Timeout
70
67
 
@@ -145,21 +142,42 @@ export class PDS {
145
142
  await this.ctx.sequencer.start()
146
143
  const server = this.app.listen(this.ctx.cfg.service.port)
147
144
  this.server = server
148
- this.server.keepAliveTimeout = 90000
149
- this.terminator = createHttpTerminator({ server })
145
+ this.server.keepAliveTimeout = 90_000
146
+ this.terminator = httpTerminator.createHttpTerminator({ server })
150
147
  await events.once(server, 'listening')
151
148
  return server
152
149
  }
153
150
 
154
151
  async destroy(): Promise<void> {
155
- await this.ctx.sequencer.destroy()
156
- await this.terminator?.terminate()
157
- await this.ctx.backgroundQueue.destroy()
158
- await this.ctx.accountManager.close()
159
- await this.ctx.redisScratch?.quit()
160
- await this.ctx.proxyAgent.destroy()
161
152
  clearInterval(this.dbStatsInterval)
162
153
  clearInterval(this.sequencerStatsInterval)
154
+
155
+ // @TODO Use disposable stack when it becomes available (Node24+)
156
+ try {
157
+ await this.terminator?.terminate()
158
+ } finally {
159
+ try {
160
+ await this.ctx.backgroundQueue.destroy()
161
+ } finally {
162
+ try {
163
+ await this.ctx.sequencer.destroy()
164
+ } finally {
165
+ try {
166
+ await this.ctx.accountManager.close()
167
+ } finally {
168
+ try {
169
+ await this.ctx.redisScratch?.quit()
170
+ } finally {
171
+ await this.ctx.proxyAgent.destroy()
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ async [Symbol.asyncDispose]() {
180
+ await this.destroy()
163
181
  }
164
182
  }
165
183
 
@@ -36,9 +36,10 @@ export class ServerMailer {
36
36
  }
37
37
 
38
38
  async sendResetPassword(
39
- params: { handle: string; token: string },
39
+ params: { handle: string; token: string; locale?: string },
40
40
  mailOpts: SendMailOptions,
41
41
  ) {
42
+ // @TODO (later) handle locale in the template
42
43
  await this.sendTemplate('resetPassword', params, {
43
44
  subject: 'Password Reset Requested',
44
45
  ...mailOpts,
@@ -46,9 +47,10 @@ export class ServerMailer {
46
47
  }
47
48
 
48
49
  async sendAccountDelete(
49
- params: { token: string },
50
+ params: { token: string; locale?: string },
50
51
  mailOpts: SendMailOptions,
51
52
  ) {
53
+ // @TODO (later) handle locale in the template
52
54
  await this.sendTemplate('deleteAccount', params, {
53
55
  subject: 'Account Deletion Requested',
54
56
  ...mailOpts,
@@ -1,7 +1,7 @@
1
1
  import { IncomingHttpHeaders, ServerResponse } from 'node:http'
2
2
  import { PassThrough, Readable, finished } from 'node:stream'
3
3
  import { Request } from 'express'
4
- import { Dispatcher } from 'undici'
4
+ import { Agent, Dispatcher, Pool, interceptors } from 'undici'
5
5
  import {
6
6
  decodeStream,
7
7
  getServiceEndpoint,
@@ -20,12 +20,49 @@ import {
20
20
  excludeErrorResult,
21
21
  parseReqNsid,
22
22
  } from '@atproto/xrpc-server'
23
+ import { isUnicastIp, unicastLookup } from '@atproto-labs/fetch-node'
23
24
  import { buildProxiedContentEncoding } from '@atproto-labs/xrpc-utils'
24
25
  import { isAccessPrivileged } from './auth-scope.js'
26
+ import { ProxyConfig } from './config/config.js'
25
27
  import { AppContext } from './context.js'
26
28
  import { chat, com, tools } from './lexicons/index.js'
27
29
  import { httpLogger } from './logger.js'
28
30
 
31
+ export function buildProxyAgent(cfg: ProxyConfig): Dispatcher {
32
+ const agent = new Agent({
33
+ allowH2: cfg.allowHTTP2,
34
+ headersTimeout: cfg.headersTimeout,
35
+ maxResponseSize: cfg.maxResponseSize,
36
+ bodyTimeout: cfg.bodyTimeout,
37
+ factory: cfg.disableSsrfProtection
38
+ ? undefined
39
+ : (origin, opts) => {
40
+ const { protocol, hostname } =
41
+ origin instanceof URL ? origin : new URL(origin)
42
+ if (protocol !== 'https:') {
43
+ throw new Error(`Forbidden protocol "${protocol}"`)
44
+ }
45
+ if (isUnicastIp(hostname) === false) {
46
+ throw new Error('Hostname resolved to non-unicast address')
47
+ }
48
+ return new Pool(origin, opts)
49
+ },
50
+ connect: {
51
+ lookup: cfg.disableSsrfProtection ? undefined : unicastLookup,
52
+ },
53
+ })
54
+
55
+ return agent.compose(
56
+ cfg.maxRetries > 0
57
+ ? interceptors.retry({
58
+ statusCodes: [], // Only retry on socket errors
59
+ methods: ['GET', 'HEAD'],
60
+ maxRetries: cfg.maxRetries,
61
+ })
62
+ : (dispatch) => dispatch,
63
+ )
64
+ }
65
+
29
66
  export const proxyHandler = (ctx: AppContext): CatchallHandler => {
30
67
  const performAuth = ctx.authVerifier.authorization<RpcPermissionMatch>({
31
68
  authorize: (permissions, { params }) => permissions.assertRpc(params),
@@ -35,7 +35,7 @@ export const getAndMigrateRecoveryDb = async (
35
35
  const pragmas: Record<string, string> = disableWalAutoCheckpoint
36
36
  ? { wal_autocheckpoint: '0' }
37
37
  : {}
38
- const db = Database.sqlite(location, pragmas)
38
+ const db = Database.sqlite<RecoveryDbSchema>(location, pragmas)
39
39
  const migrator = new Migrator(db.db, migrations)
40
40
  await migrator.migrateToLatestOrThrow()
41
41
  return db
@@ -83,12 +83,10 @@ export const formatSeqIdentityEvt = async (
83
83
  did: DidString,
84
84
  handle?: HandleString,
85
85
  ): Promise<RepoSeqInsert> => {
86
- const evt: IdentityEvt = {
87
- did,
88
- }
89
- if (handle) {
90
- evt.handle = handle
91
- }
86
+ const evt: IdentityEvt = handle //
87
+ ? { did, handle }
88
+ : { did }
89
+
92
90
  return {
93
91
  did,
94
92
  eventType: 'identity',
@@ -101,13 +99,10 @@ export const formatSeqAccountEvt = async (
101
99
  did: DidString,
102
100
  status: AccountStatus,
103
101
  ): Promise<RepoSeqInsert> => {
104
- const evt: AccountEvt = {
105
- did,
106
- active: status === 'active',
107
- }
108
- if (status !== AccountStatus.Active) {
109
- evt.status = status
110
- }
102
+ const evt: AccountEvt =
103
+ status === AccountStatus.Active
104
+ ? { did, active: true }
105
+ : { did, active: false, status }
111
106
 
112
107
  return {
113
108
  did,