@atproto/ozone 0.1.107 → 0.1.108

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 (198) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +6 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/server/getConfig.d.ts.map +1 -1
  6. package/dist/api/server/getConfig.js +1 -0
  7. package/dist/api/server/getConfig.js.map +1 -1
  8. package/dist/api/setting/removeOptions.d.ts.map +1 -1
  9. package/dist/api/setting/removeOptions.js +1 -0
  10. package/dist/api/setting/removeOptions.js.map +1 -1
  11. package/dist/api/setting/upsertOption.js +7 -0
  12. package/dist/api/setting/upsertOption.js.map +1 -1
  13. package/dist/api/util.d.ts +1 -1
  14. package/dist/api/util.d.ts.map +1 -1
  15. package/dist/api/util.js +6 -1
  16. package/dist/api/util.js.map +1 -1
  17. package/dist/api/verification/grantVerifications.d.ts +4 -0
  18. package/dist/api/verification/grantVerifications.d.ts.map +1 -0
  19. package/dist/api/verification/grantVerifications.js +52 -0
  20. package/dist/api/verification/grantVerifications.js.map +1 -0
  21. package/dist/api/verification/listVerifications.d.ts +4 -0
  22. package/dist/api/verification/listVerifications.d.ts.map +1 -0
  23. package/dist/api/verification/listVerifications.js +32 -0
  24. package/dist/api/verification/listVerifications.js.map +1 -0
  25. package/dist/api/verification/revokeVerifications.d.ts +4 -0
  26. package/dist/api/verification/revokeVerifications.d.ts.map +1 -0
  27. package/dist/api/verification/revokeVerifications.js +36 -0
  28. package/dist/api/verification/revokeVerifications.js.map +1 -0
  29. package/dist/auth-verifier.d.ts +4 -1
  30. package/dist/auth-verifier.d.ts.map +1 -1
  31. package/dist/auth-verifier.js +4 -3
  32. package/dist/auth-verifier.js.map +1 -1
  33. package/dist/background.d.ts +3 -1
  34. package/dist/background.d.ts.map +1 -1
  35. package/dist/background.js +3 -2
  36. package/dist/background.js.map +1 -1
  37. package/dist/config/config.d.ts +9 -0
  38. package/dist/config/config.d.ts.map +1 -1
  39. package/dist/config/config.js +10 -0
  40. package/dist/config/config.js.map +1 -1
  41. package/dist/config/env.d.ts +5 -0
  42. package/dist/config/env.d.ts.map +1 -1
  43. package/dist/config/env.js +5 -0
  44. package/dist/config/env.js.map +1 -1
  45. package/dist/context.d.ts +6 -0
  46. package/dist/context.d.ts.map +1 -1
  47. package/dist/context.js +12 -0
  48. package/dist/context.js.map +1 -1
  49. package/dist/daemon/context.d.ts +3 -0
  50. package/dist/daemon/context.d.ts.map +1 -1
  51. package/dist/daemon/context.js +11 -0
  52. package/dist/daemon/context.js.map +1 -1
  53. package/dist/daemon/verification-listener.d.ts +29 -0
  54. package/dist/daemon/verification-listener.d.ts.map +1 -0
  55. package/dist/daemon/verification-listener.js +171 -0
  56. package/dist/daemon/verification-listener.js.map +1 -0
  57. package/dist/db/migrations/20250415T201720309Z-verification.d.ts +4 -0
  58. package/dist/db/migrations/20250415T201720309Z-verification.d.ts.map +1 -0
  59. package/dist/db/migrations/20250415T201720309Z-verification.js +35 -0
  60. package/dist/db/migrations/20250415T201720309Z-verification.js.map +1 -0
  61. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts +4 -0
  62. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts.map +1 -0
  63. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js +17 -0
  64. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js.map +1 -0
  65. package/dist/db/migrations/index.d.ts +2 -0
  66. package/dist/db/migrations/index.d.ts.map +1 -1
  67. package/dist/db/migrations/index.js +3 -1
  68. package/dist/db/migrations/index.js.map +1 -1
  69. package/dist/db/pagination.d.ts +15 -0
  70. package/dist/db/pagination.d.ts.map +1 -1
  71. package/dist/db/pagination.js +23 -1
  72. package/dist/db/pagination.js.map +1 -1
  73. package/dist/db/schema/firehose_cursor.d.ts +11 -0
  74. package/dist/db/schema/firehose_cursor.d.ts.map +1 -0
  75. package/dist/db/schema/firehose_cursor.js +5 -0
  76. package/dist/db/schema/firehose_cursor.js.map +1 -0
  77. package/dist/db/schema/index.d.ts +3 -1
  78. package/dist/db/schema/index.d.ts.map +1 -1
  79. package/dist/db/schema/member.d.ts +1 -1
  80. package/dist/db/schema/member.d.ts.map +1 -1
  81. package/dist/db/schema/verification.d.ts +19 -0
  82. package/dist/db/schema/verification.d.ts.map +1 -0
  83. package/dist/db/schema/verification.js +5 -0
  84. package/dist/db/schema/verification.js.map +1 -0
  85. package/dist/jetstream/service.d.ts +64 -0
  86. package/dist/jetstream/service.d.ts.map +1 -0
  87. package/dist/jetstream/service.js +65 -0
  88. package/dist/jetstream/service.js.map +1 -0
  89. package/dist/lexicon/index.d.ts +12 -0
  90. package/dist/lexicon/index.d.ts.map +1 -1
  91. package/dist/lexicon/index.js +33 -1
  92. package/dist/lexicon/index.js.map +1 -1
  93. package/dist/lexicon/lexicons.d.ts +672 -12
  94. package/dist/lexicon/lexicons.d.ts.map +1 -1
  95. package/dist/lexicon/lexicons.js +353 -0
  96. package/dist/lexicon/lexicons.js.map +1 -1
  97. package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts +3 -1
  98. package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts.map +1 -1
  99. package/dist/lexicon/types/tools/ozone/server/getConfig.js.map +1 -1
  100. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts +1 -1
  101. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts.map +1 -1
  102. package/dist/lexicon/types/tools/ozone/setting/defs.js.map +1 -1
  103. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts +1 -1
  104. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts.map +1 -1
  105. package/dist/lexicon/types/tools/ozone/team/addMember.d.ts +1 -1
  106. package/dist/lexicon/types/tools/ozone/team/addMember.d.ts.map +1 -1
  107. package/dist/lexicon/types/tools/ozone/team/defs.d.ts +3 -1
  108. package/dist/lexicon/types/tools/ozone/team/defs.d.ts.map +1 -1
  109. package/dist/lexicon/types/tools/ozone/team/defs.js +3 -1
  110. package/dist/lexicon/types/tools/ozone/team/defs.js.map +1 -1
  111. package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts +1 -1
  112. package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts.map +1 -1
  113. package/dist/lexicon/types/tools/ozone/verification/defs.d.ts +43 -0
  114. package/dist/lexicon/types/tools/ozone/verification/defs.d.ts.map +1 -0
  115. package/dist/lexicon/types/tools/ozone/verification/defs.js +16 -0
  116. package/dist/lexicon/types/tools/ozone/verification/defs.js.map +1 -0
  117. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts +66 -0
  118. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts.map +1 -0
  119. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js +25 -0
  120. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js.map +1 -0
  121. package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts +52 -0
  122. package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts.map +1 -0
  123. package/dist/lexicon/types/tools/ozone/verification/listVerifications.js +7 -0
  124. package/dist/lexicon/types/tools/ozone/verification/listVerifications.js.map +1 -0
  125. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts +56 -0
  126. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts.map +1 -0
  127. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js +16 -0
  128. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js.map +1 -0
  129. package/dist/logger.d.ts +1 -0
  130. package/dist/logger.d.ts.map +1 -1
  131. package/dist/logger.js +2 -1
  132. package/dist/logger.js.map +1 -1
  133. package/dist/mod-service/status.d.ts +6 -0
  134. package/dist/mod-service/status.d.ts.map +1 -1
  135. package/dist/team/index.d.ts +1 -0
  136. package/dist/team/index.d.ts.map +1 -1
  137. package/dist/team/index.js +3 -0
  138. package/dist/team/index.js.map +1 -1
  139. package/dist/verification/issuer.d.ts +37 -0
  140. package/dist/verification/issuer.d.ts.map +1 -0
  141. package/dist/verification/issuer.js +119 -0
  142. package/dist/verification/issuer.js.map +1 -0
  143. package/dist/verification/service.d.ts +47 -0
  144. package/dist/verification/service.d.ts.map +1 -0
  145. package/dist/verification/service.js +141 -0
  146. package/dist/verification/service.js.map +1 -0
  147. package/dist/verification/util.d.ts +6 -0
  148. package/dist/verification/util.d.ts.map +1 -0
  149. package/dist/verification/util.js +32 -0
  150. package/dist/verification/util.js.map +1 -0
  151. package/package.json +5 -4
  152. package/src/api/index.ts +6 -0
  153. package/src/api/server/getConfig.ts +1 -0
  154. package/src/api/setting/removeOptions.ts +1 -0
  155. package/src/api/setting/upsertOption.ts +7 -0
  156. package/src/api/util.ts +7 -1
  157. package/src/api/verification/grantVerifications.ts +74 -0
  158. package/src/api/verification/listVerifications.ts +44 -0
  159. package/src/api/verification/revokeVerifications.ts +43 -0
  160. package/src/auth-verifier.ts +8 -4
  161. package/src/background.ts +7 -2
  162. package/src/config/config.ts +21 -0
  163. package/src/config/env.ts +10 -0
  164. package/src/context.ts +22 -0
  165. package/src/daemon/context.ts +19 -0
  166. package/src/daemon/verification-listener.ts +164 -0
  167. package/src/db/migrations/20250415T201720309Z-verification.ts +34 -0
  168. package/src/db/migrations/20250417T201720309Z-firehose-cursor.ts +16 -0
  169. package/src/db/migrations/index.ts +2 -0
  170. package/src/db/pagination.ts +31 -0
  171. package/src/db/schema/firehose_cursor.ts +13 -0
  172. package/src/db/schema/index.ts +5 -1
  173. package/src/db/schema/member.ts +1 -0
  174. package/src/db/schema/verification.ts +21 -0
  175. package/src/jetstream/service.ts +110 -0
  176. package/src/lexicon/index.ts +47 -0
  177. package/src/lexicon/lexicons.ts +372 -0
  178. package/src/lexicon/types/tools/ozone/server/getConfig.ts +3 -0
  179. package/src/lexicon/types/tools/ozone/setting/defs.ts +1 -0
  180. package/src/lexicon/types/tools/ozone/setting/upsertOption.ts +1 -0
  181. package/src/lexicon/types/tools/ozone/team/addMember.ts +1 -0
  182. package/src/lexicon/types/tools/ozone/team/defs.ts +3 -0
  183. package/src/lexicon/types/tools/ozone/team/updateMember.ts +1 -0
  184. package/src/lexicon/types/tools/ozone/verification/defs.ts +59 -0
  185. package/src/lexicon/types/tools/ozone/verification/grantVerifications.ts +100 -0
  186. package/src/lexicon/types/tools/ozone/verification/listVerifications.ts +70 -0
  187. package/src/lexicon/types/tools/ozone/verification/revokeVerifications.ts +81 -0
  188. package/src/logger.ts +2 -0
  189. package/src/team/index.ts +4 -0
  190. package/src/verification/issuer.ts +135 -0
  191. package/src/verification/service.ts +208 -0
  192. package/src/verification/util.ts +50 -0
  193. package/tests/__snapshots__/verification-listener.test.ts.snap +146 -0
  194. package/tests/__snapshots__/verification.test.ts.snap +288 -0
  195. package/tests/verification-listener.test.ts +102 -0
  196. package/tests/verification.test.ts +136 -0
  197. package/tsconfig.build.tsbuildinfo +1 -1
  198. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,74 @@
