@atproto/pds 0.4.28 → 0.4.29

Sign up to get free protection for your applications and to get access to all the features.
Files changed (188) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/account-manager/helpers/account.d.ts +7 -0
  3. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  4. package/dist/account-manager/helpers/account.js +9 -1
  5. package/dist/account-manager/helpers/account.js.map +1 -1
  6. package/dist/account-manager/index.d.ts +3 -1
  7. package/dist/account-manager/index.d.ts.map +1 -1
  8. package/dist/account-manager/index.js +22 -6
  9. package/dist/account-manager/index.js.map +1 -1
  10. package/dist/api/com/atproto/admin/deleteAccount.d.ts.map +1 -1
  11. package/dist/api/com/atproto/admin/deleteAccount.js +4 -2
  12. package/dist/api/com/atproto/admin/deleteAccount.js.map +1 -1
  13. package/dist/api/com/atproto/admin/updateAccountHandle.js +1 -1
  14. package/dist/api/com/atproto/admin/updateAccountHandle.js.map +1 -1
  15. package/dist/api/com/atproto/admin/updateSubjectStatus.d.ts.map +1 -1
  16. package/dist/api/com/atproto/admin/updateSubjectStatus.js +2 -0
  17. package/dist/api/com/atproto/admin/updateSubjectStatus.js.map +1 -1
  18. package/dist/api/com/atproto/identity/submitPlcOperation.d.ts.map +1 -1
  19. package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
  20. package/dist/api/com/atproto/identity/updateHandle.js +1 -1
  21. package/dist/api/com/atproto/identity/updateHandle.js.map +1 -1
  22. package/dist/api/com/atproto/repo/describeRepo.d.ts.map +1 -1
  23. package/dist/api/com/atproto/repo/describeRepo.js +2 -4
  24. package/dist/api/com/atproto/repo/describeRepo.js.map +1 -1
  25. package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
  26. package/dist/api/com/atproto/server/activateAccount.js +2 -1
  27. package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
  28. package/dist/api/com/atproto/server/createAccount.d.ts.map +1 -1
  29. package/dist/api/com/atproto/server/createAccount.js +4 -2
  30. package/dist/api/com/atproto/server/createAccount.js.map +1 -1
  31. package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
  32. package/dist/api/com/atproto/server/deactivateAccount.js +2 -0
  33. package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
  34. package/dist/api/com/atproto/server/deleteAccount.d.ts.map +1 -1
  35. package/dist/api/com/atproto/server/deleteAccount.js +4 -3
  36. package/dist/api/com/atproto/server/deleteAccount.js.map +1 -1
  37. package/dist/api/com/atproto/server/util.d.ts +1 -0
  38. package/dist/api/com/atproto/server/util.d.ts.map +1 -1
  39. package/dist/api/com/atproto/server/util.js +9 -6
  40. package/dist/api/com/atproto/server/util.js.map +1 -1
  41. package/dist/api/com/atproto/sync/deprecated/getCheckout.d.ts.map +1 -1
  42. package/dist/api/com/atproto/sync/deprecated/getCheckout.js +2 -8
  43. package/dist/api/com/atproto/sync/deprecated/getCheckout.js.map +1 -1
  44. package/dist/api/com/atproto/sync/deprecated/getHead.d.ts.map +1 -1
  45. package/dist/api/com/atproto/sync/deprecated/getHead.js +2 -7
  46. package/dist/api/com/atproto/sync/deprecated/getHead.js.map +1 -1
  47. package/dist/api/com/atproto/sync/getBlob.d.ts.map +1 -1
  48. package/dist/api/com/atproto/sync/getBlob.js +3 -6
  49. package/dist/api/com/atproto/sync/getBlob.js.map +1 -1
  50. package/dist/api/com/atproto/sync/getBlocks.d.ts.map +1 -1
  51. package/dist/api/com/atproto/sync/getBlocks.js +2 -7
  52. package/dist/api/com/atproto/sync/getBlocks.js.map +1 -1
  53. package/dist/api/com/atproto/sync/getLatestCommit.d.ts.map +1 -1
  54. package/dist/api/com/atproto/sync/getLatestCommit.js +2 -7
  55. package/dist/api/com/atproto/sync/getLatestCommit.js.map +1 -1
  56. package/dist/api/com/atproto/sync/getRecord.d.ts.map +1 -1
  57. package/dist/api/com/atproto/sync/getRecord.js +2 -7
  58. package/dist/api/com/atproto/sync/getRecord.js.map +1 -1
  59. package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
  60. package/dist/api/com/atproto/sync/getRepo.js +2 -7
  61. package/dist/api/com/atproto/sync/getRepo.js.map +1 -1
  62. package/dist/api/com/atproto/sync/getRepoStatus.d.ts +4 -0
  63. package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -0
  64. package/dist/api/com/atproto/sync/getRepoStatus.js +28 -0
  65. package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -0
  66. package/dist/api/com/atproto/sync/index.d.ts.map +1 -1
  67. package/dist/api/com/atproto/sync/index.js +2 -0
  68. package/dist/api/com/atproto/sync/index.js.map +1 -1
  69. package/dist/api/com/atproto/sync/listBlobs.d.ts.map +1 -1
  70. package/dist/api/com/atproto/sync/listBlobs.js +2 -8
  71. package/dist/api/com/atproto/sync/listBlobs.js.map +1 -1
  72. package/dist/api/com/atproto/sync/listRepos.d.ts.map +1 -1
  73. package/dist/api/com/atproto/sync/listRepos.js +13 -8
  74. package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
  75. package/dist/api/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
  76. package/dist/api/com/atproto/sync/subscribeRepos.js +8 -0
  77. package/dist/api/com/atproto/sync/subscribeRepos.js.map +1 -1
  78. package/dist/api/com/atproto/sync/util.d.ts +11 -0
  79. package/dist/api/com/atproto/sync/util.d.ts.map +1 -0
  80. package/dist/api/com/atproto/sync/util.js +41 -0
  81. package/dist/api/com/atproto/sync/util.js.map +1 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +3 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/lexicon/index.d.ts +2 -0
  87. package/dist/lexicon/index.d.ts.map +1 -1
  88. package/dist/lexicon/index.js +4 -0
  89. package/dist/lexicon/index.js.map +1 -1
  90. package/dist/lexicon/lexicons.d.ts +105 -0
  91. package/dist/lexicon/lexicons.d.ts.map +1 -1
  92. package/dist/lexicon/lexicons.js +185 -4
  93. package/dist/lexicon/lexicons.js.map +1 -1
  94. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts +1 -1
  95. package/dist/lexicon/types/com/atproto/sync/getBlob.d.ts +1 -0
  96. package/dist/lexicon/types/com/atproto/sync/getBlob.d.ts.map +1 -1
  97. package/dist/lexicon/types/com/atproto/sync/getBlocks.d.ts +1 -0
  98. package/dist/lexicon/types/com/atproto/sync/getBlocks.d.ts.map +1 -1
  99. package/dist/lexicon/types/com/atproto/sync/getLatestCommit.d.ts +1 -1
  100. package/dist/lexicon/types/com/atproto/sync/getLatestCommit.d.ts.map +1 -1
  101. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts +1 -0
  102. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts.map +1 -1
  103. package/dist/lexicon/types/com/atproto/sync/getRepo.d.ts +1 -0
  104. package/dist/lexicon/types/com/atproto/sync/getRepo.d.ts.map +1 -1
  105. package/dist/lexicon/types/com/atproto/sync/getRepoStatus.d.ts +42 -0
  106. package/dist/lexicon/types/com/atproto/sync/getRepoStatus.d.ts.map +1 -0
  107. package/dist/lexicon/types/com/atproto/sync/getRepoStatus.js +3 -0
  108. package/dist/lexicon/types/com/atproto/sync/getRepoStatus.js.map +1 -0
  109. package/dist/lexicon/types/com/atproto/sync/listBlobs.d.ts +1 -0
  110. package/dist/lexicon/types/com/atproto/sync/listBlobs.d.ts.map +1 -1
  111. package/dist/lexicon/types/com/atproto/sync/listRepos.d.ts +3 -0
  112. package/dist/lexicon/types/com/atproto/sync/listRepos.d.ts.map +1 -1
  113. package/dist/lexicon/types/com/atproto/sync/listRepos.js.map +1 -1
  114. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts +19 -4
  115. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
  116. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js +11 -1
  117. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js.map +1 -1
  118. package/dist/scripts/index.d.ts +4 -0
  119. package/dist/scripts/index.d.ts.map +1 -0
  120. package/dist/scripts/index.js +8 -0
  121. package/dist/scripts/index.js.map +1 -0
  122. package/dist/scripts/rebuild-repo.d.ts +3 -0
  123. package/dist/scripts/rebuild-repo.d.ts.map +1 -0
  124. package/dist/scripts/rebuild-repo.js +121 -0
  125. package/dist/scripts/rebuild-repo.js.map +1 -0
  126. package/dist/sequencer/db/schema.d.ts +1 -1
  127. package/dist/sequencer/db/schema.d.ts.map +1 -1
  128. package/dist/sequencer/events.d.ts +27 -2
  129. package/dist/sequencer/events.d.ts.map +1 -1
  130. package/dist/sequencer/events.js +35 -2
  131. package/dist/sequencer/events.js.map +1 -1
  132. package/dist/sequencer/sequencer.d.ts +8 -6
  133. package/dist/sequencer/sequencer.d.ts.map +1 -1
  134. package/dist/sequencer/sequencer.js +22 -9
  135. package/dist/sequencer/sequencer.js.map +1 -1
  136. package/package.json +4 -4
  137. package/src/account-manager/helpers/account.ts +8 -0
  138. package/src/account-manager/index.ts +19 -6
  139. package/src/api/com/atproto/admin/deleteAccount.ts +7 -2
  140. package/src/api/com/atproto/admin/updateAccountHandle.ts +1 -1
  141. package/src/api/com/atproto/admin/updateSubjectStatus.ts +2 -0
  142. package/src/api/com/atproto/identity/submitPlcOperation.ts +1 -0
  143. package/src/api/com/atproto/identity/updateHandle.ts +1 -1
  144. package/src/api/com/atproto/repo/describeRepo.ts +2 -4
  145. package/src/api/com/atproto/server/activateAccount.ts +2 -1
  146. package/src/api/com/atproto/server/createAccount.ts +6 -3
  147. package/src/api/com/atproto/server/deactivateAccount.ts +2 -0
  148. package/src/api/com/atproto/server/deleteAccount.ts +7 -3
  149. package/src/api/com/atproto/server/util.ts +12 -5
  150. package/src/api/com/atproto/sync/deprecated/getCheckout.ts +6 -8
  151. package/src/api/com/atproto/sync/deprecated/getHead.ts +7 -10
  152. package/src/api/com/atproto/sync/getBlob.ts +8 -6
  153. package/src/api/com/atproto/sync/getBlocks.ts +6 -7
  154. package/src/api/com/atproto/sync/getLatestCommit.ts +7 -10
  155. package/src/api/com/atproto/sync/getRecord.ts +7 -7
  156. package/src/api/com/atproto/sync/getRepo.ts +6 -7
  157. package/src/api/com/atproto/sync/getRepoStatus.ts +31 -0
  158. package/src/api/com/atproto/sync/index.ts +2 -0
  159. package/src/api/com/atproto/sync/listBlobs.ts +6 -8
  160. package/src/api/com/atproto/sync/listRepos.ts +13 -8
  161. package/src/api/com/atproto/sync/subscribeRepos.ts +7 -0
  162. package/src/api/com/atproto/sync/util.ts +59 -0
  163. package/src/index.ts +1 -0
  164. package/src/lexicon/index.ts +12 -0
  165. package/src/lexicon/lexicons.ts +193 -7
  166. package/src/lexicon/types/chat/bsky/convo/defs.ts +1 -1
  167. package/src/lexicon/types/com/atproto/sync/getBlob.ts +6 -0
  168. package/src/lexicon/types/com/atproto/sync/getBlocks.ts +6 -0
  169. package/src/lexicon/types/com/atproto/sync/getLatestCommit.ts +1 -1
  170. package/src/lexicon/types/com/atproto/sync/getRecord.ts +6 -0
  171. package/src/lexicon/types/com/atproto/sync/getRepo.ts +1 -0
  172. package/src/lexicon/types/com/atproto/sync/getRepoStatus.ts +52 -0
  173. package/src/lexicon/types/com/atproto/sync/listBlobs.ts +1 -0
  174. package/src/lexicon/types/com/atproto/sync/listRepos.ts +3 -0
  175. package/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +30 -3
  176. package/src/scripts/index.ts +5 -0
  177. package/src/scripts/rebuild-repo.ts +143 -0
  178. package/src/sequencer/db/schema.ts +1 -0
  179. package/src/sequencer/events.ts +47 -0
  180. package/src/sequencer/sequencer.ts +35 -14
  181. package/tests/account-deactivation.test.ts +36 -10
  182. package/tests/account-deletion.test.ts +10 -2
  183. package/tests/moderation.test.ts +2 -2
  184. package/tests/proxied/notif.test.ts +1 -0
  185. package/tests/sequencer.test.ts +2 -2
  186. package/tests/sync/list.test.ts +1 -0
  187. package/tests/sync/subscribe-repos.test.ts +224 -40
  188. package/tests/sync/sync.test.ts +48 -4
