@atproto/ozone 0.1.106 → 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 (215) hide show
  1. package/CHANGELOG.md +20 -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/proxied.d.ts.map +1 -1
  6. package/dist/api/proxied.js +29 -16
  7. package/dist/api/proxied.js.map +1 -1
  8. package/dist/api/server/getConfig.d.ts.map +1 -1
  9. package/dist/api/server/getConfig.js +1 -0
  10. package/dist/api/server/getConfig.js.map +1 -1
  11. package/dist/api/setting/removeOptions.d.ts.map +1 -1
  12. package/dist/api/setting/removeOptions.js +1 -0
  13. package/dist/api/setting/removeOptions.js.map +1 -1
  14. package/dist/api/setting/upsertOption.js +7 -0
  15. package/dist/api/setting/upsertOption.js.map +1 -1
  16. package/dist/api/util.d.ts +1 -1
  17. package/dist/api/util.d.ts.map +1 -1
  18. package/dist/api/util.js +6 -1
  19. package/dist/api/util.js.map +1 -1
  20. package/dist/api/verification/grantVerifications.d.ts +4 -0
  21. package/dist/api/verification/grantVerifications.d.ts.map +1 -0
  22. package/dist/api/verification/grantVerifications.js +52 -0
  23. package/dist/api/verification/grantVerifications.js.map +1 -0
  24. package/dist/api/verification/listVerifications.d.ts +4 -0
  25. package/dist/api/verification/listVerifications.d.ts.map +1 -0
  26. package/dist/api/verification/listVerifications.js +32 -0
  27. package/dist/api/verification/listVerifications.js.map +1 -0
  28. package/dist/api/verification/revokeVerifications.d.ts +4 -0
  29. package/dist/api/verification/revokeVerifications.d.ts.map +1 -0
  30. package/dist/api/verification/revokeVerifications.js +36 -0
  31. package/dist/api/verification/revokeVerifications.js.map +1 -0
  32. package/dist/auth-verifier.d.ts +4 -1
  33. package/dist/auth-verifier.d.ts.map +1 -1
  34. package/dist/auth-verifier.js +4 -3
  35. package/dist/auth-verifier.js.map +1 -1
  36. package/dist/background.d.ts +3 -1
  37. package/dist/background.d.ts.map +1 -1
  38. package/dist/background.js +3 -2
  39. package/dist/background.js.map +1 -1
  40. package/dist/config/config.d.ts +9 -0
  41. package/dist/config/config.d.ts.map +1 -1
  42. package/dist/config/config.js +10 -0
  43. package/dist/config/config.js.map +1 -1
  44. package/dist/config/env.d.ts +5 -0
  45. package/dist/config/env.d.ts.map +1 -1
  46. package/dist/config/env.js +5 -0
  47. package/dist/config/env.js.map +1 -1
  48. package/dist/context.d.ts +6 -0
  49. package/dist/context.d.ts.map +1 -1
  50. package/dist/context.js +12 -0
  51. package/dist/context.js.map +1 -1
  52. package/dist/daemon/context.d.ts +3 -0
  53. package/dist/daemon/context.d.ts.map +1 -1
  54. package/dist/daemon/context.js +11 -0
  55. package/dist/daemon/context.js.map +1 -1
  56. package/dist/daemon/verification-listener.d.ts +29 -0
  57. package/dist/daemon/verification-listener.d.ts.map +1 -0
  58. package/dist/daemon/verification-listener.js +171 -0
  59. package/dist/daemon/verification-listener.js.map +1 -0
  60. package/dist/db/migrations/20250415T201720309Z-verification.d.ts +4 -0
  61. package/dist/db/migrations/20250415T201720309Z-verification.d.ts.map +1 -0
  62. package/dist/db/migrations/20250415T201720309Z-verification.js +35 -0
  63. package/dist/db/migrations/20250415T201720309Z-verification.js.map +1 -0
  64. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts +4 -0
  65. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts.map +1 -0
  66. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js +17 -0
  67. package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js.map +1 -0
  68. package/dist/db/migrations/index.d.ts +2 -0
  69. package/dist/db/migrations/index.d.ts.map +1 -1
  70. package/dist/db/migrations/index.js +3 -1
  71. package/dist/db/migrations/index.js.map +1 -1
  72. package/dist/db/pagination.d.ts +15 -0
  73. package/dist/db/pagination.d.ts.map +1 -1
  74. package/dist/db/pagination.js +23 -1
  75. package/dist/db/pagination.js.map +1 -1
  76. package/dist/db/schema/firehose_cursor.d.ts +11 -0
  77. package/dist/db/schema/firehose_cursor.d.ts.map +1 -0
  78. package/dist/db/schema/firehose_cursor.js +5 -0
  79. package/dist/db/schema/firehose_cursor.js.map +1 -0
  80. package/dist/db/schema/index.d.ts +3 -1
  81. package/dist/db/schema/index.d.ts.map +1 -1
  82. package/dist/db/schema/member.d.ts +1 -1
  83. package/dist/db/schema/member.d.ts.map +1 -1
  84. package/dist/db/schema/verification.d.ts +19 -0
  85. package/dist/db/schema/verification.d.ts.map +1 -0
  86. package/dist/db/schema/verification.js +5 -0
  87. package/dist/db/schema/verification.js.map +1 -0
  88. package/dist/jetstream/service.d.ts +64 -0
  89. package/dist/jetstream/service.d.ts.map +1 -0
  90. package/dist/jetstream/service.js +65 -0
  91. package/dist/jetstream/service.js.map +1 -0
  92. package/dist/lexicon/index.d.ts +19 -0
  93. package/dist/lexicon/index.d.ts.map +1 -1
  94. package/dist/lexicon/index.js +56 -1
  95. package/dist/lexicon/index.js.map +1 -1
  96. package/dist/lexicon/lexicons.d.ts +976 -80
  97. package/dist/lexicon/lexicons.d.ts.map +1 -1
  98. package/dist/lexicon/lexicons.js +485 -1
  99. package/dist/lexicon/lexicons.js.map +1 -1
  100. package/dist/lexicon/types/chat/bsky/actor/defs.d.ts +1 -1
  101. package/dist/lexicon/types/chat/bsky/actor/defs.d.ts.map +1 -1
  102. package/dist/lexicon/types/tools/ozone/hosting/getAccountHistory.d.ts +81 -0
  103. package/dist/lexicon/types/tools/ozone/hosting/getAccountHistory.d.ts.map +1 -0
  104. package/dist/lexicon/types/tools/ozone/hosting/getAccountHistory.js +61 -0
  105. package/dist/lexicon/types/tools/ozone/hosting/getAccountHistory.js.map +1 -0
  106. package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts +3 -1
  107. package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts.map +1 -1
  108. package/dist/lexicon/types/tools/ozone/server/getConfig.js.map +1 -1
  109. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts +1 -1
  110. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts.map +1 -1
  111. package/dist/lexicon/types/tools/ozone/setting/defs.js.map +1 -1
  112. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts +1 -1
  113. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts.map +1 -1
  114. package/dist/lexicon/types/tools/ozone/team/addMember.d.ts +1 -1
  115. package/dist/lexicon/types/tools/ozone/team/addMember.d.ts.map +1 -1
  116. package/dist/lexicon/types/tools/ozone/team/defs.d.ts +3 -1
  117. package/dist/lexicon/types/tools/ozone/team/defs.d.ts.map +1 -1
  118. package/dist/lexicon/types/tools/ozone/team/defs.js +3 -1
  119. package/dist/lexicon/types/tools/ozone/team/defs.js.map +1 -1
  120. package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts +1 -1
  121. package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts.map +1 -1
  122. package/dist/lexicon/types/tools/ozone/verification/defs.d.ts +43 -0
  123. package/dist/lexicon/types/tools/ozone/verification/defs.d.ts.map +1 -0
  124. package/dist/lexicon/types/tools/ozone/verification/defs.js +16 -0
  125. package/dist/lexicon/types/tools/ozone/verification/defs.js.map +1 -0
  126. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts +66 -0
  127. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts.map +1 -0
  128. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js +25 -0
  129. package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js.map +1 -0
  130. package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts +52 -0
  131. package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts.map +1 -0
  132. package/dist/lexicon/types/tools/ozone/verification/listVerifications.js +7 -0
  133. package/dist/lexicon/types/tools/ozone/verification/listVerifications.js.map +1 -0
  134. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts +56 -0
  135. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts.map +1 -0
  136. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js +16 -0
  137. package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js.map +1 -0
  138. package/dist/logger.d.ts +1 -0
  139. package/dist/logger.d.ts.map +1 -1
  140. package/dist/logger.js +2 -1
  141. package/dist/logger.js.map +1 -1
  142. package/dist/mod-service/index.d.ts.map +1 -1
  143. package/dist/mod-service/index.js +58 -21
  144. package/dist/mod-service/index.js.map +1 -1
  145. package/dist/mod-service/status.d.ts +6 -0
  146. package/dist/mod-service/status.d.ts.map +1 -1
  147. package/dist/team/index.d.ts +1 -0
  148. package/dist/team/index.d.ts.map +1 -1
  149. package/dist/team/index.js +3 -0
  150. package/dist/team/index.js.map +1 -1
  151. package/dist/verification/issuer.d.ts +37 -0
  152. package/dist/verification/issuer.d.ts.map +1 -0
  153. package/dist/verification/issuer.js +119 -0
  154. package/dist/verification/issuer.js.map +1 -0
  155. package/dist/verification/service.d.ts +47 -0
  156. package/dist/verification/service.d.ts.map +1 -0
  157. package/dist/verification/service.js +141 -0
  158. package/dist/verification/service.js.map +1 -0
  159. package/dist/verification/util.d.ts +6 -0
  160. package/dist/verification/util.d.ts.map +1 -0
  161. package/dist/verification/util.js +32 -0
  162. package/dist/verification/util.js.map +1 -0
  163. package/package.json +5 -4
  164. package/src/api/index.ts +6 -0
  165. package/src/api/proxied.ts +39 -24
  166. package/src/api/server/getConfig.ts +1 -0
  167. package/src/api/setting/removeOptions.ts +1 -0
  168. package/src/api/setting/upsertOption.ts +7 -0
  169. package/src/api/util.ts +7 -1
  170. package/src/api/verification/grantVerifications.ts +74 -0
  171. package/src/api/verification/listVerifications.ts +44 -0
  172. package/src/api/verification/revokeVerifications.ts +43 -0
  173. package/src/auth-verifier.ts +8 -4
  174. package/src/background.ts +7 -2
  175. package/src/config/config.ts +21 -0
  176. package/src/config/env.ts +10 -0
  177. package/src/context.ts +22 -0
  178. package/src/daemon/context.ts +19 -0
  179. package/src/daemon/verification-listener.ts +164 -0
  180. package/src/db/migrations/20250415T201720309Z-verification.ts +34 -0
  181. package/src/db/migrations/20250417T201720309Z-firehose-cursor.ts +16 -0
  182. package/src/db/migrations/index.ts +2 -0
  183. package/src/db/pagination.ts +31 -0
  184. package/src/db/schema/firehose_cursor.ts +13 -0
  185. package/src/db/schema/index.ts +5 -1
  186. package/src/db/schema/member.ts +1 -0
  187. package/src/db/schema/verification.ts +21 -0
  188. package/src/jetstream/service.ts +110 -0
  189. package/src/lexicon/index.ts +69 -0
  190. package/src/lexicon/lexicons.ts +505 -1
  191. package/src/lexicon/types/chat/bsky/actor/defs.ts +1 -1
  192. package/src/lexicon/types/tools/ozone/hosting/getAccountHistory.ts +161 -0
  193. package/src/lexicon/types/tools/ozone/server/getConfig.ts +3 -0
  194. package/src/lexicon/types/tools/ozone/setting/defs.ts +1 -0
  195. package/src/lexicon/types/tools/ozone/setting/upsertOption.ts +1 -0
  196. package/src/lexicon/types/tools/ozone/team/addMember.ts +1 -0
  197. package/src/lexicon/types/tools/ozone/team/defs.ts +3 -0
  198. package/src/lexicon/types/tools/ozone/team/updateMember.ts +1 -0
  199. package/src/lexicon/types/tools/ozone/verification/defs.ts +59 -0
  200. package/src/lexicon/types/tools/ozone/verification/grantVerifications.ts +100 -0
  201. package/src/lexicon/types/tools/ozone/verification/listVerifications.ts +70 -0
  202. package/src/lexicon/types/tools/ozone/verification/revokeVerifications.ts +81 -0
  203. package/src/logger.ts +2 -0
  204. package/src/mod-service/index.ts +83 -30
  205. package/src/team/index.ts +4 -0
  206. package/src/verification/issuer.ts +135 -0
  207. package/src/verification/service.ts +208 -0
  208. package/src/verification/util.ts +50 -0
  209. package/tests/__snapshots__/verification-listener.test.ts.snap +146 -0
  210. package/tests/__snapshots__/verification.test.ts.snap +288 -0
  211. package/tests/get-reporter-stats.test.ts +24 -9
  212. package/tests/verification-listener.test.ts +102 -0
  213. package/tests/verification.test.ts +136 -0
  214. package/tsconfig.build.tsbuildinfo +1 -1
  215. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