1
+ import { Selectable } from 'kysely'
2
+ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
3
+ import { AppContext } from '../../context'
4
+ import { Verification } from '../../db/schema/verification'
5
+ import { Server } from '../../lexicon'
6
+ import { getReposForVerifications } from '../../verification/util'
7
+
8
+ export default function (server: Server, ctx: AppContext) {
9
+ server.tools.ozone.verification.grantVerifications({
10
+ auth: ctx.authVerifier.modOrAdminToken,
11
+ handler: async ({ input, auth, req }) => {
12
+ if (!ctx.cfg.verifier) {
13
+ throw new InvalidRequestError('Verifier not configured')
14
+ }
15
+
16
+ if (!auth.credentials.isVerifier) {
17
+ throw new AuthRequiredError(
18
+ 'Must be an admin or verifier to grant verifications',
19
+ )
20
+ }
21
+
22
+ const modViews = ctx.modService(ctx.db).views
23
+ const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)
24
+ const verificationService = ctx.verificationService(ctx.db)
25
+ const { grantedVerifications, failedVerifications } =
26
+ await verificationIssuer.verify(input.body.verifications)
27
+
28
+ if (!grantedVerifications.length) {
29
+ return {
30
+ encoding: 'application/json',
31
+ body: {
32
+ verifications: [],
33
+ failedVerifications,
34
+ },
35
+ }
36
+ }
37
+
38
+ const createdVerifications: Selectable<Verification>[] = []
39
+ const verificationEntries =
40
+ await verificationService.create(grantedVerifications)
41
+
42
+ const dids = new Set<string>([ctx.cfg.verifier.did])
43
+
44
+ for (const verification of verificationEntries) {
45
+ createdVerifications.push(verification)
46
+ dids.add(verification.subject)
47
+ }
48
+
49
+ const didsArr = Array.from(dids)
50
+ const [repos, profiles] = await Promise.all([
51
+ getReposForVerifications(
52
+ ctx,
53
+ ctx.reqLabelers(req),
54
+ ctx.modService(ctx.db),
55
+ didsArr,
56
+ auth.credentials.isModerator,
57
+ ),
58
+ modViews.getProfiles(didsArr),
59
+ ])
60
+ const verifications = verificationService.view(
61
+ createdVerifications,
62
+ repos,
63
+ profiles,
64
+ )
65
+ return {
66
+ encoding: 'application/json',
67
+ body: {
68
+ verifications,
69
+ failedVerifications,
70
+ },
71
+ }
72
+ },
73
+ })
74
+ }
@@ -0,0 +1,44 @@
1
+ import { AppContext } from '../../context'
2
+ import { Server } from '../../lexicon'
3
+ import { getReposForVerifications } from '../../verification/util'
4
+
5
+ export default function (server: Server, ctx: AppContext) {
6
+ server.tools.ozone.verification.listVerifications({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
+ handler: async ({ req, params, auth }) => {
9
+ const modViews = ctx.modService(ctx.db).views
10
+ const verificationService = ctx.verificationService(ctx.db)
11
+ const { verifications, cursor } = await verificationService.list(params)
12
+
13
+ const dids = new Set<string>()
14
+ for (const verification of verifications) {
15
+ dids.add(verification.subject)
16
+ dids.add(verification.issuer)
17
+ }
18
+
19
+ const didsArr = Array.from(dids)
20
+ const [repos, profiles] = await Promise.all([
21
+ getReposForVerifications(
22
+ ctx,
23
+ ctx.reqLabelers(req),
24
+ ctx.modService(ctx.db),
25
+ didsArr,
26
+ auth.credentials.isModerator,
27
+ ),
28
+ modViews.getProfiles(didsArr),
29
+ ])
30
+
31
+ return {
32
+ encoding: 'application/json',
33
+ body: {
34
+ cursor,
35
+ verifications: verificationService.view(
36
+ verifications,
37
+ repos,
38
+ profiles,
39
+ ),
40
+ },
41
+ }
42
+ },
43
+ })
44
+ }
@@ -0,0 +1,43 @@
1
+ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
2
+ import { AppContext } from '../../context'
3
+ import { Server } from '../../lexicon'
4
+
5
+ export default function (server: Server, ctx: AppContext) {
6
+ server.tools.ozone.verification.revokeVerifications({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
+ handler: async ({ input, auth }) => {
9
+ if (!ctx.cfg.verifier) {
10
+ throw new InvalidRequestError('Verifier not configured')
11
+ }
12
+
13
+ if (!auth.credentials.isVerifier) {
14
+ throw new AuthRequiredError(
15
+ 'Must be an admin or verifier to revoke verifications',
16
+ )
17
+ }
18
+
19
+ const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)
20
+ const { uris, revokeReason } = input.body
21
+ const { revokedVerifications, failedRevocations } =
22
+ await verificationIssuer.revoke({ uris })
23
+
24
+ if (revokedVerifications.length) {
25
+ const verificationService = ctx.verificationService(ctx.db)
26
+ await verificationService.markRevoked({
27
+ uris: revokedVerifications,
28
+ revokeReason,
29
+ revokedBy:
30
+ 'iss' in auth.credentials ? auth.credentials.iss : undefined,
31
+ })
32
+ }
33
+
34
+ return {
35
+ encoding: 'application/json',
36
+ body: {
37
+ revokedVerifications,
38
+ failedRevocations,
39
+ },
40
+ }
41
+ },
42
+ })
43
+ }
@@ -18,6 +18,7 @@ export type AdminTokenOutput = {
18
18
  isAdmin: true
19
19
  isModerator: true
20
20
  isTriage: true
21
+ isVerifier: true
21
22
  }
