@atproto/dev-env 0.0.0-spaces-alpha-20260818163953

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 (187) hide show
  1. package/CHANGELOG.md +3424 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +39 -0
  4. package/bin.js +7 -0
  5. package/dist/bin-multi-pds.d.ts +2 -0
  6. package/dist/bin-multi-pds.d.ts.map +1 -0
  7. package/dist/bin-multi-pds.js +130 -0
  8. package/dist/bin-multi-pds.js.map +1 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.d.ts.map +1 -0
  11. package/dist/bin.js +56 -0
  12. package/dist/bin.js.map +1 -0
  13. package/dist/bsky.d.ts +27 -0
  14. package/dist/bsky.d.ts.map +1 -0
  15. package/dist/bsky.js +165 -0
  16. package/dist/bsky.js.map +1 -0
  17. package/dist/bsync.d.ts +22 -0
  18. package/dist/bsync.d.ts.map +1 -0
  19. package/dist/bsync.js +54 -0
  20. package/dist/bsync.js.map +1 -0
  21. package/dist/const.d.ts +4 -0
  22. package/dist/const.d.ts.map +1 -0
  23. package/dist/const.js +4 -0
  24. package/dist/const.js.map +1 -0
  25. package/dist/env.d.ts +2 -0
  26. package/dist/env.d.ts.map +1 -0
  27. package/dist/env.js +10 -0
  28. package/dist/env.js.map +1 -0
  29. package/dist/feed-gen.d.ts +15 -0
  30. package/dist/feed-gen.d.ts.map +1 -0
  31. package/dist/feed-gen.js +73 -0
  32. package/dist/feed-gen.js.map +1 -0
  33. package/dist/index.d.ts +17 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +17 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/introspect.d.ts +22 -0
  38. package/dist/introspect.d.ts.map +1 -0
  39. package/dist/introspect.js +65 -0
  40. package/dist/introspect.js.map +1 -0
  41. package/dist/mock/data.d.ts +3 -0
  42. package/dist/mock/data.d.ts.map +1 -0
  43. package/dist/mock/data.js +46 -0
  44. package/dist/mock/data.js.map +1 -0
  45. package/dist/mock/img/blur-hash-avatar-b64.d.ts +3 -0
  46. package/dist/mock/img/blur-hash-avatar-b64.d.ts.map +1 -0
  47. package/dist/mock/img/blur-hash-avatar-b64.js +2 -0
  48. package/dist/mock/img/blur-hash-avatar-b64.js.map +1 -0
  49. package/dist/mock/img/labeled-img-b64.d.ts +3 -0
  50. package/dist/mock/img/labeled-img-b64.d.ts.map +1 -0
  51. package/dist/mock/img/labeled-img-b64.js +2 -0
  52. package/dist/mock/img/labeled-img-b64.js.map +1 -0
  53. package/dist/mock/index.d.ts +3 -0
  54. package/dist/mock/index.d.ts.map +1 -0
  55. package/dist/mock/index.js +574 -0
  56. package/dist/mock/index.js.map +1 -0
  57. package/dist/moderator-client.d.ts +58 -0
  58. package/dist/moderator-client.d.ts.map +1 -0
  59. package/dist/moderator-client.js +110 -0
  60. package/dist/moderator-client.js.map +1 -0
  61. package/dist/network-no-appview.d.ts +21 -0
  62. package/dist/network-no-appview.d.ts.map +1 -0
  63. package/dist/network-no-appview.js +74 -0
  64. package/dist/network-no-appview.js.map +1 -0
  65. package/dist/network.d.ts +32 -0
  66. package/dist/network.d.ts.map +1 -0
  67. package/dist/network.js +190 -0
  68. package/dist/network.js.map +1 -0
  69. package/dist/ozone.d.ts +31 -0
  70. package/dist/ozone.d.ts.map +1 -0
  71. package/dist/ozone.js +254 -0
  72. package/dist/ozone.js.map +1 -0
  73. package/dist/pds.d.ts +23 -0
  74. package/dist/pds.d.ts.map +1 -0
  75. package/dist/pds.js +97 -0
  76. package/dist/pds.js.map +1 -0
  77. package/dist/plc.d.ts +14 -0
  78. package/dist/plc.d.ts.map +1 -0
  79. package/dist/plc.js +28 -0
  80. package/dist/plc.js.map +1 -0
  81. package/dist/seed/author-feed.d.ts +4 -0
  82. package/dist/seed/author-feed.d.ts.map +1 -0
  83. package/dist/seed/author-feed.js +50 -0
  84. package/dist/seed/author-feed.js.map +1 -0
  85. package/dist/seed/basic.d.ts +16 -0
  86. package/dist/seed/basic.d.ts.map +1 -0
  87. package/dist/seed/basic.js +113 -0
  88. package/dist/seed/basic.js.map +1 -0
  89. package/dist/seed/client.d.ts +151 -0
  90. package/dist/seed/client.d.ts.map +1 -0
  91. package/dist/seed/client.js +365 -0
  92. package/dist/seed/client.js.map +1 -0
  93. package/dist/seed/follows.d.ts +4 -0
  94. package/dist/seed/follows.d.ts.map +1 -0
  95. package/dist/seed/follows.js +55 -0
  96. package/dist/seed/follows.js.map +1 -0
  97. package/dist/seed/index.d.ts +11 -0
  98. package/dist/seed/index.d.ts.map +1 -0
  99. package/dist/seed/index.js +11 -0
  100. package/dist/seed/index.js.map +1 -0
  101. package/dist/seed/likes.d.ts +4 -0
  102. package/dist/seed/likes.d.ts.map +1 -0
  103. package/dist/seed/likes.js +37 -0
  104. package/dist/seed/likes.js.map +1 -0
  105. package/dist/seed/quotes.d.ts +4 -0
  106. package/dist/seed/quotes.d.ts.map +1 -0
  107. package/dist/seed/quotes.js +21 -0
  108. package/dist/seed/quotes.js.map +1 -0
  109. package/dist/seed/reposts.d.ts +4 -0
  110. package/dist/seed/reposts.d.ts.map +1 -0
  111. package/dist/seed/reposts.js +17 -0
  112. package/dist/seed/reposts.js.map +1 -0
  113. package/dist/seed/thread-v2.d.ts +227 -0
  114. package/dist/seed/thread-v2.d.ts.map +1 -0
  115. package/dist/seed/thread-v2.js +755 -0
  116. package/dist/seed/thread-v2.js.map +1 -0
  117. package/dist/seed/users-bulk.d.ts +3 -0
  118. package/dist/seed/users-bulk.d.ts.map +1 -0
  119. package/dist/seed/users-bulk.js +224 -0
  120. package/dist/seed/users-bulk.js.map +1 -0
  121. package/dist/seed/users.d.ts +3 -0
  122. package/dist/seed/users.d.ts.map +1 -0
  123. package/dist/seed/users.js +45 -0
  124. package/dist/seed/users.js.map +1 -0
  125. package/dist/seed/verifications.d.ts +5 -0
  126. package/dist/seed/verifications.d.ts.map +1 -0
  127. package/dist/seed/verifications.js +164 -0
  128. package/dist/seed/verifications.js.map +1 -0
  129. package/dist/service-profile-lexicon.d.ts +11 -0
  130. package/dist/service-profile-lexicon.d.ts.map +1 -0
  131. package/dist/service-profile-lexicon.js +115 -0
  132. package/dist/service-profile-lexicon.js.map +1 -0
  133. package/dist/service-profile-ozone.d.ts +18 -0
  134. package/dist/service-profile-ozone.d.ts.map +1 -0
  135. package/dist/service-profile-ozone.js +333 -0
  136. package/dist/service-profile-ozone.js.map +1 -0
  137. package/dist/service-profile.d.ts +24 -0
  138. package/dist/service-profile.d.ts.map +1 -0
  139. package/dist/service-profile.js +61 -0
  140. package/dist/service-profile.js.map +1 -0
  141. package/dist/types.d.ts +61 -0
  142. package/dist/types.d.ts.map +1 -0
  143. package/dist/types.js +2 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/util.d.ts +14 -0
  146. package/dist/util.d.ts.map +1 -0
  147. package/dist/util.js +86 -0
  148. package/dist/util.js.map +1 -0
  149. package/package.json +69 -0
  150. package/src/bin-multi-pds.ts +166 -0
  151. package/src/bin.ts +64 -0
  152. package/src/bsky.ts +198 -0
  153. package/src/bsync.ts +66 -0
  154. package/src/const.ts +3 -0
  155. package/src/env.ts +9 -0
  156. package/src/feed-gen.ts +96 -0
  157. package/src/index.ts +17 -0
  158. package/src/introspect.ts +91 -0
  159. package/src/mock/data.ts +46 -0
  160. package/src/mock/img/blur-hash-avatar-b64.ts +1 -0
  161. package/src/mock/img/labeled-img-b64.ts +1 -0
  162. package/src/mock/index.ts +702 -0
  163. package/src/moderator-client.ts +267 -0
  164. package/src/network-no-appview.ts +89 -0
  165. package/src/network.ts +243 -0
  166. package/src/ozone.ts +287 -0
  167. package/src/pds.ts +118 -0
  168. package/src/plc.ts +33 -0
  169. package/src/seed/author-feed.ts +98 -0
  170. package/src/seed/basic.ts +189 -0
  171. package/src/seed/client.ts +676 -0
  172. package/src/seed/follows.ts +57 -0
  173. package/src/seed/index.ts +11 -0
  174. package/src/seed/likes.ts +43 -0
  175. package/src/seed/quotes.ts +53 -0
  176. package/src/seed/reposts.ts +18 -0
  177. package/src/seed/thread-v2.ts +925 -0
  178. package/src/seed/users-bulk.ts +229 -0
  179. package/src/seed/users.ts +64 -0
  180. package/src/seed/verifications.ts +254 -0
  181. package/src/service-profile-lexicon.ts +127 -0
  182. package/src/service-profile-ozone.ts +375 -0
  183. package/src/service-profile.ts +95 -0
  184. package/src/types.ts +68 -0
  185. package/src/util.ts +118 -0
  186. package/tsconfig.build.json +25 -0
  187. package/tsconfig.json +4 -0