1
1
  import net from 'node:net'
2
- import { Insertable, sql } from 'kysely'
2
+ import { Insertable, RawBuilder, sql } from 'kysely'
3
3
  import { CID } from 'multiformats/cid'
4
4
  import { AtpAgent } from '@atproto/api'
5
5
  import { addHoursToDate, chunkArray } from '@atproto/common'
@@ -1275,17 +1275,35 @@ export class ModerationService {
1275
1275
  }
1276
1276
  }
1277
1277
 
1278
- buildModerationQuery(
1278
+ async buildModerationQuery(
1279
1279
  subjectType: 'account' | 'record',
1280
1280
  createdByDids: string[],
1281
1281
  isActionQuery: boolean,
1282
1282
  ): Promise<(Partial<ReporterStatsResult> & { did: string })[]> {
1283
- const isAccount = subjectType === 'account'
1283
+ if (!createdByDids.length) return []
1284
+
1284
1285
  const actionTypes = [
1285
1286
  'tools.ozone.moderation.defs#modEventTakedown',
1286
1287
  'tools.ozone.moderation.defs#modEventLabel',
1287
1288
  ] as const
1288
1289
 
1290
+ const countAll = () => {
1291
+ return sql<number>`COUNT(*)`
1292
+ }
1293
+ const countAllDistinctBy = (ref: RawBuilder) => {
1294
+ return sql<number>`COUNT(DISTINCT ${ref})`
1295
+ }
1296
+ const countTakedownsDistinctBy = (ref: RawBuilder) => {
1297
+ return sql<number>`COUNT(DISTINCT ${ref}) FILTER (
1298
+ WHERE actions."action" = 'tools.ozone.moderation.defs#modEventTakedown'
1299
+ )`
1300
+ }
1301
+ const countLabelsDistinctBy = (ref: RawBuilder) => {
1302
+ return sql<number>`COUNT(DISTINCT ${ref}) FILTER (
1303
+ WHERE actions."action" = 'tools.ozone.moderation.defs#modEventLabel'
1304
+ )`
1305
+ }
1306
+
1289
1307
  const query = this.db.db
1290
1308
  .selectFrom('moderation_event as reports')
1291
1309
  .where(
@@ -1293,48 +1311,83 @@ export class ModerationService {
1293
1311
  '=',
1294
1312
  'tools.ozone.moderation.defs#modEventReport',
1295
1313
  )
1296
- .where('reports.subjectUri', isAccount ? 'is' : 'is not', null)
1314
+ .where(
1315
+ 'reports.subjectUri',
1316
+ subjectType === 'account' ? 'is' : 'is not',
1317
+ null,
1318
+ )
1297
1319
  .where('reports.createdBy', 'in', createdByDids)
1298
1320
  .select(['reports.createdBy as did'])
1299
1321
 
1300
- if (isActionQuery) {
1322
+ if (!isActionQuery) {
1323
+ if (subjectType === 'account') {
1324
+ return query
1325
+ .select([
1326
+ () => countAll().as('accountReportCount'),
1327
+ (eb) =>
1328
+ countAllDistinctBy(eb.ref('reports.subjectDid')).as(
1329
+ 'reportedAccountCount',
1330
+ ),
1331
+ ])
1332
+ .groupBy('reports.createdBy')
1333
+ .execute()
1334
+ } else {
1335
+ return query
1336
+ .select([
1337
+ () => countAll().as('recordReportCount'),
1338
+ (eb) =>
1339
+ countAllDistinctBy(eb.ref('reports.subjectUri')).as(
1340
+ 'reportedRecordCount',
1341
+ ),
1342
+ ])
1343
+ .groupBy('reports.createdBy')
1344
+ .execute()
1345
+ }
1346
+ }
1347
+
1348
+ if (subjectType === 'account') {
1349
+ return query
1350
+ .leftJoin('moderation_event as actions', (join) =>
1351
+ join
1352
+ .onRef('actions.subjectDid', '=', 'reports.subjectDid')
1353
+ .on('actions.subjectUri', 'is', null)
1354
+ .onRef('actions.createdAt', '>', 'reports.createdAt')
1355
+ .on('actions.action', 'in', actionTypes),
1356
+ )
1357
+ .select([
1358
+ (eb) =>
1359
+ countTakedownsDistinctBy(eb.ref('actions.subjectDid')).as(
1360
+ 'takendownAccountCount',
1361
+ ),
1362
+ (eb) =>
1363
+ countLabelsDistinctBy(eb.ref('actions.subjectDid')).as(
1364
+ 'labeledAccountCount',
1365
+ ),
1366
+ ])
1367
+ .groupBy('reports.createdBy')
1368
+ .execute()
1369
+ } else {
1301
1370
  return query
1302
1371
  .leftJoin('moderation_event as actions', (join) =>
1303
1372
  join
1304
1373
  .onRef('actions.subjectDid', '=', 'reports.subjectDid')
1305
- .on('actions.subjectUri', isAccount ? 'is' : 'is not', null)
1374
+ .onRef('actions.subjectUri', '=', 'reports.subjectUri')
1306
1375
  .onRef('actions.createdAt', '>', 'reports.createdAt')
1307
1376
  .on('actions.action', 'in', actionTypes),
1308
1377
  )
1309
1378
  .select([
1310
- () =>
1311
- sql<number>`COUNT(DISTINCT actions."subjectDid") FILTER (
1312
- WHERE actions."action" = 'tools.ozone.moderation.defs#modEventTakedown'
1313
- )`.as(`takendown${isAccount ? 'Account' : 'Record'}Count`),
1314
-
1315
- () =>
1316
- sql<number>`COUNT(DISTINCT actions."subjectDid") FILTER (
1317
- WHERE actions."action" = 'tools.ozone.moderation.defs#modEventLabel'
1318
- )`.as(`labeled${isAccount ? 'Account' : 'Record'}Count`),
1379
+ (eb) =>
1380
+ countTakedownsDistinctBy(eb.ref('actions.subjectUri')).as(
1381
+ 'takendownRecordCount',
1382
+ ),
1383
+ (eb) =>
1384
+ countLabelsDistinctBy(eb.ref('actions.subjectUri')).as(
1385
+ 'labeledRecordCount',
1386
+ ),
1319
1387
  ])
1320
1388
  .groupBy('reports.createdBy')
1321
1389
  .execute()
1322
1390
  }
1323
-
1324
- return query
1325
- .select([
1326
- (eb) =>
1327
- eb.fn.count<number>('reports.id').as(`${subjectType}ReportCount`),
1328
- (eb) =>
1329
- eb.fn
1330
- .count<number>(
1331
- isAccount ? 'reports.subjectDid' : 'reports.subjectUri',
1332
- )
1333
- .distinct()
1334
- .as(`reported${isAccount ? 'Account' : 'Record'}Count`),
1335
- ])
1336
- .groupBy('reports.createdBy')
1337
- .execute()
1338
1391
  }
1339
1392
 
1340
1393
  async getReporterStats(dids: string[]) {
package/src/team/index.ts CHANGED
@@ -54,6 +54,7 @@ export class TeamService {
54
54
  (r) =>
55
55
  r === 'tools.ozone.team.defs#roleAdmin' ||
56
56
  r === 'tools.ozone.team.defs#roleModerator' ||
57
+ r === 'tools.ozone.team.defs#roleVerifier' ||
57
58
  r === 'tools.ozone.team.defs#roleTriage',
58
59
  )
59
60
 
@@ -197,11 +198,14 @@ export class TeamService {
197
198
  isAdmin || member?.role === 'tools.ozone.team.defs#roleModerator'
198
199
  const isTriage =
199
200
  isModerator || member?.role === 'tools.ozone.team.defs#roleTriage'
201
+ const isVerifier =
202
+ isAdmin || member?.role === 'tools.ozone.team.defs#roleVerifier'
200
203
 
201
204
  return {
202
205
  isModerator,
203
206
  isAdmin,
204
207
  isTriage,
208
+ isVerifier,
205
209
  }
206
210
  }
207
211
 
@@ -0,0 +1,135 @@
1
+ import { Selectable } from 'kysely'
2
+ import { Agent, AtUri, CredentialSession } from '@atproto/api'
3
+ import { VerifierConfig } from '../config'
4
+ import { Verification } from '../db/schema/verification'
5
+
6
+ export type VerificationInput = {
7
+ displayName: string
8
+ handle: string
9
+ subject: string
10
+ createdAt?: string
11
+ }
12
+
13
+ export type VerificationIssuerCreator = (
14
+ verifierConfig: VerifierConfig,
15
+ ) => VerificationIssuer
16
+
17
+ export class VerificationIssuer {
18
+ private session = new CredentialSession(new URL(this.verifierConfig.url))
19
+ private agent = new Agent(this.session)
20
+ constructor(private verifierConfig: VerifierConfig) {}
21
+
22
+ static creator() {
23
+ return (verifierConfig: VerifierConfig) =>
24
+ new VerificationIssuer(verifierConfig)
25
+ }
26
+
27
+ async getAgent() {
28
+ if (!this.session.hasSession) {
29
+ await this.session.login({
30
+ identifier: this.verifierConfig.did,
31
+ password: this.verifierConfig.password,
32
+ })
33
+ }
34
+
35
+ // Trigger a test request to check if the session is still valid, if not, we will login again
36
+ try {
37
+ await this.agent.com.atproto.server.getSession()
38
+ } catch (err) {
39
+ if ((err as any).status === 401) {
40
+ await this.session.login({
41
+ identifier: this.verifierConfig.did,
42
+ password: this.verifierConfig.password,
43
+ })
44
+ }
45
+ }
46
+
47
+ return this.agent
48
+ }
49
+
50
+ async verify(verifications: VerificationInput[]) {
51
+ const grantedVerifications: Selectable<Verification>[] = []
52
+ const failedVerifications: {
53
+ $type: 'tools.ozone.verification.grantVerifications#grantError'
54
+ subject: string
55
+ error: string
56
+ }[] = []
57
+ const now = new Date().toISOString()
58
+ const agent = await this.getAgent()
59
+ await Promise.allSettled(
60
+ verifications.map(async ({ displayName, handle, subject, createdAt }) => {
61
+ try {
62
+ const verificationRecord = {
63
+ createdAt: createdAt || now,
64
+ issuer: this.verifierConfig.did,
65
+ displayName,
66
+ handle,
67
+ subject,
68
+ }
69
+ const {
70
+ data: { uri, cid },
71
+ } = await agent.com.atproto.repo.createRecord({
72
+ repo: this.verifierConfig.did,
73
+ record: verificationRecord,
74
+ collection: 'app.bsky.graph.verification',
75
+ })
76
+ grantedVerifications.push({
77
+ ...verificationRecord,
78
+ uri,
79
+ cid,
80
+ revokedAt: null,
81
+ updatedAt: now,
82
+ revokedBy: null,
83
+ revokeReason: null,
84
+ })
85
+ } catch (err) {
86
+ failedVerifications.push({
87
+ $type: 'tools.ozone.verification.grantVerifications#grantError',
88
+ error: (err as Error).message,
89
+ subject,
90
+ })
91
+ return
92
+ }
93
+ }),
94
+ )
95
+
96
+ return { grantedVerifications, failedVerifications }
97
+ }
98
+
99
+ async revoke({ uris }: { uris: string[] }) {
100
+ const revokedVerifications: string[] = []
101
+ const failedRevocations: Array<{ uri: string; error: string }> = []
102
+
103
+ const agent = await this.getAgent()
104
+
105
+ await Promise.allSettled(
106
+ uris.map(async (uri) => {
107
+ try {
108
+ const atUri = new AtUri(uri)
109
+
110
+ if (atUri.collection !== 'app.bsky.graph.verification') {
111
+ throw new Error(`Only verification records can be revoked`)
112
+ }
113
+
114
+ if (atUri.host !== this.verifierConfig.did) {
115
+ throw new Error(
116
+ `Cannot revoke verification record ${uri} not issued by ${this.verifierConfig.did}`,
117
+ )
118
+ }
119
+
120
+ await agent.com.atproto.repo.deleteRecord({
121
+ collection: atUri.collection,
122
+ repo: this.verifierConfig.did,
123
+ rkey: atUri.rkey,
124
+ })
125
+ revokedVerifications.push(uri)
126
+ } catch (err) {
127
+ failedRevocations.push({ uri, error: (err as Error).message })
128
+ return
129
+ }
130
+ }),
131
+ )
132
+
133
+ return { revokedVerifications, failedRevocations }
134
+ }
135
+ }
@@ -0,0 +1,208 @@
1
+ import { Selectable } from 'kysely'
2
+ import {
3
+ $Typed,
4
+ AppBskyActorDefs,
5
+ AtUri,
6
+ ToolsOzoneModerationDefs,
7
+ ToolsOzoneVerificationDefs,
8
+ } from '@atproto/api'
9
+ import { Database } from '../db'
10
+ import { CreatedAtUriKeyset, paginate } from '../db/pagination'
11
+ import { Verification } from '../db/schema/verification'
12
+
13
+ export type VerificationServiceCreator = (db: Database) => VerificationService
14
+
15
+ export class VerificationService {
16
+ constructor(public db: Database) {}
17
+
18
+ static creator() {
19
+ return (db: Database) => new VerificationService(db)
20
+ }
21
+
22
+ async create(
23
+ verifications: Pick<
24
+ Verification,
25
+ | 'uri'
26
+ | 'issuer'
27
+ | 'subject'
28
+ | 'handle'
29
+ | 'displayName'
30
+ | 'createdAt'
31
+ | 'cid'
32
+ >[],
33
+ ) {
34
+ return this.db.transaction(async (tx) => {
35
+ return tx.db
36
+ .insertInto('verification')
37
+ .values(verifications)
38
+ .onConflict((oc) => oc.doNothing())
39
+ .returningAll()
40
+ .execute()
41
+ })
42
+ }
43
+
44
+ async markRevoked({
45
+ uris,
46
+ revokedBy,
47
+ revokedAt,
48
+ revokeReason,
49
+ }: {
50
+ uris: string[]
51
+ revokedBy?: string
52
+ revokedAt?: string
53
+ revokeReason?: string
54
+ }) {
55
+ const now = new Date().toISOString()
56
+ return this.db.transaction(async (tx) => {
57
+ for (const uri of uris) {
58
+ return tx.db
59
+ .updateTable('verification')
60
+ .set({
61
+ revokeReason,
62
+ updatedAt: now,
63
+ revokedAt: revokedAt || now,
64
+ // Allow setting revokedBy to a moderator/verifier DID and if it isn't set, default to the author of the verification record
65
+ revokedBy: revokedBy || new AtUri(uri).host,
66
+ })
67
+ .where('uri', '=', uri)
68
+ .where('revokedAt', 'is', null)
69
+ .execute()
70
+ }
71
+ })
72
+ }
73
+
74
+ async list({
75
+ sortDirection,
76
+ cursor,
77
+ createdAfter,
78
+ createdBefore,
79
+ issuers = [],
80
+ subjects = [],
81
+ isRevoked,
82
+ limit = 100,
83
+ }: {
84
+ sortDirection?: 'asc' | 'desc'
85
+ cursor?: string
86
+ createdAfter?: string
87
+ createdBefore?: string
88
+ issuers?: string[]
89
+ subjects?: string[]
90
+ isRevoked?: boolean
91
+ limit?: number
92
+ }) {
93
+ const { ref } = this.db.db.dynamic
94
+
95
+ let qb = this.db.db.selectFrom('verification').selectAll()
96
+
97
+ if (issuers.length) {
98
+ qb = qb.where('issuer', 'in', issuers)
99
+ }
100
+
101
+ if (isRevoked !== undefined) {
102
+ qb = qb.where('revokedAt', isRevoked ? 'is not' : 'is', null)
103
+ }
104
+
105
+ if (subjects.length) {
106
+ qb = qb.where('subject', 'in', subjects)
107
+ }
108
+
109
+ if (createdAfter) {
110
+ qb = qb.where('createdAt', '>=', createdAfter)
111
+ }
112
+
113
+ if (createdBefore) {
114
+ qb = qb.where('createdAt', '<=', createdBefore)
115
+ }
116
+
117
+ const keyset = new CreatedAtUriKeyset(ref(`createdAt`), ref('uri'))
118
+ const paginatedBuilder = paginate(qb, {
119
+ limit,
120
+ cursor,
121
+ keyset,
122
+ tryIndex: true,
123
+ direction: sortDirection === 'desc' ? 'desc' : 'asc',
124
+ })
125
+
126
+ const result = await paginatedBuilder.execute()
127
+ return { verifications: result, cursor: keyset.packFromResult(result) }
128
+ }
129
+
130
+ view(
131
+ verifications: Selectable<Verification>[],
132
+ repos: Map<
133
+ string,
134
+ | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>
135
+ | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>
136
+ >,
137
+ profiles: Map<string, AppBskyActorDefs.ProfileViewDetailed>,
138
+ ): $Typed<ToolsOzoneVerificationDefs.VerificationView>[] {
139
+ return verifications.map((verification) => {
140
+ const issuerRepo = repos.get(verification.issuer)
141
+ const subjectRepo = repos.get(verification.subject)
142
+ const subjectProfile = profiles.get(verification.subject)
143
+ const issuerProfile = profiles.get(verification.issuer)
144
+ return {
145
+ $type: 'tools.ozone.verification.defs#verificationView',
146
+ uri: verification.uri,
147
+ issuer: verification.issuer,
148
+ subject: verification.subject,
149
+ createdAt: verification.createdAt,
150
+ displayName: verification.displayName,
151
+ handle: verification.handle,
152
+ updatedAt: verification.updatedAt || undefined,
153
+ revokedAt: verification.revokedAt || undefined,
154
+ revokedBy: verification.revokedBy || undefined,
155
+ revokeReason: verification.revokeReason || undefined,
156
+ issuerRepo,
157
+ subjectRepo,
158
+ subjectProfile: subjectProfile
159
+ ? {
160
+ $type: 'app.bsky.actor.defs#profileViewDetailed',
161
+ ...subjectProfile,
162
+ }
163
+ : undefined,
164
+ issuerProfile: issuerProfile
165
+ ? {
166
+ $type: 'app.bsky.actor.defs#profileViewDetailed',
167
+ ...issuerProfile,
168
+ }
169
+ : undefined,
170
+ }
171
+ })
172
+ }
173
+
174
+ async getFirehoseCursor() {
175
+ const entry = await this.db.db
176
+ .selectFrom('firehose_cursor')
177
+ .select('cursor')
178
+ .where('service', '=', 'verification')
179
+ .executeTakeFirst()
180
+
181
+ return entry?.cursor || null
182
+ }
183
+
184
+ createFirehoseCursor() {
185
+ return this.db.db
186
+ .insertInto('firehose_cursor')
187
+ .values({
188
+ service: 'verification',
189
+ cursor: null,
190
+ })
191
+ .onConflict((oc) => oc.doNothing())
192
+ .execute()
193
+ }
194
+
195
+ async updateFirehoseCursor(cursor: number) {
196
+ const updated = await this.db.db
197
+ .updateTable('firehose_cursor')
198
+ .set({ cursor })
199
+ .where('service', '=', 'verification')
200
+ .where((qb) =>
201
+ qb.where('cursor', '<', cursor).orWhere('cursor', 'is', null),
202
+ )
203
+ .returningAll()
204
+ .executeTakeFirst()
205
+
206
+ return updated?.cursor
207
+ }
208
+ }
@@ -0,0 +1,50 @@
1
+ import { $Typed, ToolsOzoneModerationDefs } from '@atproto/api'
2
+ import { addAccountInfoToRepoViewDetail, getPdsAccountInfos } from '../api/util'
3
+ import { AppContext } from '../context'
4
+ import { ModerationService } from '../mod-service'
5
+ import { ParsedLabelers } from '../util'
6
+
7
+ export const getReposForVerifications = async (
8
+ ctx: AppContext,
9
+ labelers: ParsedLabelers,
10
+ modService: ModerationService,
11
+ dids: string[],
12
+ isModerator: boolean,
13
+ ) => {
14
+ const [partialRepos, accountInfo] = await Promise.all([
15
+ modService.views.repoDetails(dids, labelers),
16
+ getPdsAccountInfos(ctx, dids),
17
+ ])
18
+
19
+ const repos = new Map<
20
+ string,
21
+ | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>
22
+ | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>
23
+ >(
24
+ dids.map((did) => {
25
+ const partialRepo = partialRepos.get(did)
26
+ if (!partialRepo) {
27
+ return [
28
+ did,
29
+ {
30
+ did,
31
+ $type: 'tools.ozone.moderation.defs#repoViewNotFound',
32
+ },
33
+ ]
34
+ }
35
+ return [
36
+ did,
37
+ {
38
+ ...addAccountInfoToRepoViewDetail(
39
+ partialRepo,
40
+ accountInfo.get(did) || null,
41
+ isModerator,
42
+ ),
43
+ $type: 'tools.ozone.moderation.defs#repoViewDetail',
44
+ },
45
+ ]
46
+ }),
47
+ )
48
+
49
+ return repos
50
+ }