@atproto/pds 0.5.2 → 0.5.4

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 (131) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/account-manager/account-manager.d.ts.map +1 -1
  3. package/dist/account-manager/account-manager.js +16 -19
  4. package/dist/account-manager/account-manager.js.map +1 -1
  5. package/dist/account-manager/oauth-store.d.ts.map +1 -1
  6. package/dist/account-manager/oauth-store.js +46 -26
  7. package/dist/account-manager/oauth-store.js.map +1 -1
  8. package/dist/api/com/atproto/admin/deleteAccount.d.ts.map +1 -1
  9. package/dist/api/com/atproto/admin/deleteAccount.js +9 -4
  10. package/dist/api/com/atproto/admin/deleteAccount.js.map +1 -1
  11. package/dist/api/com/atproto/admin/updateSubjectStatus.js +1 -1
  12. package/dist/api/com/atproto/admin/updateSubjectStatus.js.map +1 -1
  13. package/dist/api/com/atproto/identity/submitPlcOperation.js +1 -1
  14. package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
  15. package/dist/api/com/atproto/server/activateAccount.js +1 -3
  16. package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
  17. package/dist/api/com/atproto/server/createAccount.d.ts.map +1 -1
  18. package/dist/api/com/atproto/server/createAccount.js +61 -45
  19. package/dist/api/com/atproto/server/createAccount.js.map +1 -1
  20. package/dist/api/com/atproto/server/deactivateAccount.js +1 -1
  21. package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
  22. package/dist/api/com/atproto/server/deleteAccount.d.ts.map +1 -1
  23. package/dist/api/com/atproto/server/deleteAccount.js +9 -4
  24. package/dist/api/com/atproto/server/deleteAccount.js.map +1 -1
  25. package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
  26. package/dist/api/com/atproto/sync/getRepo.js +21 -9
  27. package/dist/api/com/atproto/sync/getRepo.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -39
  30. package/dist/index.js.map +1 -1
  31. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +4 -0
  32. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
  33. package/dist/lexicons/chat/bsky/convo/defs.defs.js +1 -0
  34. package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
  35. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.d.ts +3 -0
  36. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.d.ts.map +1 -0
  37. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.d.ts +23 -0
  38. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.d.ts.map +1 -0
  39. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.js +19 -0
  40. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.js.map +1 -0
  41. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.js +6 -0
  42. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.js.map +1 -0
  43. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts +1 -1
  44. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts.map +1 -1
  45. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js +1 -0
  46. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js.map +1 -1
  47. package/dist/lexicons/chat/bsky/convo.d.ts +1 -0
  48. package/dist/lexicons/chat/bsky/convo.d.ts.map +1 -1
  49. package/dist/lexicons/chat/bsky/convo.js +1 -0
  50. package/dist/lexicons/chat/bsky/convo.js.map +1 -1
  51. package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts +1 -1
  52. package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts.map +1 -1
  53. package/dist/lexicons/chat/bsky/embed/joinLink.defs.js +5 -1
  54. package/dist/lexicons/chat/bsky/embed/joinLink.defs.js.map +1 -1
  55. package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts +5 -5
  56. package/dist/lexicons/chat/bsky/group/createGroup.defs.js +3 -3
  57. package/dist/lexicons/chat/bsky/group/createGroup.defs.js.map +1 -1
  58. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts +26 -2
  59. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts.map +1 -1
  60. package/dist/lexicons/chat/bsky/group/defs.defs.js +15 -2
  61. package/dist/lexicons/chat/bsky/group/defs.defs.js.map +1 -1
  62. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts +3 -3
  63. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts.map +1 -1
  64. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js +6 -2
  65. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js.map +1 -1
  66. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts +1 -1
  67. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts.map +1 -1
  68. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.js.map +1 -1
  69. package/dist/lexicons/index.d.ts +1 -0
  70. package/dist/lexicons/index.d.ts.map +1 -1
  71. package/dist/lexicons/index.js +1 -0
  72. package/dist/lexicons/index.js.map +1 -1
  73. package/dist/lexicons/internal/bsky/actor/getProfiles.d.ts +3 -0
  74. package/dist/lexicons/internal/bsky/actor/getProfiles.d.ts.map +1 -0
  75. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.d.ts +38 -0
  76. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.d.ts.map +1 -0
  77. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.js +26 -0
  78. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.js.map +1 -0
  79. package/dist/lexicons/internal/bsky/actor/getProfiles.js +6 -0
  80. package/dist/lexicons/internal/bsky/actor/getProfiles.js.map +1 -0
  81. package/dist/lexicons/internal/bsky/actor.d.ts +2 -0
  82. package/dist/lexicons/internal/bsky/actor.d.ts.map +1 -0
  83. package/dist/lexicons/internal/bsky/actor.js +5 -0
  84. package/dist/lexicons/internal/bsky/actor.js.map +1 -0
  85. package/dist/lexicons/internal/bsky.d.ts +2 -0
  86. package/dist/lexicons/internal/bsky.d.ts.map +1 -0
  87. package/dist/lexicons/internal/bsky.js +5 -0
  88. package/dist/lexicons/internal/bsky.js.map +1 -0
  89. package/dist/lexicons/internal.d.ts +2 -0
  90. package/dist/lexicons/internal.d.ts.map +1 -0
  91. package/dist/lexicons/internal.js +5 -0
  92. package/dist/lexicons/internal.js.map +1 -0
  93. package/dist/rate-limits.d.ts +7 -0
  94. package/dist/rate-limits.d.ts.map +1 -0
  95. package/dist/rate-limits.js +50 -0
  96. package/dist/rate-limits.js.map +1 -0
  97. package/dist/scripts/publish-identity.js +1 -1
  98. package/dist/scripts/publish-identity.js.map +1 -1
  99. package/dist/scripts/rebuild-repo.js +1 -1
  100. package/dist/scripts/rebuild-repo.js.map +1 -1
  101. package/dist/scripts/rotate-keys.js +2 -2
  102. package/dist/scripts/rotate-keys.js.map +1 -1
  103. package/dist/scripts/sequencer-recovery/recoverer.js +7 -5
  104. package/dist/scripts/sequencer-recovery/recoverer.js.map +1 -1
  105. package/dist/sequencer/sequencer.d.ts +8 -6
  106. package/dist/sequencer/sequencer.d.ts.map +1 -1
  107. package/dist/sequencer/sequencer.js +40 -21
  108. package/dist/sequencer/sequencer.js.map +1 -1
  109. package/package.json +10 -10
  110. package/src/account-manager/account-manager.ts +26 -23
  111. package/src/account-manager/oauth-store.ts +55 -36
  112. package/src/api/com/atproto/admin/deleteAccount.ts +9 -7
  113. package/src/api/com/atproto/admin/updateSubjectStatus.ts +1 -1
  114. package/src/api/com/atproto/identity/submitPlcOperation.ts +1 -1
  115. package/src/api/com/atproto/server/activateAccount.ts +3 -3
  116. package/src/api/com/atproto/server/createAccount.ts +72 -63
  117. package/src/api/com/atproto/server/deactivateAccount.ts +1 -1
  118. package/src/api/com/atproto/server/deleteAccount.ts +9 -7
  119. package/src/api/com/atproto/sync/getRepo.ts +26 -9
  120. package/src/index.ts +3 -42
  121. package/src/rate-limits.ts +59 -0
  122. package/src/scripts/publish-identity.ts +1 -1
  123. package/src/scripts/rebuild-repo.ts +1 -1
  124. package/src/scripts/rotate-keys.ts +2 -2
  125. package/src/scripts/sequencer-recovery/recoverer.ts +9 -5
  126. package/src/sequencer/sequencer.ts +52 -23
  127. package/tests/account-manager.test.ts +78 -0
  128. package/tsconfig.build.json +2 -2
  129. package/tsconfig.build.tsbuildinfo +1 -1
  130. package/tsconfig.json +2 -2
  131. package/tsconfig.tests.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import * as plc from '@did-plc/lib'