@@ -0,0 +1,267 @@
1
+ import {
2
+ type $Typed,
3
+ type AtpAgent,
4
+ type ChatBskyConvoDefs,
5
+ type ComAtprotoAdminDefs,
6
+ type ComAtprotoRepoStrongRef,
7
+ type ToolsOzoneModerationDefs,
8
+ type ToolsOzoneModerationEmitEvent as EmitModerationEvent,
9
+ ToolsOzoneModerationGetReporterStats as _GetReporterStats, // includes types for getReporterStats()
10
+ type ToolsOzoneModerationQueryEvents as QueryModerationEvents,
11
+ type ToolsOzoneModerationQueryStatuses as QueryModerationStatuses,
12
+ type ToolsOzoneReportQueryReports as QueryModerationReports,
13
+ type ToolsOzoneSettingRemoveOptions,
14
+ type ToolsOzoneSettingUpsertOption,
15
+ } from '@atproto/api'
16
+ import type { TestOzone } from './ozone.js'
17
+
18
+ type TakeActionInput = EmitModerationEvent.InputSchema
19
+ type QueryStatusesParams = QueryModerationStatuses.QueryParams
20
+ type QueryEventsParams = QueryModerationEvents.QueryParams
21
+ type QueryReportsParams = QueryModerationReports.QueryParams
22
+ type ModLevel = 'admin' | 'moderator' | 'triage'
23
+
24
+ export class ModeratorClient {
25
+ agent: AtpAgent
26
+ constructor(public ozone: TestOzone) {
27
+ this.agent = ozone.getAgent()
28
+ }
29
+
30
+ async getEvent(id: number, role?: ModLevel) {
31
+ const result = await this.agent.tools.ozone.moderation.getEvent(
32
+ { id },
33
+ {
34
+ headers: await this.ozone.modHeaders(
35
+ 'tools.ozone.moderation.getEvent',
36
+ role,
37
+ ),
38
+ },
39
+ )
40
+ return result.data
41
+ }
42
+
43
+ async queryStatuses(input: QueryStatusesParams, role?: ModLevel) {
44
+ const result = await this.agent.tools.ozone.moderation.queryStatuses(
45
+ input,
46
+ {
47
+ headers: await this.ozone.modHeaders(
48
+ 'tools.ozone.moderation.queryStatuses',
49
+ role,
50
+ ),
51
+ },
52
+ )
53
+ return result.data
54
+ }
55
+
56
+ async getReporterStats(dids: string[]) {
57
+ const result = await this.agent.tools.ozone.moderation.getReporterStats(
58
+ { dids },
59
+ {
60
+ headers: await this.ozone.modHeaders(
61
+ 'tools.ozone.moderation.getReporterStats',
62
+ 'admin',
63
+ ),
64
+ },
65
+ )
66
+ return result.data
67
+ }
68
+
69
+ async queryEvents(input: QueryEventsParams, role?: ModLevel) {
70
+ const result = await this.agent.tools.ozone.moderation.queryEvents(input, {
71
+ headers: await this.ozone.modHeaders(
72
+ 'tools.ozone.moderation.queryEvents',
73
+ role,
74
+ ),
75
+ })
76
+ return result.data
77
+ }
78
+
79
+ async queryReports(input: QueryReportsParams, role?: ModLevel) {
80
+ const result = await this.agent.tools.ozone.report.queryReports(input, {
81
+ headers: await this.ozone.modHeaders(
82
+ 'tools.ozone.report.queryReports',
83
+ role,
84
+ ),
85
+ })
86
+ return result.data
87
+ }
88
+
89
+ async emitEvent(
90
+ opts: {
91
+ event: TakeActionInput['event']
92
+ subject:
93
+ | $Typed<ComAtprotoAdminDefs.RepoRef>
94
+ | $Typed<ComAtprotoRepoStrongRef.Main>
95
+ | $Typed<ChatBskyConvoDefs.MessageRef>
96
+ | $Typed<ChatBskyConvoDefs.ConvoRef>
97
+ | { $type: string }
98
+ subjectBlobCids?: TakeActionInput['subjectBlobCids']
99
+ reason?: string
100
+ createdBy?: string
101
+ meta?: unknown
102
+ modTool?: ToolsOzoneModerationDefs.ModTool
103
+ externalId?: string
104
+ reportAction?: TakeActionInput['reportAction']
105
+ },
106
+ role?: ModLevel,
107
+ ) {
108
+ const {
109
+ event,
110
+ subject,
111
+ subjectBlobCids,
112
+ createdBy = 'did:example:admin',
113
+ modTool,
114
+ externalId,
115
+ reportAction,
116
+ } = opts
117
+ const result = await this.agent.tools.ozone.moderation.emitEvent(
118
+ {
119
+ event,
120
+ subject,
121
+ subjectBlobCids,
122
+ createdBy,
123
+ modTool,
124
+ externalId,
125
+ reportAction,
126
+ },
127
+ {
128
+ encoding: 'application/json',
129
+ headers: await this.ozone.modHeaders(
130
+ 'tools.ozone.moderation.emitEvent',
131
+ role,
132
+ ),
133
+ },
134
+ )
135
+ return result.data
136
+ }
137
+
138
+ async reverseAction(
139
+ opts: {
140
+ id: number
141
+ subject: TakeActionInput['subject']
142
+ reason?: string
143
+ createdBy?: string
144
+ modTool?: ToolsOzoneModerationDefs.ModTool
145
+ },
146
+ role?: ModLevel,
147
+ ) {
148
+ const {
149
+ subject,
150
+ reason = 'X',
151
+ createdBy = 'did:example:admin',
152
+ modTool,
153
+ } = opts
154
+ const result = await this.agent.tools.ozone.moderation.emitEvent(
155
+ {
156
+ subject,
157
+ event: {
158
+ $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',
159
+ comment: reason,
160
+ },
161
+ createdBy,
162
+ modTool,
163
+ },
164
+ {
165
+ encoding: 'application/json',
166
+ headers: await this.ozone.modHeaders(
167
+ 'tools.ozone.moderation.emitEvent',
168
+ role,
169
+ ),
170
+ },
171
+ )
172
+ return result.data
173
+ }
174
+
175
+ async performTakedown(
176
+ opts: {
177
+ subject:
178
+ | $Typed<ComAtprotoAdminDefs.RepoRef>
179
+ | $Typed<ComAtprotoRepoStrongRef.Main>
180
+ | $Typed<ChatBskyConvoDefs.MessageRef>
181
+ | $Typed<ChatBskyConvoDefs.ConvoRef>
182
+ | { $type: string }
183
+ subjectBlobCids?: TakeActionInput['subjectBlobCids']
184
+ durationInHours?: number
185
+ acknowledgeAccountSubjects?: boolean
186
+ reason?: string
187
+ policies?: string[]
188
+ },
189
+ role?: ModLevel,
190
+ ) {
191
+ const { durationInHours, acknowledgeAccountSubjects, policies, ...rest } =
192
+ opts
193
+ return this.emitEvent(
194
+ {
195
+ event: {
196
+ $type: 'tools.ozone.moderation.defs#modEventTakedown',
197
+ acknowledgeAccountSubjects,
198
+ durationInHours,
199
+ policies,
200
+ },
201
+ ...rest,
202
+ },
203
+ role,
204
+ )
205
+ }
206
+
207
+ async performReverseTakedown(
208
+ opts: {
209
+ subject: TakeActionInput['subject']
210
+ subjectBlobCids?: TakeActionInput['subjectBlobCids']
211
+ reason?: string
212
+ },
213
+ role?: ModLevel,
214
+ ) {
215
+ return this.emitEvent(
216
+ {
217
+ event: {
218
+ $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',
219
+ },
220
+ ...opts,
221
+ },
222
+ role,
223
+ )
224
+ }
225
+
226
+ async upsertSettingOption(
227
+ setting: ToolsOzoneSettingUpsertOption.InputSchema,
228
+ callerRole: 'admin' | 'moderator' | 'triage' = 'admin',
229
+ ) {
230
+ const { data } = await this.agent.tools.ozone.setting.upsertOption(
231
+ setting,
232
+ {
233
+ encoding: 'application/json',
234
+ headers: await this.ozone.modHeaders(
235
+ 'tools.ozone.setting.upsertOption',
236
+ callerRole,
237
+ ),
238
+ },
239
+ )
240
+
241
+ return data
242
+ }
243
+
244
+ async removeSettingOptions(
245
+ params: ToolsOzoneSettingRemoveOptions.InputSchema,
246
+ callerRole: 'admin' | 'moderator' | 'triage' = 'admin',
247
+ ) {
248
+ const { data } = await this.agent.tools.ozone.setting.removeOptions(
249
+ params,
250
+ {
251
+ encoding: 'application/json',
252
+ headers: await this.ozone.modHeaders(
253
+ 'tools.ozone.setting.removeOptions',
254
+ callerRole,
255
+ ),
256
+ },
257
+ )
258
+
259
+ return data
260
+ }
261
+
262
+ async computeStats() {
263
+ const db = this.ozone.ctx.db
264
+ const statsService = this.ozone.ctx.reportStatsService(db)
265
+ await statsService.materializeAll({ force: true })
266
+ }
267
+ }
@@ -0,0 +1,89 @@
1
+ import getPort from 'get-port'
2
+ import type { SkeletonHandler } from '@atproto/pds'
3
+ import { TestFeedGen } from './feed-gen.js'
4
+ import { TestPds } from './pds.js'
5
+ import { TestPlc } from './plc.js'
6
+ import { SeedClient } from './seed/client.js'
7
+ import { LexiconAuthorityProfile } from './service-profile-lexicon.js'
8
+ import type { TestServerParams } from './types.js'
9
+ import { mockNetworkUtilities } from './util.js'
10
+
11
+ export class TestNetworkNoAppView {
12
+ feedGens: TestFeedGen[] = []
13
+ constructor(
14
+ public plc: TestPlc,
15
+ public pds: TestPds,
16
+ public extraPdses: TestPds[] = [],
17
+ public lexiconAuthority?: LexiconAuthorityProfile,
18
+ ) {}
19
+
20
+ static async create(
21
+ params: Partial<TestServerParams> = {},
22
+ ): Promise<TestNetworkNoAppView> {
23
+ const plc = await TestPlc.create(params.plc ?? {})
24
+ const pds = await TestPds.create({
25
+ didPlcUrl: plc.url,
26
+ ...params.pds,
27
+ })
28
+
29
+ const extraPdsCount = params.extraPdses ?? 0
30
+ const extraPdses: TestPds[] = []
31
+ for (let i = 0; i < extraPdsCount; i++) {
32
+ // Extra PDSes get their own non-overlapping handle domain (.test2, .test3, ...)
33
+ // to avoid colliding with the primary PDS's .test.
34
+ const domain = `.test${i + 2}`
35
+ const extra = await TestPds.create({
36
+ didPlcUrl: plc.url,
37
+ ...params.pds,
38
+ // Override after spreading so each extra PDS gets a unique port and
39
+ // its own handle domain (rather than inheriting the primary's).
40
+ port: await getPort(),
41
+ serviceHandleDomains: [domain],
42
+ })
43
+ extraPdses.push(extra)
44
+ }
45
+
46
+ mockNetworkUtilities([pds, ...extraPdses])
47
+
48
+ let lexiconAuthority: LexiconAuthorityProfile | undefined
49
+ if (params.lexiconAuthority) {
50
+ // The authority account has to live on a PDS, but a PDS wants the authority
51
+ // DID in its config — so it's created after boot and the DID assigned here.
52
+ // The resolver hook reads `cfg.lexicon.didAuthority` per call rather than at
53
+ // construction (see pds `context.ts`), so assigning it now takes effect.
54
+ lexiconAuthority = await LexiconAuthorityProfile.create(pds)
55
+ for (const each of [pds, ...extraPdses]) {
56
+ each.ctx.cfg.lexicon.didAuthority = lexiconAuthority.did
57
+ }
58
+ await lexiconAuthority.createRecords()
59
+ }
60
+
61
+ return new TestNetworkNoAppView(plc, pds, extraPdses, lexiconAuthority)
62
+ }
63
+
64
+ async createFeedGen(
65
+ feeds: Record<string, SkeletonHandler>,
66
+ ): Promise<TestFeedGen> {
67
+ const fg = await TestFeedGen.create(this.plc.url, feeds)
68
+ this.feedGens.push(fg)
69
+ return fg
70
+ }
71
+
72
+ getSeedClient(): SeedClient<typeof this> {
73
+ const agent = this.pds.getAgent()
74
+ const client = this.pds.getClient()
75
+ return new SeedClient(this, agent, client)
76
+ }
77
+
78
+ async processAll() {
79
+ await this.pds.processAll()
80
+ await Promise.all(this.extraPdses.map((p) => p.processAll()))
81
+ }
82
+
83
+ async close() {
84
+ await Promise.all(this.feedGens.map((fg) => fg.close()))
85
+ await this.pds.close()
86
+ await Promise.all(this.extraPdses.map((p) => p.close()))
87
+ await this.plc.close()
88
+ }
89
+ }
package/src/network.ts ADDED
@@ -0,0 +1,243 @@
1
+ import assert from 'node:assert'
2
+ import getPort from 'get-port'
3
+ import * as uint8arrays from 'uint8arrays'
4
+ import { allFulfilled, wait } from '@atproto/common-web'
5
+ import { createServiceJwt } from '@atproto/xrpc-server'
6
+ import { TestBsky } from './bsky.js'
7
+ import { TestBsync } from './bsync.js'
8
+ import { EXAMPLE_LABELER } from './const.js'
9
+ import { IntrospectServer } from './introspect.js'
10
+ import { TestNetworkNoAppView } from './network-no-appview.js'
11
+ import { TestOzone } from './ozone.js'
12
+ import { TestPds } from './pds.js'
13
+ import { TestPlc } from './plc.js'
14
+ import { LexiconAuthorityProfile } from './service-profile-lexicon.js'
15
+ import { OzoneServiceProfile } from './service-profile-ozone.js'
16
+ import type { TestServerParams } from './types.js'
17
+ import { mockNetworkUtilities } from './util.js'
18
+
19
+ const ADMIN_USERNAME = 'admin'
20
+ const ADMIN_PASSWORD = 'admin-pass'
21
+
22
+ export class TestNetwork extends TestNetworkNoAppView {
23
+ constructor(
24
+ public plc: TestPlc,
25
+ public pds: TestPds,
26
+ public bsync: TestBsync,
27
+ public bsky: TestBsky,
28
+ public ozone: TestOzone,
29
+ public introspect?: IntrospectServer,
30
+ ) {
31
+ super(plc, pds)
32
+ }
33
+
34
+ static async create(
35
+ params: Partial<TestServerParams> = {},
36
+ ): Promise<TestNetwork> {
37
+ const redisHost = process.env.REDIS_HOST
38
+ const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL
39
+ assert(dbPostgresUrl, 'Missing postgres url for tests')
40
+ assert(redisHost, 'Missing redis host for tests')
41
+ const dbPostgresSchema =
42
+ params.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA
43
+
44
+ const plc = await TestPlc.create(params.plc ?? {})
45
+
46
+ const bskyPort = params.bsky?.port ?? (await getPort())
47
+ const pdsPort = params.pds?.port ?? (await getPort())
48
+ const ozonePort = params.ozone?.port ?? (await getPort())
49
+
50
+ const thirdPartyPds = await TestPds.create({
51
+ didPlcUrl: plc.url,
52
+ ...params.pds,
53
+ inviteRequired: false,
54
+ port: await getPort(),
55
+ })
56
+
57
+ const ozoneUrl = `http://localhost:${ozonePort}`
58
+
59
+ // @TODO (?) rework the ServiceProfile to live on a separate PDS instead of
60
+ // requiring to migrate to the main PDS
61
+ const ozoneServiceProfile = await OzoneServiceProfile.create(
62
+ thirdPartyPds,
63
+ ozoneUrl,
64
+ )
65
+ const lexiconAuthorityProfile =
66
+ await LexiconAuthorityProfile.create(thirdPartyPds)
67
+
68
+ const bsyncApiKey = 'bsync-api-key'
69
+ const bsync = await TestBsync.create({
70
+ apiKeys: [bsyncApiKey],
71
+ dbUrl: dbPostgresUrl,
72
+ })
73
+
74
+ const bsky = await TestBsky.create({
75
+ port: bskyPort,
76
+ plcUrl: plc.url,
77
+ pdsPort,
78
+ bsyncApiKey,
79
+ bsyncUrl: bsync.url,
80
+ rolodexUrl: process.env.BSKY_ROLODEX_URL,
81
+ rolodexIgnoreBadTls: true,
82
+ repoProvider: `ws://localhost:${pdsPort}`,
83
+ dbPostgresSchema: `appview_${dbPostgresSchema}`,
84
+ dbPostgresUrl,
85
+ redisHost,
86
+ modServiceDid: ozoneServiceProfile.did,
87
+ labelsFromIssuerDids: [ozoneServiceProfile.did, EXAMPLE_LABELER],
88
+ // Using a static private key results in a static DID, which is useful for e2e tests with the social-app repo.
89
+ privateKey:
90
+ '3f916c70dc69e4c5e83877f013325b11ecac31742e6a42f5c4fb240d0703d9d5=',
91
+ ...params.bsky,
92
+ })
93
+
94
+ const pds = await TestPds.create({
95
+ port: pdsPort,
96
+ didPlcUrl: plc.url,
97
+ bskyAppViewUrl: bsky.url,
98
+ bskyAppViewDid: bsky.ctx.cfg.serverDid,
99
+ modServiceUrl: ozoneUrl,
100
+ modServiceDid: ozoneServiceProfile.did,
101
+ lexiconDidAuthority: lexiconAuthorityProfile.did,
102
+ ...params.pds,
103
+ })
104
+
105
+ // mock before any events start flowing from pds so that we don't miss e.g. any handle resolutions.
106
+ mockNetworkUtilities(pds, bsky)
107
+
108
+ const ozone = await TestOzone.create({
109
+ port: ozonePort,
110
+ plcUrl: plc.url,
111
+ signingKey: ozoneServiceProfile.key,
112
+ serverDid: ozoneServiceProfile.did,
113
+ dbPostgresSchema: `ozone_${dbPostgresSchema || 'db'}`,
114
+ dbPostgresUrl,
115
+ appviewUrl: bsky.url,
116
+ appviewDid: bsky.ctx.cfg.serverDid,
117
+ appviewPushEvents: true,
118
+ pdsUrl: pds.url,
119
+ pdsDid: pds.ctx.cfg.service.did,
120
+ verifierDid: ozoneServiceProfile.did,
121
+ verifierUrl: pds.url,
122
+ verifierPassword: 'temp',
123
+ ...params.ozone,
124
+ })
125
+
126
+ await ozoneServiceProfile.migrateTo(pds)
127
+ await ozoneServiceProfile.createRecords()
128
+
129
+ await lexiconAuthorityProfile.migrateTo(pds)
130
+ await lexiconAuthorityProfile.createRecords()
131
+
132
+ await ozone.addAdminDid(ozoneServiceProfile.did)
133
+ await ozone.createPolicies()
134
+
135
+ await thirdPartyPds.processAll()
136
+ await pds.processAll()
137
+ await ozone.processAll()
138
+ await bsky.sub.processAll()
139
+ await bsky.bsyncSub.processAll(await bsync.getStreamHeads())
140
+ await thirdPartyPds.close()
141
+
142
+ // Weird but if we do this before pds.processAll() somehow appview loses this user and tests in different parts fail because appview doesn't return this user in various contexts anymore
143
+ const ozoneVerifierPassword =
144
+ await ozoneServiceProfile.createAppPasswordForVerification()
145
+ if (ozone.daemon.ctx.cfg.verifier) {
146
+ ozone.daemon.ctx.cfg.verifier.password = ozoneVerifierPassword
147
+ }
148
+
149
+ let introspect: IntrospectServer | undefined = undefined
150
+ if (params.introspect?.port) {
151
+ introspect = await IntrospectServer.start(
152
+ params.introspect.port,
153
+ plc,
154
+ pds,
155
+ bsync,
156
+ bsky,
157
+ ozone,
158
+ )
159
+ }
160
+
161
+ return new TestNetwork(plc, pds, bsync, bsky, ozone, introspect)
162
+ }
163
+
164
+ async processFullSubscription(timeout = 5000) {
165
+ const sub = this.bsky.sub
166
+ // If the subscription is not running, there is no point in
167
+ // waiting for it to process events
168
+ if (!sub.running) return
169
+ const start = Date.now()
170
+ const lastSeq = await this.pds.ctx.sequencer.curr()
171
+ if (!lastSeq) return
172
+ while (Date.now() - start < timeout) {
173
+ await sub.processAll()
174
+ const runnerCursor = await sub.getCursor()
175
+ // if subscription claims to be done, ensure we are at the most recent cursor from PDS, else wait to process again
176
+ // (the subscription may claim to be finished before the PDS has even emitted it's event)
177
+ if (runnerCursor && runnerCursor >= lastSeq) {
178
+ return
179
+ }
180
+ await wait(5)
181
+ }
182
+ throw new Error(`Sequence was not processed within ${timeout}ms`)
183
+ }
184
+
185
+ async processAll(timeout?: number) {
186
+ await this.pds.processAll()
187
+ await this.ozone.processAll()
188
+ await this.bsky.sub.processAll()
189
+ await this.bsky.bsyncSub.processAll(
190
+ await this.bsync.getStreamHeads(),
191
+ timeout,
192
+ )
193
+ await this.processFullSubscription(timeout)
194
+ }
195
+
196
+ async serviceHeaders(did: string, lxm: string, aud?: string) {
197
+ const keypair = await this.pds.ctx.actorStore.keypair(did)
198
+ const jwt = await createServiceJwt({
199
+ iss: did,
200
+ aud: aud ?? this.bsky.ctx.cfg.serverDid,
201
+ lxm,
202
+ keypair,
203
+ })
204
+ return { authorization: `Bearer ${jwt}` }
205
+ }
206
+
207
+ async adminHeaders({
208
+ username = ADMIN_USERNAME,
209
+ password = ADMIN_PASSWORD,
210
+ }: {
211
+ username?: string
212
+ password?: string
213
+ }) {
214
+ return {
215
+ authorization:
216
+ 'Basic ' +
217
+ uint8arrays.toString(
218
+ uint8arrays.fromString(`${username}:${password}`, 'utf8'),
219
+ 'base64pad',
220
+ ),
221
+ }
222
+ }
223
+
224
+ async close() {
225
+ try {
226
+ await this.processAll()
227
+ } finally {
228
+ await allFulfilled([
229
+ ...this.feedGens.map(async (fg) => fg.close()),
230
+ this.ozone.close(),
231
+ this.bsky.close(),
232
+ this.bsync.close(),
233
+ this.pds.close(),
234
+ this.plc.close(),
235
+ this.introspect?.close(),
236
+ ])
237
+ }
238
+ }
239
+
240
+ async [Symbol.asyncDispose]() {
241
+ await this.close()
242
+ }
243
+ }