22
23
  }
23
24
 
@@ -28,7 +29,8 @@ export type ModeratorOutput = {
28
29
  iss: string
29
30
  isAdmin: boolean
30
31
  isModerator: boolean
31
- isTriage: true
32
+ isTriage: boolean
33
+ isVerifier: boolean
32
34
  }
33
35
  }
34
36
 
@@ -40,6 +42,7 @@ type StandardOutput = {
40
42
  isAdmin: boolean
41
43
  isModerator: boolean
42
44
  isTriage: boolean
45
+ isVerifier: boolean
43
46
  }
44
47
  }
45
48
 
@@ -82,14 +85,13 @@ export class AuthVerifier {
82
85
 
83
86
  moderator = async (reqCtx: ReqCtx): Promise<ModeratorOutput> => {
84
87
  const creds = await this.standard(reqCtx)
85
- if (!creds.credentials.isTriage) {
88
+ if (!creds.credentials.isTriage && !creds.credentials.isVerifier) {
86
89
  throw new AuthRequiredError('not a moderator account')
87
90
  }
88
91
  return {
89
92
  credentials: {
90
93
  ...creds.credentials,
91
94
  type: 'moderator',
92
- isTriage: true,
93
95
  },
94
96
  }
95
97
  }
@@ -125,7 +127,7 @@ export class AuthVerifier {
125
127
  throw new AuthRequiredError('member is disabled', 'MemberDisabled')
126
128
  }
127
129
 
128
- const { isAdmin, isModerator, isTriage } =
130
+ const { isAdmin, isModerator, isTriage, isVerifier } =
129
131
  this.teamService.getMemberRole(member)
130
132
 
131
133
  return {
@@ -136,6 +138,7 @@ export class AuthVerifier {
136
138
  isAdmin,
137
139
  isModerator,
138
140
  isTriage,
141
+ isVerifier,
139
142
  },
140
143
  }
141
144
  }
@@ -173,6 +176,7 @@ export class AuthVerifier {
173
176
  isAdmin: true,
174
177
  isModerator: true,
175
178
  isTriage: true,
179
+ isVerifier: true,
176
180
  },
177
181
  }