2
2
  import { isEmailValid } from '@hapi/address'
3
3
  import { isDisposableEmail } from 'disposable-email-domains-js'
4
- import { DidDocument, MINUTE, check } from '@atproto/common'
4
+ import { MINUTE, check } from '@atproto/common'
5
5
  import { ExportableKeypair, Keypair, Secp256k1Keypair } from '@atproto/crypto'
6
6
  import { AtprotoData, ensureAtpDocument } from '@atproto/identity'
7
7
  import { DidString } from '@atproto/syntax'
@@ -10,12 +10,10 @@ import {
10
10
  InvalidRequestError,
11
11
  Server,
12
12
  } from '@atproto/xrpc-server'
13
- import { AccountStatus } from '../../../../account-manager/account-manager.js'
14
13
  import { NEW_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt.js'
15
14
  import { AppContext } from '../../../../context.js'
16
15
  import { baseNormalizeAndValidate } from '../../../../handle/index.js'
17
16
  import { com } from '../../../../lexicons/index.js'
18
- import { syncEvtDataFromCommit } from '../../../../sequencer/index.js'
19
17
  import { safeResolveDidDoc } from './util.js'
20
18
 
21
19
  export default function (server: Server, ctx: AppContext) {
@@ -47,68 +45,85 @@ export default function (server: Server, ctx: AppContext) {
47
45
  ? await validateInputsForEntrywayPds(ctx, input.body)
48
46
  : await validateInputsForLocalPds(ctx, input.body, requester)
49
47
 
50
- let didDoc: DidDocument | undefined
51
- let creds: { accessJwt: string; refreshJwt: string }
52
48
  await ctx.actorStore.create(did, signingKey)
49
+
53
50
  try {
54
- const commit = await ctx.actorStore.transact(did, (actorTxn) =>
55
- actorTxn.repo.createRepo([]),
56
- )
51
+ const commit = await ctx.actorStore.transact(did, (actorTxn) => {
52
+ return actorTxn.repo.createRepo([])
53
+ })
54
+
55
+ const canTombstone =
56
+ // @NOTE IMPORTANT Because the user may be bringing their own did, we
57
+ // must make sure not to tombstone their did on failure if we didn't
58
+ // create it here.
59
+ !ctx.entrywayClient && !input.body.did && !!plcOp
57
60
 
58
61
  // Generate a real did with PLC
59
62
  if (plcOp) {
60
- try {
61
- await ctx.plcClient.sendOperation(did, plcOp)
62
- } catch (err) {
63
- req.log.error(
64
- { didKey: ctx.plcRotationKey.did(), handle },
65
- 'failed to create did:plc',
66
- )
67
- throw err
68
- }
63
+ await ctx.plcClient.sendOperation(did, plcOp)
69
64
  }
70
65
 
71
- didDoc = await safeResolveDidDoc(ctx, did, true)
66
+ try {
67
+ const didDoc = await safeResolveDidDoc(ctx, did, true)
72
68
 
73
- creds = await ctx.accountManager.createAccountAndSession({
74
- did,
75
- handle,
76
- email,
77
- password,
78
- repoCid: commit.cid,
79
- repoRev: commit.rev,
80
- inviteCode,
81
- deactivated,
82
- })
83
-
84
- if (!deactivated) {
85
- await ctx.sequencer.sequenceIdentityEvt(did, handle)
86
- await ctx.sequencer.sequenceAccountEvt(did, AccountStatus.Active)
87
- await ctx.sequencer.sequenceCommit(did, commit)
88
- await ctx.sequencer.sequenceSyncEvt(
69
+ const creds = await ctx.accountManager.createAccountAndSession({
89
70
  did,
90
- syncEvtDataFromCommit(commit),
91
- )
71
+ handle,
72
+ email,
73
+ password,
74
+ repoCid: commit.cid,
75
+ repoRev: commit.rev,
76
+ inviteCode,
77
+ deactivated,
78
+ })
79
+
80
+ try {
81
+ const sequenceEvt = !deactivated
82
+ if (sequenceEvt) {
83
+ await ctx.sequencer.sequenceAccountCreation(did, handle, commit)
84
+ }
85
+
86
+ try {
87
+ await ctx.actorStore
88
+ .clearReservedKeypair(signingKey.did(), did)
89
+ .catch((err) => {
90
+ // @NOTE This is a cleanup operation so we won't fail the whole
91
+ // flow if it fails, but we log it just in case
92
+ req.log.error(
93
+ { did, signingKeyDid: signingKey.did(), err },
94
+ 'Failed to clear reserved keypair',
95
+ )
96
+ })
97
+
98
+ return {
99
+ encoding: 'application/json' as const,
100
+ body: {
101
+ handle,
102
+ did: did,
103
+ // @ts-expect-error https://github.com/bluesky-social/atproto/pull/4406
104
+ didDoc,
105
+ accessJwt: creds.accessJwt,
106
+ refreshJwt: creds.refreshJwt,
107
+ },
108
+ }
109
+ } catch (err) {
110
+ if (sequenceEvt) await ctx.sequencer.sequenceAccountDeletion(did)
111
+ throw err
112
+ }
113
+ } catch (err) {
114
+ await ctx.accountManager.deleteAccount(did)
115
+ throw err
116
+ }
117
+ } catch (err) {
118
+ if (canTombstone) {
119
+ await ctx.plcClient.tombstone(did, ctx.plcRotationKey)
120
+ }
121
+ throw err
92
122
  }
93
- await ctx.accountManager.updateRepoRoot(did, commit.cid, commit.rev)
94
- await ctx.actorStore.clearReservedKeypair(signingKey.did(), did)
95
123
  } catch (err) {
96
- // this will only be reached if the actor store _did not_ exist before
97
124
  await ctx.actorStore.destroy(did)
98
125
  throw err
99
126
  }
100
-
101
- return {
102
- encoding: 'application/json' as const,
103
- body: {
104
- handle,
105
- did: did,
106
- // @ts-expect-error https://github.com/bluesky-social/atproto/pull/4406
107
- didDoc,
108
- accessJwt: creds.accessJwt,
109
- refreshJwt: creds.refreshJwt,
110
- },
111
- }
112
127
  },
113
128
  })
114
129
  }