@@ -0,0 +1,52 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+
11
+ export interface QueryParams {
12
+ /** The DID of the repo. */
13
+ did: string
14
+ }
15
+
16
+ export type InputSchema = undefined
17
+
18
+ export interface OutputSchema {
19
+ did: string
20
+ active: boolean
21
+ /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */
22
+ status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})
23
+ /** Optional field, the current rev of the repo, if active=true */
24
+ rev?: string
25
+ [k: string]: unknown
26
+ }
27
+
28
+ export type HandlerInput = undefined
29
+
30
+ export interface HandlerSuccess {
31
+ encoding: 'application/json'
32
+ body: OutputSchema
33
+ headers?: { [key: string]: string }
34
+ }
35
+
36
+ export interface HandlerError {
37
+ status: number
38
+ message?: string
39
+ error?: 'RepoNotFound'
40
+ }
41
+
42
+ export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
43
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
44
+ auth: HA
45
+ params: QueryParams
46
+ input: HandlerInput
47
+ req: express.Request
48
+ res: express.Response
49
+ }
50
+ export type Handler<HA extends HandlerAuth = never> = (
51
+ ctx: HandlerReqCtx<HA>,
52
+ ) => Promise<HandlerOutput> | HandlerOutput
@@ -36,6 +36,7 @@ export interface HandlerSuccess {
36
36
  export interface HandlerError {
37
37
  status: number
38
38
  message?: string
39
+ error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'
39
40
  }
40
41
 
41
42
  export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
@@ -51,6 +51,9 @@ export interface Repo {
51
51
  /** Current repo commit CID */
52
52
  head: string
53
53
  rev: string
54
+ active?: boolean
55
+ /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */
56
+ status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})
54
57
  [k: string]: unknown