178
182
  }
package/src/background.ts CHANGED
@@ -10,7 +10,7 @@ type Task = (db: Database, signal: AbortSignal) => Promise<void>
10
10
  */
11
11
  export class BackgroundQueue {
12
12
  private abortController = new AbortController()
13
- private queue = new PQueue({ concurrency: 20 })
13
+ private queue: PQueue
14
14
 
15
15
  public get signal() {
16
16
  return this.abortController.signal
@@ -20,7 +20,12 @@ export class BackgroundQueue {
20
20
  return this.signal.aborted
21
21
  }
22
22
 
23
- constructor(protected db: Database) {}
23
+ constructor(
24
+ protected db: Database,
25
+ queueOpts?: { concurrency?: number },
26
+ ) {
27
+ this.queue = new PQueue(queueOpts ?? { concurrency: 20 })
28
+ }
24
29
 
25
30
  getStats() {
26
31
  return {
@@ -79,6 +79,15 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
79
79
  moderators: env.moderatorDids,
80
80
  triage: env.triageDids,
81
81
  }
82
+ const verifierCfg: OzoneConfig['verifier'] =
83
+ env.verifierUrl && env.verifierDid && env.verifierPassword
84
+ ? {
85
+ url: env.verifierUrl,
86
+ did: env.verifierDid,
87
+ password: env.verifierPassword,
88
+ issuersToIndex: env.verifierIssuersToIndex,
89
+ }
90
+ : null
82
91
 
83
92
  return {
84
93
  service: serviceCfg,
@@ -90,6 +99,8 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
90
99
  identity: identityCfg,
91
100
  blobDivert: blobDivertServiceCfg,
92
101
  access: accessCfg,
102
+ verifier: verifierCfg,
103
+ jetstreamUrl: env.jetstreamUrl,
93
104
  }
94
105
  }
95
106
 
@@ -103,6 +114,8 @@ export type OzoneConfig = {
103
114
  identity: IdentityConfig
104
115
  blobDivert: BlobDivertConfig | null
105
116
  access: AccessConfig
117
+ jetstreamUrl?: string
118
+ verifier: VerifierConfig | null
106
119
  }
107
120
 
108
121
  export type ServiceConfig = {
@@ -159,3 +172,11 @@ export type AccessConfig = {
159
172
  moderators: string[]
160
173
  triage: string[]
161
174
  }
175
+
176
+ export type VerifierConfig = {
177
+ url: string
178
+ did: string
179
+ password: string
180
+ jetstreamUrl?: string
181
+ issuersToIndex?: string[]
182
+ }
package/src/config/env.ts CHANGED
@@ -37,6 +37,11 @@ export const readEnv = (): OzoneEnvironment => {
37
37
  signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'),
38
38
  blobDivertUrl: envStr('OZONE_BLOB_DIVERT_URL'),
39
39
  blobDivertAdminPassword: envStr('OZONE_BLOB_DIVERT_ADMIN_PASSWORD'),
40
+ verifierUrl: envStr('OZONE_VERIFIER_URL'),
41
+ verifierDid: envStr('OZONE_VERIFIER_DID'),
42
+ verifierPassword: envStr('OZONE_VERIFIER_PASSWORD'),
43
+ verifierIssuersToIndex: envList('OZONE_VERIFIER_ISSUERS_TO_INDEX'),
44
+ jetstreamUrl: envStr('OZONE_JETSTREAM_URL'),
40
45
  }
41
46
  }
42
47
 
@@ -72,4 +77,9 @@ export type OzoneEnvironment = {
72
77
  signingKeyHex?: string
73
78
  blobDivertUrl?: string
74
79
  blobDivertAdminPassword?: string
80
+ verifierUrl?: string
81
+ verifierDid?: string
82
+ verifierPassword?: string
83
+ verifierIssuersToIndex?: string[]
84
+ jetstreamUrl?: string
75
85
  }
package/src/context.ts CHANGED
@@ -28,6 +28,14 @@ import {
28
28
  getSigningKeyId,
29
29
  parseLabelerHeader,
30
30
  } from './util'
31
+ import {
32
+ VerificationIssuer,
33
+ VerificationIssuerCreator,
34
+ } from './verification/issuer'
35
+ import {
36
+ VerificationService,
37
+ VerificationServiceCreator,
38
+ } from './verification/service'
31
39
 
32
40
  export type AppContextOptions = {
33
41
  db: Database
@@ -49,6 +57,8 @@ export type AppContextOptions = {
49
57
  backgroundQueue: BackgroundQueue
50
58
  sequencer: Sequencer
51
59
  authVerifier: AuthVerifier
60
+ verificationService: VerificationServiceCreator
61
+ verificationIssuer: VerificationIssuerCreator
52
62
  }
53
63
 
54
64
  export class AppContext {
@@ -127,6 +137,8 @@ export class AppContext {
127
137
  )
128
138
  const setService = SetService.creator()
129
139
  const settingService = SettingService.creator()
140
+ const verificationService = VerificationService.creator()
141
+ const verificationIssuer = VerificationIssuer.creator()
130
142
 
131
143
  const sequencer = new Sequencer(modService(db))
132
144
 
@@ -156,6 +168,8 @@ export class AppContext {
156
168
  sequencer,
157
169
  authVerifier,
158
170
  blobDiverter,
171
+ verificationService,
172
+ verificationIssuer,
159
173
  ...(overrides ?? {}),
160
174
  },
161
175
  secrets,
@@ -202,6 +216,14 @@ export class AppContext {
202
216
  return this.opts.settingService
203
217
  }
204
218
 
219
+ get verificationService(): VerificationServiceCreator {
220
+ return this.opts.verificationService
221
+ }
222
+
223
+ get verificationIssuer(): VerificationIssuerCreator {
224
+ return this.opts.verificationIssuer
225
+ }
226
+
205
227
  get appviewAgent(): AtpAgent {
206
228
  return this.opts.appviewAgent
207
229
  }
@@ -13,6 +13,7 @@ import { EventPusher } from './event-pusher'
13
13
  import { EventReverser } from './event-reverser'
14
14
  import { MaterializedViewRefresher } from './materialized-view-refresher'
15
15
  import { TeamProfileSynchronizer } from './team-profile-synchronizer'
16
+ import { VerificationListener } from './verification-listener'
16
17
 
17
18
  export type DaemonContextOptions = {
18
19
  db: Database
@@ -23,6 +24,7 @@ export type DaemonContextOptions = {
23
24
  eventReverser: EventReverser
24
25
  materializedViewRefresher: MaterializedViewRefresher
25
26
  teamProfileSynchronizer: TeamProfileSynchronizer
27
+ verificationListener?: VerificationListener
26
28
  }
27
29
 
28
30
  export class DaemonContext {
@@ -88,6 +90,16 @@ export class DaemonContext {
88
90
  cfg.db.materializedViewRefreshIntervalMs,
89
91
  )
90
92
 
93
+ // Only spawn the listener if verifier config exists and a jetstream URL is provided
94
+ const verificationListener =
95
+ cfg.verifier && cfg.jetstreamUrl
96
+ ? new VerificationListener(
97
+ db,
98
+ cfg.jetstreamUrl,
99
+ cfg.verifier?.issuersToIndex,
100
+ )
101
+ : undefined
102
+
91
103
  return new DaemonContext({
92
104
  db,
93
105
  cfg,
@@ -97,6 +109,7 @@ export class DaemonContext {
97
109
  eventReverser,
98
110
  materializedViewRefresher,
99
111
  teamProfileSynchronizer,
112
+ verificationListener,
100
113
  ...(overrides ?? {}),
101
114
  })
102
115
  }
@@ -129,11 +142,16 @@ export class DaemonContext {
129
142
  return this.opts.teamProfileSynchronizer
130
143
  }
131
144
 
145
+ get verificationListener(): VerificationListener | undefined {
146
+ return this.opts.verificationListener
147
+ }
148
+
132
149
  async start() {
133
150
  this.eventPusher.start()
134
151
  this.eventReverser.start()
135
152
  this.materializedViewRefresher.start()
136
153
  this.teamProfileSynchronizer.start()
154
+ this.verificationListener?.start()
137
155
  }
138
156
 
139
157
  async processAll() {
@@ -150,6 +168,7 @@ export class DaemonContext {
150
168
  this.eventPusher.destroy(),
151
169
  this.materializedViewRefresher.destroy(),
152
170
  this.teamProfileSynchronizer.destroy(),
171
+ this.verificationListener?.stop(),
153
172
  ])
154
173
  } finally {
155
174
  await this.backgroundQueue.destroy()
@@ -0,0 +1,164 @@
1
+ import { lexicons } from '@atproto/api'
2
+ import { BackgroundQueue } from '../background'
3
+ import { Database } from '../db'
4
+ import { CommitCreateEvent, Jetstream } from '../jetstream/service'
5
+ import { verificationLogger } from '../logger'
6
+ import { VerificationService } from '../verification/service'
7
+
8
+ type VerificationRecord = {
9
+ subject: string
10
+ handle: string
11
+ displayName: string
12
+ createdAt: string
13
+ }
14
+
15
+ export class VerificationListener {
16
+ destroyed = false
17
+ private cursor?: number
18
+ private jetstream: Jetstream | null = null
19
+ private collection = 'app.bsky.graph.verification'
20
+ public backgroundQueue = new BackgroundQueue(this.db, { concurrency: 1 })
21
+ private verificationService = VerificationService.creator()(this.db)
22
+
23
+ constructor(
24
+ private db: Database,
25
+ private jetstreamUrl: string,
26
+ private verifierIssuersToIndex?: string[],
27
+ ) {}
28
+
29
+ // When the queue has capacity, this method returns true which means we can continue to handle events
30
+ // otherwise, it will close jetstream connection and wait for all previously queued events to be processed first
31
+ // and then start jetstream listener again before returning false. At that point, the previous listeners should
32
+ // have updates the cursor in db to the last processed event and the new listener will start from that cursor
33
+ async ensureCoolDown() {
34
+ const { waitingCount, runningCount } = this.backgroundQueue.getStats()
35
+ if (waitingCount > 50 || runningCount > 50) {
36
+ verificationLogger.warn(`Background queue is full, pausing listener`)
37
+ this.jetstream?.close()
38
+ await this.backgroundQueue.processAll()
39
+ await this.start()
40
+ return false
41
+ }
42
+ return true
43
+ }
44
+
45
+ handleNewVerification(
46
+ issuer: string,
47
+ uri: string,
48
+ cid: string,
49
+ record: VerificationRecord,
50
+ cursor: number,
51
+ ) {
52
+ this.backgroundQueue.add(async () => {
53
+ try {
54
+ const { subject, handle, displayName, createdAt } = record
55
+ await this.verificationService.create([
56
+ { uri, cid, issuer, subject, handle, displayName, createdAt },
57
+ ])
58
+ await this.updateCursor(cursor)
59
+ } catch (err) {
60
+ verificationLogger.error(
61
+ err,
62
+ 'Error handling verification create event',
63
+ )
64
+ }
65
+ })
66
+ }
67
+
68
+ handleDeletedVerification(uri: string, cursor: number) {
69
+ this.backgroundQueue.add(async () => {
70
+ try {
71
+ await this.verificationService.markRevoked({
72
+ uris: [uri],
73
+ })
74
+ await this.updateCursor(cursor)
75
+ } catch (err) {
76
+ verificationLogger.error(
77
+ err,
78
+ 'Error handling verification delete event',
79
+ )
80
+ }
81
+ })
82
+ }
83
+
84
+ async getCursor() {
85
+ await this.verificationService.createFirehoseCursor()
86
+ const cursor = await this.verificationService.getFirehoseCursor()
87
+ if (cursor) {
88
+ this.cursor = cursor
89
+ }
90
+ return this.cursor
91
+ }
92
+
93
+ async updateCursor(cursor: number) {
94
+ // Assuming cursors are always incremental, if we have processed an event with higher value cursor, let's not update to a lower value
95
+ if (this.cursor && this.cursor >= cursor) {
96
+ return
97
+ }
98
+
99
+ // This will only update if the cursor is higher than the current one in db
100
+ const updatedCursor =
101
+ await this.verificationService.updateFirehoseCursor(cursor)
102
+
103
+ if (updatedCursor) {
104
+ this.cursor = updatedCursor
105
+ }
106
+ }
107
+
108
+ async start() {
109
+ await this.getCursor()
110
+
111
+ this.jetstream = new Jetstream({
112
+ endpoint: this.jetstreamUrl,
113
+ cursor: this.cursor || undefined,
114
+ wantedCollections: [this.collection],
115
+ wantedDids: this.verifierIssuersToIndex?.length
116
+ ? this.verifierIssuersToIndex
117
+ : undefined,
118
+ })
119
+
120
+ await this.jetstream.start({
121
+ onCreate: {
122
+ [this.collection]: async (e: CommitCreateEvent<VerificationRecord>) => {
123
+ const recordValidity = lexicons.validate(
124
+ this.collection,
125
+ e.commit.record,
126
+ )
127
+
128
+ if (!recordValidity.success) {
129
+ verificationLogger.error(
130
+ recordValidity.error,
131
+ 'Invalid verification record in the firehose',
132
+ )
133
+ return
134
+ }
135
+
136
+ const hasCapacity = await this.ensureCoolDown()
137
+ if (hasCapacity) {
138
+ const issuer = e.did
139
+ const { record, rkey, collection, cid } = e.commit
140
+ const uri = `at://${issuer}/${collection}/${rkey}`
141
+ this.handleNewVerification(issuer, uri, cid, record, e.time_us)
142
+ }
143
+ },
144
+ },
145
+ onDelete: {
146
+ [this.collection]: async (e) => {
147
+ const hasCapacity = await this.ensureCoolDown()
148
+ if (hasCapacity) {
149
+ this.handleDeletedVerification(
150
+ `at://${e.did}/${e.commit.collection}/${e.commit.rkey}`,
151
+ e.time_us,
152
+ )
153
+ }
154
+ },
155
+ },
156
+ })
157
+ }
158
+
159
+ stop() {
160
+ this.jetstream?.close()
161
+ this.backgroundQueue.destroy()
162
+ this.destroyed = true
163
+ }
164
+ }
@@ -0,0 +1,34 @@
1
+ import { Kysely, sql } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .createTable('verification')
6
+ .addColumn('uri', 'text', (col) => col.notNull().primaryKey())
7
+ .addColumn('cid', 'text', (col) => col.notNull())
8
+ .addColumn('issuer', 'text', (col) => col.notNull())
9
+ .addColumn('subject', 'text', (col) => col.notNull())
10
+ .addColumn('handle', 'text', (col) => col.notNull())
11
+ .addColumn('displayName', 'text', (col) => col.notNull())
12
+ .addColumn('revokeReason', 'text')
13
+ .addColumn('revokedBy', 'text')
14
+ .addColumn('revokedAt', 'text')
15
+ .addColumn('createdAt', 'text', (col) => col.notNull())
16
+ .addColumn('updatedAt', 'text', (col) =>
17
+ col.defaultTo(sql`now()`).notNull(),
18
+ )
19
+ .execute()
20
+ await db.schema
21
+ .createIndex('verification_issuer_idx')
22
+ .on('verification')
23
+ .column('issuer')
24
+ .execute()
25
+ await db.schema
26
+ .createIndex('verification_createdat_uri_idx')
27
+ .on('verification')
28
+ .columns(['createdAt', 'uri'])
29
+ .execute()
30
+ }
31
+
32
+ export async function down(db: Kysely<unknown>): Promise<void> {
33
+ await db.schema.dropTable('verification').execute()
34
+ }
@@ -0,0 +1,16 @@
1
+ import { Kysely, sql } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .createTable('firehose_cursor')
6
+ .addColumn('service', 'text', (col) => col.primaryKey())
7
+ .addColumn('cursor', 'bigint')
8
+ .addColumn('updatedAt', 'text', (col) =>
9
+ col.defaultTo(sql`now()`).notNull(),
10
+ )
11
+ .execute()
12
+ }
13
+
14
+ export async function down(db: Kysely<unknown>): Promise<void> {
15
+ await db.schema.dropTable('firehose_cursor').execute()
16
+ }
@@ -23,3 +23,5 @@ export * as _20250211T003647759Z from './20250211T003647759Z-add-reporter-stats-
23
23
  export * as _20250211T132135150Z from './20250211T132135150Z-moderation-event-message-partial-idx'
24
24
  export * as _20250221T132135150Z from './20250221T132135150Z-member-details'
25
25
  export * as _20250404T201720309Z from './20250404T201720309Z-subject-status-sort-idxs'
26
+ export * as _20250415T201720309Z from './20250415T201720309Z-verification'
27
+ export * as _20250417T201720309Z from './20250417T201720309Z-firehose-cursor'