@@ -117,9 +132,10 @@ const validateInputsForEntrywayPds = async (
117
132
  ctx: AppContext,
118
133
  input: com.atproto.server.createAccount.$InputBody,
119
134
  ) => {
120
- const { did, plcOp } = input
121
135
  const handle = baseNormalizeAndValidate(input.handle)
122
- if (!did || !input.plcOp) {
136
+
137
+ const { did, plcOp } = input
138
+ if (!did || !plcOp) {
123
139
  throw new InvalidRequestError(
124
140
  'non-entryway pds requires bringing a DID and plcOp',
125
141
  )
@@ -244,7 +260,7 @@ const validateInputsForLocalPds = async (
244
260
  } else {
245
261
  const formatted = await formatDidAndPlcOp(ctx, handle, input, signingKey)
246
262
  did = formatted.did as DidString
247
- plcOp = formatted.plcOp
263
+ plcOp = formatted.op
248
264
  }
249
265
 
250
266
  return {
@@ -264,10 +280,7 @@ const formatDidAndPlcOp = async (
264
280
  handle: string,
265
281
  input: com.atproto.server.createAccount.$InputBody,
266
282
  signingKey: Keypair,
267
- ): Promise<{
268
- did: string
269
- plcOp: plc.Operation | null
270
- }> => {
283
+ ) => {
271
284
  // if the user is not bringing a DID, then we format a create op for PLC
272
285
  const rotationKeys = [ctx.plcRotationKey.did()]
273
286
  if (ctx.cfg.identity.recoveryDidKey) {
@@ -276,17 +289,13 @@ const formatDidAndPlcOp = async (
276
289
  if (input.recoveryKey) {
277
290
  rotationKeys.unshift(input.recoveryKey)
278
291
  }
279
- const plcCreate = await plc.createOp({
292
+ return plc.createOp({
280
293
  signingKey: signingKey.did(),
281
294
  rotationKeys,
282
295
  handle,
283
296
  pds: ctx.cfg.service.publicUrl,
284
297
  signer: ctx.plcRotationKey,
285
298
  })
286
- return {
287
- did: plcCreate.did,
288
- plcOp: plcCreate.op,
289
- }
290
299
  }
291
300
  const validateAtprotoData = (
292
301
  data: AtprotoData,
@@ -38,7 +38,7 @@ export default function (server: Server, ctx: AppContext) {
38
38
  body.deleteAfter ?? null,
39
39
  )
40
40
  const status = await ctx.accountManager.getAccountStatus(requester)
41
- await ctx.sequencer.sequenceAccountEvt(requester, status)
41
+ await ctx.sequencer.sequenceAccount(requester, status)
42
42
  },
43
43
  })
44
44
  }
@@ -4,7 +4,6 @@ import {
4
4
  InvalidRequestError,
5
5
  Server,
6
6
  } from '@atproto/xrpc-server'
7
- import { AccountStatus } from '../../../../account-manager/account-manager.js'
8
7
  import { OLD_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt.js'
9
8
  import { AppContext } from '../../../../context.js'
10
9
  import { com } from '../../../../lexicons/index.js'
@@ -54,13 +53,16 @@ export default function (server: Server, ctx: AppContext) {
54
53
  'delete_account',
55
54
  token,
56
55
  )
57
- await ctx.actorStore.destroy(did)
56
+
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.
58
60
  await ctx.accountManager.deleteAccount(did)
59
- const accountSeq = await ctx.sequencer.sequenceAccountEvt(
60
- did,
61
- AccountStatus.Deleted,
62
- )
63
- await ctx.sequencer.deleteAllForUser(did, [accountSeq])
61
+ try {
62
+ await ctx.sequencer.sequenceAccountDeletion(did)
63
+ } finally {
64
+ await ctx.actorStore.destroy(did)
65
+ }
64
66
  },
65
67
  })
66
68
  }
@@ -1,5 +1,9 @@
1
1
  import stream from 'node:stream'
2
- import { byteIterableToStream } from '@atproto/common'
2
+ import {
3
+ MINUTE,
4
+ byteIterableToStream,
5
+ coalesceByteStream,
6
+ } from '@atproto/common'
3
7
  import { InvalidRequestError, Server } from '@atproto/xrpc-server'
4
8
  import {
5
9
  RepoRootNotFoundError,
@@ -11,6 +15,8 @@ import { AppContext } from '../../../../context.js'
11
15
  import { com } from '../../../../lexicons/index.js'
12
16
  import { assertRepoAvailability } from './util.js'
13
17
 
18
+ const CAR_STREAM_CHUNK_SIZE = 64 * 1024
19
+
14
20
  export default function (server: Server, ctx: AppContext) {
15
21
  server.add(com.atproto.sync.getRepo, {
16
22
  auth: ctx.authVerifier.authorizationOrAdminTokenOptional({
@@ -19,7 +25,11 @@ export default function (server: Server, ctx: AppContext) {
19
25
  // always allow
20
26
  },
21
27
  }),
22
- handler: async ({ params, auth }) => {
28
+ rateLimit: {
29
+ durationMs: 5 * MINUTE,
30
+ points: 6000,
31
+ },
32
+ handler: async ({ req, params, auth }) => {
23
33
  const { did, since } = params
24
34
  await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))
25
35
 
@@ -27,7 +37,15 @@ export default function (server: Server, ctx: AppContext) {
27
37
 
28
38
  return {
29
39
  encoding: 'application/vnd.ipld.car' as const,
30
- body: carStream,
40
+ // @NOTE If the client asked for compression (via "accept-encoding"), we
41
+ // coalesce the CAR stream into larger chunks to improve compression
42
+ // efficiency. See https://github.com/bluesky-social/atproto/pull/5078
43
+ //
44
+ // @TODO This would be better handled by xrpc-server and/or the
45
+ // compression middleware instead of manually coalescing the stream.
46
+ body: req.headers['accept-encoding']
47
+ ? coalesceByteStream(carStream, CAR_STREAM_CHUNK_SIZE)
48
+ : carStream,
31
49
  }
32
50
  },
33
51
  })
@@ -39,11 +57,14 @@ export const getCarStream = async (
39
57
  since?: string,
40
58
  ): Promise<stream.Readable> => {
41
59
  const actorDb = await ctx.actorStore.openDb(did)
42
- let carStream: stream.Readable
43
60
  try {
44
61
  const storage = new SqlRepoReader(actorDb)
45
62
  const carIter = await storage.getCarStream(since)
46
- carStream = byteIterableToStream(carIter)
63
+ const carStream = byteIterableToStream(carIter)
64
+ const closeDb = () => actorDb.close()
65
+ carStream.on('error', closeDb)
66
+ carStream.on('close', closeDb)
67
+ return carStream
47
68
  } catch (err) {
48
69
  await actorDb.close()
49
70
  if (err instanceof RepoRootNotFoundError) {
@@ -51,8 +72,4 @@ export const getCarStream = async (
51
72
  }
52
73
  throw err
53
74
  }
54
- const closeDb = () => actorDb.close()
55
- carStream.on('error', closeDb)
56
- carStream.on('close', closeDb)
57
- return carStream
58
75
  }
package/src/index.ts CHANGED
@@ -14,11 +14,9 @@ import httpTerminator from 'http-terminator'
14
14
  // eslint-disable-next-line import/no-named-as-default-member
15
15
  const { createHttpTerminator } = httpTerminator
16
16
  type HttpTerminator = ReturnType<typeof createHttpTerminator>
17
- import { DAY, HOUR, MINUTE, SECOND } from '@atproto/common'
17
+ import { DAY, SECOND } from '@atproto/common'
18
18
  import {
19
- MemoryRateLimiter,
20
19
  MethodHandler,
21
- RedisRateLimiter,
22
20
  ResponseType,
23
21
  XRPCError,
24
22
  createServer,
@@ -32,6 +30,7 @@ import * as error from './error.js'
32
30
  import { app } from './lexicons.js'
33
31
  import { loggerMiddleware } from './logger.js'
34
32
  import { proxyHandler } from './pipethrough.js'
33
+ import { buildRateLimitsConfig } from './rate-limits.js'
35
34
  import compression from './util/compression.js'
36
35
  import * as wellKnown from './well-known.js'
37
36
 
@@ -113,45 +112,7 @@ export class PDS {
113
112
 
114
113
  return XRPCError.fromError(err)
115
114
  },
116
- rateLimits: rateLimits.enabled
117
- ? {
118
- creator: ctx.redisScratch
119
- ? (opts) => new RedisRateLimiter(ctx.redisScratch, opts)
120
- : (opts) => new MemoryRateLimiter(opts),
121
- bypass: ({ req }) => {
122
- const { bypassKey, bypassIps } = rateLimits
123
- if (
124
- bypassKey &&
125
- bypassKey === req.headers['x-ratelimit-bypass']
126
- ) {
127
- return true
128
- }
129
- if (bypassIps && bypassIps.includes(req.ip)) {
130
- return true
131
- }
132
- return false
133
- },
134
- global: [
135
- {
136
- name: 'global-ip',
137
- durationMs: 5 * MINUTE,
138
- points: 3000,
139
- },
140
- ],
141
- shared: [
142
- {
143
- name: 'repo-write-hour',
144
- durationMs: HOUR,
145
- points: 5000, // creates=3, puts=2, deletes=1
146
- },
147
- {
148
- name: 'repo-write-day',
149
- durationMs: DAY,
150
- points: 35000, // creates=3, puts=2, deletes=1
151
- },
152
- ],
153
- }
154
- : undefined,
115
+ rateLimits: buildRateLimitsConfig(rateLimits, ctx.redisScratch),
155
116
  })
156
117
 
157
118
  apiRoutes(server, ctx)
@@ -0,0 +1,59 @@
1
+ import type { Redis } from 'ioredis'
2
+ import { DAY, HOUR, MINUTE } from '@atproto/common'
3
+ import { MemoryRateLimiter, RedisRateLimiter } from '@atproto/xrpc-server'
4
+ import type { Options } from '@atproto/xrpc-server'
5
+ import type { RateLimitsConfig } from './config/index.js'
6
+
7
+ type RateLimitDescriptions = NonNullable<Options['rateLimits']>
8
+
9
+ const SYNC_GET_REPO_PATH = '/xrpc/com.atproto.sync.getRepo'
10
+
11
+ export const buildRateLimitsConfig = (
12
+ rateLimits: RateLimitsConfig,
13
+ redisScratch?: Redis,
14
+ ): RateLimitDescriptions | undefined => {
15
+ if (!rateLimits.enabled) return undefined
16
+
17
+ return {
18
+ creator: redisScratch
19
+ ? (opts) => new RedisRateLimiter(redisScratch, opts)
20
+ : (opts) => new MemoryRateLimiter(opts),
21
+ bypass: ({ req }) => {
22
+ const { bypassKey, bypassIps } = rateLimits
23
+ if (bypassKey && bypassKey === req.headers['x-ratelimit-bypass']) {
24
+ return true
25
+ }
26
+ if (bypassIps && bypassIps.includes(req.ip)) {
27
+ return true
28
+ }
29
+ return false
30
+ },
31
+ global: [
32
+ {
33
+ name: 'global-ip',
34
+ durationMs: 5 * MINUTE,
35
+ points: 3000,
36
+ // getRepo can be a high-volume sync path, so it has its own endpoint
37
+ // limit and should not consume the shared global read bucket.
38
+ calcKey: ({ req }) => {
39
+ if (req.path === SYNC_GET_REPO_PATH) {
40
+ return null
41
+ }
42
+ return req.ip
43
+ },
44
+ },
45
+ ],
46
+ shared: [
47
+ {
48
+ name: 'repo-write-hour',
49
+ durationMs: HOUR,
50
+ points: 5000, // creates=3, puts=2, deletes=1
51
+ },
52
+ {
53
+ name: 'repo-write-day',
54
+ durationMs: DAY,
55
+ points: 35000, // creates=3, puts=2, deletes=1
56
+ },
57
+ ],
58
+ }
59
+ }
@@ -48,7 +48,7 @@ export const publishIdentityEvtForDids = async (
48
48
  ) => {
49
49
  for (const did of dids) {
50
50
  try {
51
- await ctx.sequencer.sequenceIdentityEvt(did)
51
+ await ctx.sequencer.sequenceIdentity(did)
52
52
  console.log(`published identity evt for ${did}`)
53
53
  } catch (err) {
54
54
  console.error(`failed to sequence new identity evt for ${did}: ${err}`)
@@ -106,7 +106,7 @@ export const rebuildRepo = async (
106
106
  const syncData = await ctx.actorStore.read(did, (store) =>
107
107
  store.repo.getSyncEventData(),
108
108
  )
109
- await ctx.sequencer.sequenceSyncEvt(did, syncData)
109
+ await ctx.sequencer.sequenceSync(did, syncData)
110
110
  }
111
111
 
112
112
  const promptContinue = async (): Promise<boolean> => {
@@ -100,13 +100,13 @@ const rotateKeysForRepos = async (
100
100
  return
101
101
  }
102
102
  try {
103
- await ctx.sequencer.sequenceIdentityEvt(did)
103
+ await ctx.sequencer.sequenceIdentity(did)
104
104
  } catch (err) {
105
105
  console.error(`failed to sequence new identity evt for ${did}: ${err}`)
106
106
  return
107
107
  }
108
108
  try {
109
- await ctx.sequencer.sequenceSyncEvt(did, syncData)
109
+ await ctx.sequencer.sequenceSync(did, syncData)
110
110
  } catch (err) {
111
111
  console.error(`failed to sequence for ${did}: ${err}`)
112
112
  return
@@ -179,12 +179,16 @@ const processRepoCreation = async (
179
179
 
180
180
  const processAccountEvt = async (ctx: RecovererContext, evt: AccountEvt) => {
181
181
  // do not need to process deactivation/takedowns because we backup account DB as well
182
- if (evt.status !== AccountStatus.Deleted) {
183
- return
182
+
183
+ if (evt.status === AccountStatus.Deleted) {
184
+ // In case an account deletion was sequenced, let's make sure to (first)
185
+ // delete the accounts database, and (then) unlink the actor store from the
186
+ // file system. Order matters here.
187
+ await ctx.accountManager.deleteAccount(evt.did)
188
+
189
+ const { directory } = await ctx.actorStore.getLocation(evt.did)
190
+ await rmIfExists(directory, true)
184
191
  }
185
- const { directory } = await ctx.actorStore.getLocation(evt.did)
186
- await rmIfExists(directory, true)
187
- await ctx.accountManager.deleteAccount(evt.did)
188
192
  }
189
193
 
190
194
  const trackBlobs = async (
@@ -24,6 +24,7 @@ import {
24
24
  formatSeqCommit,
25
25
  formatSeqIdentityEvt,
26
26
  formatSeqSyncEvt,
27
+ syncEvtDataFromCommit,
27
28
  } from './events.js'
28
29
 
29
30
  export * from './events.js'
@@ -161,51 +162,79 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
161
162
  await wait(waitTime)
162
163
  }
163
164
 
164
- async sequenceEvt(evt: RepoSeqInsert): Promise<number> {
165
- const [{ seq }] = await this.db.executeWithRetry(
166
- this.db.db.insertInto('repo_seq').values(evt).returning('seq'),
165
+ protected async sequenceEvts(
166
+ events: readonly RepoSeqInsert[],
167
+ ): Promise<number[]> {
168
+ if (!events.length) return []
169
+ const rows = await this.db.executeWithRetry(
170
+ this.db.db.insertInto('repo_seq').values(events).returning('seq'),
167
171
  )
168
172
  this.crawlers.notifyOfUpdate()
169
- return seq
173
+ return rows.map((row) => row.seq)
170
174
  }
171
175
 
172
- async sequenceCommit(
176
+ public async sequenceCommit(
173
177
  did: DidString,
174
178
  commitData: CommitDataWithOps,
175
- ): Promise<number> {
176
- const evt = await formatSeqCommit(did, commitData)
177
- return this.sequenceEvt(evt)
179
+ ): Promise<void> {
180
+ await this.sequenceEvts([await formatSeqCommit(did, commitData)])
178
181
  }
179
182
 
180
- async sequenceSyncEvt(did: DidString, data: SyncEvtData) {
181
- const evt = await formatSeqSyncEvt(did, data)
182
- return this.sequenceEvt(evt)
183
+ public async sequenceSync(did: DidString, data: SyncEvtData): Promise<void> {
184
+ await this.sequenceEvts([await formatSeqSyncEvt(did, data)])
183
185
  }
184
186
 
185
- async sequenceIdentityEvt(
187
+ public async sequenceIdentity(
186
188
  did: DidString,
187
189
  handle?: HandleString,
188
- ): Promise<number> {
189
- const evt = await formatSeqIdentityEvt(did, handle)
190
- return this.sequenceEvt(evt)
190
+ ): Promise<void> {
191
+ await this.sequenceEvts([await formatSeqIdentityEvt(did, handle)])
191
192
  }
192
193
 
193
- async sequenceAccountEvt(
194
+ public async sequenceAccount(
194
195
  did: DidString,
195
196
  status: AccountStatus,
196
- ): Promise<number> {
197
- const evt = await formatSeqAccountEvt(did, status)
198
- return this.sequenceEvt(evt)
197
+ ): Promise<void> {
198
+ await this.sequenceEvts([await formatSeqAccountEvt(did, status)])
199
199
  }
200
200
 
201
- async deleteAllForUser(did: string, excludingSeqs: number[] = []) {
201
+ public async sequenceAccountCreation(
202
+ did: DidString,
203
+ handle: HandleString,
204
+ commit: CommitDataWithOps,
205
+ ): Promise<void> {
206
+ // Atomically sequence all events
207
+ await this.sequenceEvts([
208
+ await formatSeqIdentityEvt(did, handle),
209
+ await formatSeqAccountEvt(did, AccountStatus.Active),
210
+ await formatSeqCommit(did, commit),
211
+ await formatSeqSyncEvt(did, syncEvtDataFromCommit(commit)),
212
+ ])
213
+ }
214
+
215
+ public async sequenceAccountActivation(
216
+ did: DidString,
217
+ handle: HandleString,
218
+ status: AccountStatus,
219
+ syncData: SyncEvtData,
220
+ ): Promise<void> {
221
+ // Atomically sequence all events
222
+ await this.sequenceEvts([
223
+ await formatSeqAccountEvt(did, status),
224
+ await formatSeqIdentityEvt(did, handle),
225
+ await formatSeqSyncEvt(did, syncData),
226
+ ])
227
+ }
228
+
229
+ public async sequenceAccountDeletion(did: DidString) {
230
+ const [seq] = await this.sequenceEvts([
231
+ await formatSeqAccountEvt(did, AccountStatus.Deleted),
232
+ ])
202
233
  await this.db.executeWithRetry(
203
234
  this.db.db
204
235
  .deleteFrom('repo_seq')
205
236
  .where('did', '=', did)
206
- .if(excludingSeqs.length > 0, (qb) =>
207
- qb.where('seq', 'not in', excludingSeqs),
208
- ),
237
+ .where('seq', '!=', seq),
209
238
  )
210
239
  }
211
240
  }