55
58
  }
56
59
 
@@ -16,6 +16,7 @@ export interface QueryParams {
16
16
  export type OutputSchema =
17
17
  | Commit
18
18
  | Identity
19
+ | Account
19
20
  | Handle
20
21
  | Migrate
21
22
  | Tombstone
@@ -77,6 +78,8 @@ export interface Identity {
77
78
  seq: number
78
79
  did: string
79
80
  time: string
81
+ /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */
82
+ handle?: string
80
83
  [k: string]: unknown
81
84
  }
82
85
 
@@ -92,7 +95,31 @@ export function validateIdentity(v: unknown): ValidationResult {
92
95
  return lexicons.validate('com.atproto.sync.subscribeRepos#identity', v)
93
96
  }
94
97
 
95
- /** Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity. */
98
+ /** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */
99
+ export interface Account {
100
+ seq: number
101
+ did: string
102
+ time: string
103
+ /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */
104
+ active: boolean
105
+ /** If active=false, this optional field indicates a reason for why the account is not active. */
106
+ status?: 'takendown' | 'suspended' | 'deleted' | 'deactivated' | (string & {})
107
+ [k: string]: unknown
108
+ }
109
+
110
+ export function isAccount(v: unknown): v is Account {
111
+ return (
112
+ isObj(v) &&
113
+ hasProp(v, '$type') &&
114
+ v.$type === 'com.atproto.sync.subscribeRepos#account'
115
+ )
116
+ }
117
+
118
+ export function validateAccount(v: unknown): ValidationResult {
119
+ return lexicons.validate('com.atproto.sync.subscribeRepos#account', v)
120
+ }
121
+
122
+ /** DEPRECATED -- Use #identity event instead */
96
123
  export interface Handle {
97
124
  seq: number
98
125
  did: string
@@ -113,7 +140,7 @@ export function validateHandle(v: unknown): ValidationResult {
113
140
  return lexicons.validate('com.atproto.sync.subscribeRepos#handle', v)
114
141
  }
115
142
 
116
- /** Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead */
143
+ /** DEPRECATED -- Use #account event instead */
117
144
  export interface Migrate {
118
145
  seq: number
119
146
  did: string
@@ -134,7 +161,7 @@ export function validateMigrate(v: unknown): ValidationResult {
134
161
  return lexicons.validate('com.atproto.sync.subscribeRepos#migrate', v)
135
162
  }
136
163
 
137
- /** Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event */
164
+ /** DEPRECATED -- Use #account event instead */
138
165
  export interface Tombstone {
139
166
  seq: number
140
167
  did: string
@@ -0,0 +1,5 @@
1
+ import { rebuildRepo } from './rebuild-repo'
2
+
3
+ export const scripts = {
4
+ 'rebuild-repo': rebuildRepo,
5
+ }
@@ -0,0 +1,143 @@
1
+ import readline from 'node:readline/promises'
2
+ import { CID } from 'multiformats/cid'
3
+ import {
4
+ BlockMap,
5
+ CidSet,
6
+ MST,
7
+ MemoryBlockstore,
8
+ formatDataKey,
9
+ signCommit,
10
+ } from '@atproto/repo'
11
+ import { AtUri } from '@atproto/syntax'
12
+ import { TID } from '@atproto/common'
13
+ import { ActorStoreTransactor } from '../actor-store'
14
+ import AppContext from '../context'
15
+
16
+ export const rebuildRepo = async (ctx: AppContext, args: string[]) => {
17
+ const did = args[0]
18
+ if (!did || !did.startsWith('did:')) {
19
+ throw new Error('Expected DID as argument')
20
+ }
21
+
22
+ const memoryStore = new MemoryBlockstore()
23
+ const rev = TID.nextStr()
24
+ const commit = await ctx.actorStore.transact(did, async (store) => {
25
+ const [records, existingCids] = await Promise.all([
26
+ listAllRecords(store),
27
+ listExistingBlocks(store),
28
+ ])
29
+ let mst = await MST.create(memoryStore)
30
+ for (const record of records) {
31
+ mst = await mst.add(record.path, record.cid)
32
+ }
33
+ const newBlocks = new BlockMap()
34
+ for await (const node of mst.walk()) {
35
+ if (node.isTree()) {
36
+ const pointer = await node.getPointer()
37
+ if (!existingCids.has(pointer)) {
38
+ const serialized = await node.serialize()
39
+ newBlocks.set(serialized.cid, serialized.bytes)
40
+ }
41
+ }
42
+ }
43
+ const mstCids = await mst.allCids()
44
+ const toDelete = new CidSet(existingCids.toList()).subtractSet(mstCids)
45
+ const newCommit = await signCommit(
46
+ {
47
+ did,
48
+ version: 3,
49
+ rev,
50
+ prev: null,
51
+ data: await mst.getPointer(),
52
+ },
53
+ store.repo.signingKey,
54
+ )
55
+ const commitCid = await newBlocks.add(newCommit)
56
+
57
+ console.log('Record count: ', records.length)
58
+ console.log('Existing blocks: ', existingCids.toList().length)
59
+ console.log('Deleting blocks:', toDelete.toList().length)
60
+ console.log('Adding blocks: ', newBlocks.size)
61
+
62
+ const shouldContinue = await promptContinue()
63
+ if (!shouldContinue) {
64
+ throw new Error('Aborted')
65
+ }
66
+
67
+ await store.repo.storage.deleteMany(toDelete.toList())
68
+ await store.repo.storage.putMany(newBlocks, rev)
69
+ await store.repo.storage.updateRoot(commitCid, rev)
70
+ return {
71
+ cid: commitCid,
72
+ rev,
73
+ since: null,
74
+ prev: null,
75
+ newBlocks,
76
+ removedCids: toDelete,
77
+ }
78
+ })
79
+ await ctx.accountManager.updateRepoRoot(did, commit.cid, rev)
80
+ await ctx.sequencer.sequenceCommit(did, commit, [])
81
+ }
82
+
83
+ const listExistingBlocks = async (
84
+ store: ActorStoreTransactor,
85
+ ): Promise<CidSet> => {
86
+ const cids = new CidSet()
87
+ let cursor: string | undefined = ''
88
+ while (cursor !== undefined) {
89
+ const res = await store.db.db
90
+ .selectFrom('repo_block')
91
+ .select('cid')
92
+ .where('cid', '>', cursor)
93
+ .orderBy('cid', 'asc')
94
+ .limit(1000)
95
+ .execute()
96
+ for (const row of res) {
97
+ cids.add(CID.parse(row.cid))
98
+ }
99
+ cursor = res.at(-1)?.cid
100
+ }
101
+ return cids
102
+ }
103
+
104
+ const listAllRecords = async (
105
+ store: ActorStoreTransactor,
106
+ ): Promise<RecordDescript[]> => {
107
+ const records: RecordDescript[] = []
108
+ let cursor: string | undefined = ''
109
+ while (cursor !== undefined) {
110
+ const res = await store.db.db
111
+ .selectFrom('record')
112
+ .select(['uri', 'cid'])
113
+ .where('uri', '>', cursor)
114
+ .orderBy('uri', 'asc')
115
+ .limit(1000)
116
+ .execute()
117
+ for (const row of res) {
118
+ const parsed = new AtUri(row.uri)
119
+ records.push({
120
+ uri: row.uri,
121
+ path: formatDataKey(parsed.collection, parsed.rkey),
122
+ cid: CID.parse(row.cid),
123
+ })
124
+ }
125
+ cursor = res.at(-1)?.uri
126
+ }
127
+ return records
128
+ }
129
+
130
+ const promptContinue = async (): Promise<boolean> => {
131
+ const rl = readline.createInterface({
132
+ input: process.stdin,
133
+ output: process.stdout,
134
+ })
135
+ const answer = await rl.question('Continue? y/n ')
136
+ return answer === ''
137
+ }
138
+
139
+ type RecordDescript = {
140
+ uri: string
141
+ path: string
142
+ cid: CID
143
+ }
@@ -6,6 +6,7 @@ export type RepoSeqEventType =
6
6
  | 'handle'
7
7
  | 'migrate'
8
8
  | 'identity'
9
+ | 'account'
9
10
  | 'tombstone'
10
11
 
11
12
  export interface RepoSeq {
@@ -10,6 +10,7 @@ import {
10
10
  import { PreparedWrite } from '../repo'
11
11
  import { CID } from 'multiformats/cid'
12
12
  import { RepoSeqInsert } from './db'
13
+ import { AccountStatus } from '../account-manager'
13
14
 
14
15
  export const formatSeqCommit = async (
15
16
  did: string,
@@ -83,10 +84,14 @@ export const formatSeqHandleUpdate = async (
83
84
 
84
85
  export const formatSeqIdentityEvt = async (
85
86
  did: string,
87
+ handle?: string,
86
88
  ): Promise<RepoSeqInsert> => {
87
89
  const evt: IdentityEvt = {
88
90
  did,
89
91
  }
92
+ if (handle) {
93
+ evt.handle = handle
94
+ }
90
95
  return {
91
96
  did,
92
97
  eventType: 'identity',
@@ -95,6 +100,26 @@ export const formatSeqIdentityEvt = async (
95
100
  }
96
101
  }
97
102
 
103
+ export const formatSeqAccountEvt = async (
104
+ did: string,
105
+ status: AccountStatus,
106
+ ): Promise<RepoSeqInsert> => {
107
+ const evt: AccountEvt = {
108
+ did,
109
+ active: status === 'active',
110
+ }
111
+ if (status !== AccountStatus.Active) {
112
+ evt.status = status
113
+ }
114
+
115
+ return {
116
+ did,
117
+ eventType: 'account',
118
+ event: cborEncode(evt),
119
+ sequencedAt: new Date().toISOString(),
120
+ }
121
+ }
122
+
98
123
  export const formatSeqTombstone = async (
99
124
  did: string,
100
125
  ): Promise<RepoSeqInsert> => {
@@ -142,9 +167,24 @@ export type HandleEvt = z.infer<typeof handleEvt>
142
167
 
143
168
  export const identityEvt = z.object({
144
169
  did: z.string(),
170
+ handle: z.string().optional(),
145
171
  })
146
172
  export type IdentityEvt = z.infer<typeof identityEvt>
147
173
 
174
+ export const accountEvt = z.object({
175
+ did: z.string(),
176
+ active: z.boolean(),
177
+ status: z
178
+ .enum([
179
+ AccountStatus.Takendown,
180
+ AccountStatus.Suspended,
181
+ AccountStatus.Deleted,
182
+ AccountStatus.Deactivated,
183
+ ])
184
+ .optional(),
185
+ })
186
+ export type AccountEvt = z.infer<typeof accountEvt>
187
+
148
188
  export const tombstoneEvt = z.object({
149
189
  did: z.string(),
150
190
  })
@@ -168,6 +208,12 @@ type TypedIdentityEvt = {
168
208
  time: string
169
209
  evt: IdentityEvt
170
210
  }
211
+ type TypedAccountEvt = {
212
+ type: 'account'
213
+ seq: number
214
+ time: string
215
+ evt: AccountEvt
216
+ }
171
217
  type TypedTombstoneEvt = {
172
218
  type: 'tombstone'
173
219
  seq: number
@@ -178,4 +224,5 @@ export type SeqEvt =
178
224
  | TypedCommitEvt
179
225
  | TypedHandleEvt
180
226
  | TypedIdentityEvt
227
+ | TypedAccountEvt
181
228
  | TypedTombstoneEvt
@@ -4,11 +4,13 @@ import { seqLogger as log } from '../logger'
4
4
  import { SECOND, cborDecode, wait } from '@atproto/common'
5
5
  import { CommitData } from '@atproto/repo'
6
6
  import {
7
+ AccountEvt,
7
8
  CommitEvt,
8
9
  HandleEvt,
9
10
  IdentityEvt,
10
11
  SeqEvt,
11
12
  TombstoneEvt,
13
+ formatSeqAccountEvt,
12
14
  formatSeqCommit,
13
15
  formatSeqHandleUpdate,
14
16
  formatSeqIdentityEvt,
@@ -23,6 +25,7 @@ import {
23
25
  } from './db'
24
26
  import { PreparedWrite } from '../repo'
25
27
  import { Crawlers } from '../crawlers'
28
+ import { AccountStatus } from '../account-manager/helpers/account'
26
29
 
27
30
  export * from './events'
28
31
 
@@ -154,6 +157,13 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
154
157
  time: row.sequencedAt,
155
158
  evt: evt as IdentityEvt,
156
159
  })
160
+ } else if (row.eventType === 'account') {
161
+ seqEvts.push({
162
+ type: 'account',
163
+ seq: row.seq,
164
+ time: row.sequencedAt,
165
+ evt: evt as AccountEvt,
166
+ })
157
167
  } else if (row.eventType === 'tombstone') {
158
168
  seqEvts.push({
159
169
  type: 'tombstone',
@@ -197,43 +207,54 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
197
207
  await wait(waitTime)
198
208
  }
199
209
 
200
- async sequenceEvt(evt: RepoSeqInsert) {
201
- await this.db.executeWithRetry(
202
- this.db.db.insertInto('repo_seq').values(evt),
210
+ async sequenceEvt(evt: RepoSeqInsert): Promise<number> {
211
+ const res = await this.db.executeWithRetry(
212
+ this.db.db.insertInto('repo_seq').values(evt).returningAll(),
203
213
  )
204
214
  this.crawlers.notifyOfUpdate()
215
+ return res[0].seq
205
216
  }
206
217
 
207
218
  async sequenceCommit(
208
219
  did: string,
209
220
  commitData: CommitData,
210
221
  writes: PreparedWrite[],
211
- ) {
222
+ ): Promise<number> {
212
223
  const evt = await formatSeqCommit(did, commitData, writes)
213
- await this.sequenceEvt(evt)
224
+ return await this.sequenceEvt(evt)
214
225
  }
215
226
 
216
- async sequenceHandleUpdate(did: string, handle: string) {
227
+ async sequenceHandleUpdate(did: string, handle: string): Promise<number> {
217
228
  const evt = await formatSeqHandleUpdate(did, handle)
218
- await this.sequenceEvt(evt)
229
+ return await this.sequenceEvt(evt)
230
+ }
231
+
232
+ async sequenceIdentityEvt(did: string, handle?: string): Promise<number> {
233
+ const evt = await formatSeqIdentityEvt(did, handle)
234
+ return await this.sequenceEvt(evt)
219
235
  }
220
236
 
221
- async sequenceIdentityEvt(did: string) {
222
- const evt = await formatSeqIdentityEvt(did)
223
- await this.sequenceEvt(evt)
237
+ async sequenceAccountEvt(
238
+ did: string,
239
+ status: AccountStatus,
240
+ ): Promise<number> {
241
+ const evt = await formatSeqAccountEvt(did, status)
242
+ return await this.sequenceEvt(evt)
224
243
  }
225
244
 
226
- async sequenceTombstone(did: string) {
245
+ async sequenceTombstone(did: string): Promise<number> {
227
246
  const evt = await formatSeqTombstone(did)
228
- await this.sequenceEvt(evt)
247
+ return await this.sequenceEvt(evt)
229
248
  }
230
249
 
231
- async deleteAllForUser(did: string) {
250
+ async deleteAllForUser(did: string, excludingSeqs: number[] = []) {
232
251
  await this.db.executeWithRetry(
233
252
  this.db.db
234
253
  .deleteFrom('repo_seq')
235
254
  .where('did', '=', did)
236
- .where('eventType', '!=', 'tombstone'),
255
+ .if(excludingSeqs.length > 0, (qb) =>
256
+ qb.where('seq', 'not in', excludingSeqs),
257
+ ),
237
258
  )
238
259
  }
239
260
  }
@@ -1,5 +1,10 @@
1
1
  import AtpAgent from '@atproto/api'
2
- import { SeedClient, TestNetworkNoAppView, basicSeed } from '@atproto/dev-env'
2
+ import {
3
+ ImageRef,
4
+ SeedClient,
5
+ TestNetworkNoAppView,
6
+ basicSeed,
7
+ } from '@atproto/dev-env'
3
8
 
4
9
  describe('account deactivation', () => {
5
10
  let network: TestNetworkNoAppView
@@ -8,6 +13,7 @@ describe('account deactivation', () => {
8
13
  let agent: AtpAgent
9
14
 
10
15
  let alice: string
16
+ let aliceAvatar: ImageRef
11
17
 
12
18
  beforeAll(async () => {
13
19
  network = await TestNetworkNoAppView.create({
@@ -19,6 +25,16 @@ describe('account deactivation', () => {
19
25
 
20
26
  await basicSeed(sc)
21
27
  alice = sc.dids.alice
28
+
29
+ aliceAvatar = await sc.uploadFile(
30
+ alice,
31
+ '../dev-env/src/seed/img/key-portrait-small.jpg',
32
+ 'image/jpeg',
33
+ )
34
+ await sc.updateProfile(alice, {
35
+ avatar: aliceAvatar.image,
36
+ })
37
+
22
38
  await network.processAll()
23
39
  })
24
40
 
@@ -33,16 +49,25 @@ describe('account deactivation', () => {
33
49
  )
34
50
  })
35
51
 
52
+ it('returns deactivated status', async () => {
53
+ const res = await agent.com.atproto.sync.getRepoStatus({ did: alice })
54
+ expect(res.data).toEqual({
55
+ did: alice,
56
+ active: false,
57
+ status: 'deactivated',
58
+ })
59
+ })
60
+
36
61
  it('no longer serves repo data', async () => {
37
62
  await expect(
38
63
  agent.com.atproto.sync.getRepo({ did: alice }),
39
- ).rejects.toThrow()
64
+ ).rejects.toThrow(/Repo has been deactivated/)
40
65
  await expect(
41
66
  agent.com.atproto.sync.getLatestCommit({ did: alice }),
42
- ).rejects.toThrow()
67
+ ).rejects.toThrow(/Repo has been deactivated/)
43
68
  await expect(
44
69
  agent.com.atproto.sync.listBlobs({ did: alice }),
45
- ).rejects.toThrow()
70
+ ).rejects.toThrow(/Repo has been deactivated/)
46
71
  const recordUri = sc.posts[alice][0].ref.uri
47
72
  await expect(
48
73
  agent.com.atproto.sync.getRecord({
@@ -50,7 +75,7 @@ describe('account deactivation', () => {
50
75
  collection: recordUri.collection,
51
76
  rkey: recordUri.rkey,
52
77
  }),
53
- ).rejects.toThrow()
78
+ ).rejects.toThrow(/Repo has been deactivated/)
54
79
  await expect(
55
80
  agent.com.atproto.repo.getRecord({
56
81
  repo: alice,
@@ -62,17 +87,18 @@ describe('account deactivation', () => {
62
87
  agent.com.atproto.repo.describeRepo({
63
88
  repo: alice,
64
89
  }),
65
- ).rejects.toThrow()
90
+ ).rejects.toThrow(/Repo has been deactivated/)
66
91
 
67
- const blobCid = sc.profiles[alice].avatar.cid
68
92
  await expect(
69
93
  agent.com.atproto.sync.getBlob({
70
94
  did: alice,
71
- cid: blobCid,
95
+ cid: aliceAvatar.image.ref.toString(),
72
96
  }),
73
- ).rejects.toThrow()
97
+ ).rejects.toThrow(/Repo has been deactivated/)
74
98
  const listedRepos = await agent.com.atproto.sync.listRepos()
75
- expect(listedRepos.data.repos.find((r) => r.did === alice)).toBeUndefined()
99
+ const listedAlice = listedRepos.data.repos.find((r) => r.did === alice)
100
+ expect(listedAlice?.active).toBe(false)
101
+ expect(listedAlice?.status).toBe('deactivated')
76
102
  })
77
103
 
78
104
  it('no longer resolves handle', async () => {
@@ -143,10 +143,18 @@ describe('account deletion', () => {
143
143
  expect(updatedDbContents.userAccounts).toEqual(
144
144
  initialDbContents.userAccounts.filter((row) => row.did !== carol.did),
145
145
  )
146
- // check all seqs for this did are gone, except for the tombstone
146
+ // check we didn't touch other user seqs
147
147
  expect(
148
- updatedDbContents.repoSeqs.filter((row) => row.eventType !== 'tombstone'),
148
+ updatedDbContents.repoSeqs.filter((row) => row.did !== carol.did),
149
149
  ).toEqual(initialDbContents.repoSeqs.filter((row) => row.did !== carol.did))
150
+ // check all seqs for this did are gone, except for the tombstone & account events
151
+ expect(
152
+ updatedDbContents.repoSeqs
153
+ .filter((row) => row.did === carol.did)
154
+ .every(
155
+ (row) => row.eventType === 'tombstone' || row.eventType === 'account',
156
+ ),
157
+ ).toBe(true)
150
158
  // check we do have a tombstone for this did
151
159
  expect(
152
160
  updatedDbContents.repoSeqs.filter(
@@ -238,12 +238,12 @@ describe('moderation', () => {
238
238
  }
239
239
  // public, disallow
240
240
  const attempt1 = agent.api.com.atproto.sync.getBlob(blobParams)
241
- await expect(attempt1).rejects.toThrow('Blob not found')
241
+ await expect(attempt1).rejects.toThrow(/Repo has been takendown/)
242
242
  // logged-in, disallow
243
243
  const attempt2 = agent.api.com.atproto.sync.getBlob(blobParams, {
244
244
  headers: sc.getHeaders(sc.dids.bob),
245
245
  })
246
- await expect(attempt2).rejects.toThrow('Blob not found')
246
+ await expect(attempt2).rejects.toThrow(/Repo has been takendown/)
247
247
  // logged-in as account, allow
248
248
  const res1 = await agent.api.com.atproto.sync.getBlob(blobParams, {
249
249
  headers: sc.getHeaders(sc.dids.carol),
@@ -37,6 +37,7 @@ describe('notif service proxy', () => {
37
37
  }
38
38
  return x
39
39
  })
40
+ await network.pds.ctx.idResolver.did.resolve(notifDid, true)
40
41
  })
41
42
 
42
43
  afterAll(async () => {
@@ -25,8 +25,8 @@ describe('sequencer', () => {
25
25
  await userSeed(sc)
26
26
  alice = sc.dids.alice
27
27
  bob = sc.dids.bob
28
- // 10 events in userSeed
29
- totalEvts = 10
28
+ // 14 events in userSeed
29
+ totalEvts = 14
30
30
  })
31
31
 
32
32
  afterAll(async () => {
@@ -29,6 +29,7 @@ describe('sync listing', () => {
29
29
  sc.dids.carol,
30
30
  sc.dids.dan,
31
31
  ])
32
+ expect(res.data.repos.every((r) => r.active === true)).toBe(true)
32
33
  })
33
34
 
34
35
  it('paginates listed hosted repos', async () => {