@atproto/ozone 0.1.107 → 0.1.109
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.
- package/CHANGELOG.md +27 -0
- package/dist/api/health.js +1 -1
- package/dist/api/health.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +6 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/server/getConfig.d.ts.map +1 -1
- package/dist/api/server/getConfig.js +1 -0
- package/dist/api/server/getConfig.js.map +1 -1
- package/dist/api/setting/removeOptions.d.ts.map +1 -1
- package/dist/api/setting/removeOptions.js +1 -0
- package/dist/api/setting/removeOptions.js.map +1 -1
- package/dist/api/setting/upsertOption.js +7 -0
- package/dist/api/setting/upsertOption.js.map +1 -1
- package/dist/api/util.d.ts +1 -1
- package/dist/api/util.d.ts.map +1 -1
- package/dist/api/util.js +6 -1
- package/dist/api/util.js.map +1 -1
- package/dist/api/verification/grantVerifications.d.ts +4 -0
- package/dist/api/verification/grantVerifications.d.ts.map +1 -0
- package/dist/api/verification/grantVerifications.js +60 -0
- package/dist/api/verification/grantVerifications.js.map +1 -0
- package/dist/api/verification/listVerifications.d.ts +4 -0
- package/dist/api/verification/listVerifications.d.ts.map +1 -0
- package/dist/api/verification/listVerifications.js +32 -0
- package/dist/api/verification/listVerifications.js.map +1 -0
- package/dist/api/verification/revokeVerifications.d.ts +4 -0
- package/dist/api/verification/revokeVerifications.d.ts.map +1 -0
- package/dist/api/verification/revokeVerifications.js +36 -0
- package/dist/api/verification/revokeVerifications.js.map +1 -0
- package/dist/auth-verifier.d.ts +4 -1
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +4 -3
- package/dist/auth-verifier.js.map +1 -1
- package/dist/background.d.ts +3 -1
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +4 -3
- package/dist/background.js.map +1 -1
- package/dist/config/config.d.ts +9 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +10 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +5 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +5 -0
- package/dist/config/env.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -0
- package/dist/context.js.map +1 -1
- package/dist/daemon/context.d.ts +3 -0
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +11 -0
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/verification-listener.d.ts +29 -0
- package/dist/daemon/verification-listener.d.ts.map +1 -0
- package/dist/daemon/verification-listener.js +171 -0
- package/dist/daemon/verification-listener.js.map +1 -0
- package/dist/db/migrations/20250415T201720309Z-verification.d.ts +4 -0
- package/dist/db/migrations/20250415T201720309Z-verification.d.ts.map +1 -0
- package/dist/db/migrations/20250415T201720309Z-verification.js +35 -0
- package/dist/db/migrations/20250415T201720309Z-verification.js.map +1 -0
- package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts +4 -0
- package/dist/db/migrations/20250417T201720309Z-firehose-cursor.d.ts.map +1 -0
- package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js +17 -0
- package/dist/db/migrations/20250417T201720309Z-firehose-cursor.js.map +1 -0
- package/dist/db/migrations/index.d.ts +2 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +3 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/pagination.d.ts +15 -0
- package/dist/db/pagination.d.ts.map +1 -1
- package/dist/db/pagination.js +23 -1
- package/dist/db/pagination.js.map +1 -1
- package/dist/db/schema/firehose_cursor.d.ts +11 -0
- package/dist/db/schema/firehose_cursor.d.ts.map +1 -0
- package/dist/db/schema/firehose_cursor.js +5 -0
- package/dist/db/schema/firehose_cursor.js.map +1 -0
- package/dist/db/schema/index.d.ts +3 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/member.d.ts +1 -1
- package/dist/db/schema/member.d.ts.map +1 -1
- package/dist/db/schema/verification.d.ts +19 -0
- package/dist/db/schema/verification.d.ts.map +1 -0
- package/dist/db/schema/verification.js +5 -0
- package/dist/db/schema/verification.js.map +1 -0
- package/dist/error.js +1 -1
- package/dist/error.js.map +1 -1
- package/dist/jetstream/service.d.ts +60 -0
- package/dist/jetstream/service.d.ts.map +1 -0
- package/dist/jetstream/service.js +65 -0
- package/dist/jetstream/service.js.map +1 -0
- package/dist/lexicon/index.d.ts +15 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +36 -1
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +918 -98
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +434 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +21 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.js +9 -0
- package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/status.d.ts +23 -0
- package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/actor/status.js +19 -0
- package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts +3 -1
- package/dist/lexicon/types/tools/ozone/server/getConfig.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/server/getConfig.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/setting/defs.d.ts +1 -1
- package/dist/lexicon/types/tools/ozone/setting/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/setting/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts +1 -1
- package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/team/addMember.d.ts +1 -1
- package/dist/lexicon/types/tools/ozone/team/addMember.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/team/defs.d.ts +3 -1
- package/dist/lexicon/types/tools/ozone/team/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/team/defs.js +3 -1
- package/dist/lexicon/types/tools/ozone/team/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts +1 -1
- package/dist/lexicon/types/tools/ozone/team/updateMember.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/verification/defs.d.ts +43 -0
- package/dist/lexicon/types/tools/ozone/verification/defs.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/defs.js +16 -0
- package/dist/lexicon/types/tools/ozone/verification/defs.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts +66 -0
- package/dist/lexicon/types/tools/ozone/verification/grantVerifications.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js +25 -0
- package/dist/lexicon/types/tools/ozone/verification/grantVerifications.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts +52 -0
- package/dist/lexicon/types/tools/ozone/verification/listVerifications.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/listVerifications.js +7 -0
- package/dist/lexicon/types/tools/ozone/verification/listVerifications.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts +56 -0
- package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js +16 -0
- package/dist/lexicon/types/tools/ozone/verification/revokeVerifications.js.map +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +2 -1
- package/dist/logger.js.map +1 -1
- package/dist/mod-service/index.js +1 -1
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +6 -0
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +2 -0
- package/dist/mod-service/views.js.map +1 -1
- package/dist/team/index.d.ts +1 -0
- package/dist/team/index.d.ts.map +1 -1
- package/dist/team/index.js +5 -2
- package/dist/team/index.js.map +1 -1
- package/dist/verification/issuer.d.ts +37 -0
- package/dist/verification/issuer.d.ts.map +1 -0
- package/dist/verification/issuer.js +119 -0
- package/dist/verification/issuer.js.map +1 -0
- package/dist/verification/service.d.ts +47 -0
- package/dist/verification/service.d.ts.map +1 -0
- package/dist/verification/service.js +141 -0
- package/dist/verification/service.js.map +1 -0
- package/dist/verification/util.d.ts +6 -0
- package/dist/verification/util.d.ts.map +1 -0
- package/dist/verification/util.js +32 -0
- package/dist/verification/util.js.map +1 -0
- package/package.json +10 -9
- package/src/api/health.ts +1 -1
- package/src/api/index.ts +6 -0
- package/src/api/server/getConfig.ts +1 -0
- package/src/api/setting/removeOptions.ts +1 -0
- package/src/api/setting/upsertOption.ts +7 -0
- package/src/api/util.ts +7 -1
- package/src/api/verification/grantVerifications.ts +90 -0
- package/src/api/verification/listVerifications.ts +44 -0
- package/src/api/verification/revokeVerifications.ts +43 -0
- package/src/auth-verifier.ts +8 -4
- package/src/background.ts +8 -3
- package/src/config/config.ts +21 -0
- package/src/config/env.ts +10 -0
- package/src/context.ts +22 -0
- package/src/daemon/context.ts +19 -0
- package/src/daemon/verification-listener.ts +164 -0
- package/src/db/migrations/20250415T201720309Z-verification.ts +34 -0
- package/src/db/migrations/20250417T201720309Z-firehose-cursor.ts +16 -0
- package/src/db/migrations/index.ts +2 -0
- package/src/db/pagination.ts +31 -0
- package/src/db/schema/firehose_cursor.ts +13 -0
- package/src/db/schema/index.ts +5 -1
- package/src/db/schema/member.ts +1 -0
- package/src/db/schema/verification.ts +21 -0
- package/src/error.ts +1 -1
- package/src/jetstream/service.ts +104 -0
- package/src/lexicon/index.ts +50 -0
- package/src/lexicon/lexicons.ts +457 -0
- package/src/lexicon/types/app/bsky/actor/defs.ts +26 -0
- package/src/lexicon/types/app/bsky/actor/status.ts +40 -0
- package/src/lexicon/types/tools/ozone/server/getConfig.ts +3 -0
- package/src/lexicon/types/tools/ozone/setting/defs.ts +1 -0
- package/src/lexicon/types/tools/ozone/setting/upsertOption.ts +1 -0
- package/src/lexicon/types/tools/ozone/team/addMember.ts +1 -0
- package/src/lexicon/types/tools/ozone/team/defs.ts +3 -0
- package/src/lexicon/types/tools/ozone/team/updateMember.ts +1 -0
- package/src/lexicon/types/tools/ozone/verification/defs.ts +59 -0
- package/src/lexicon/types/tools/ozone/verification/grantVerifications.ts +100 -0
- package/src/lexicon/types/tools/ozone/verification/listVerifications.ts +70 -0
- package/src/lexicon/types/tools/ozone/verification/revokeVerifications.ts +81 -0
- package/src/logger.ts +2 -0
- package/src/mod-service/index.ts +1 -1
- package/src/mod-service/views.ts +4 -0
- package/src/team/index.ts +6 -5
- package/src/verification/issuer.ts +135 -0
- package/src/verification/service.ts +208 -0
- package/src/verification/util.ts +50 -0
- package/tests/__snapshots__/verification-listener.test.ts.snap +146 -0
- package/tests/__snapshots__/verification.test.ts.snap +288 -0
- package/tests/expiring-label.test.ts +72 -0
- package/tests/verification-listener.test.ts +102 -0
- package/tests/verification.test.ts +166 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
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 profilesBefore = await modViews.getProfiles(
|
|
24
|
+
input.body.verifications.map((v) => v.subject),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Filter out any subject for which, the current issuer already has a valid verification record indexed
|
|
28
|
+
const verificationsToBeGranted = input.body.verifications.filter(
|
|
29
|
+
(verificationInput) => {
|
|
30
|
+
const hasValidVerification = profilesBefore
|
|
31
|
+
.get(verificationInput.subject)
|
|
32
|
+
?.verification?.verifications.find(
|
|
33
|
+
(v) => v.issuer === ctx.cfg.verifier?.did && v.isValid,
|
|
34
|
+
)
|
|
35
|
+
return !hasValidVerification
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)
|
|
40
|
+
const verificationService = ctx.verificationService(ctx.db)
|
|
41
|
+
const { grantedVerifications, failedVerifications } =
|
|
42
|
+
await verificationIssuer.verify(verificationsToBeGranted)
|
|
43
|
+
|
|
44
|
+
if (!grantedVerifications.length) {
|
|
45
|
+
return {
|
|
46
|
+
encoding: 'application/json',
|
|
47
|
+
body: {
|
|
48
|
+
verifications: [],
|
|
49
|
+
failedVerifications,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const createdVerifications: Selectable<Verification>[] = []
|
|
55
|
+
const verificationEntries =
|
|
56
|
+
await verificationService.create(grantedVerifications)
|
|
57
|
+
|
|
58
|
+
const dids = new Set<string>([ctx.cfg.verifier.did])
|
|
59
|
+
|
|
60
|
+
for (const verification of verificationEntries) {
|
|
61
|
+
createdVerifications.push(verification)
|
|
62
|
+
dids.add(verification.subject)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const didsArr = Array.from(dids)
|
|
66
|
+
const [repos, profiles] = await Promise.all([
|
|
67
|
+
getReposForVerifications(
|
|
68
|
+
ctx,
|
|
69
|
+
ctx.reqLabelers(req),
|
|
70
|
+
ctx.modService(ctx.db),
|
|
71
|
+
didsArr,
|
|
72
|
+
auth.credentials.isModerator,
|
|
73
|
+
),
|
|
74
|
+
modViews.getProfiles(didsArr),
|
|
75
|
+
])
|
|
76
|
+
const verifications = verificationService.view(
|
|
77
|
+
createdVerifications,
|
|
78
|
+
repos,
|
|
79
|
+
profiles,
|
|
80
|
+
)
|
|
81
|
+
return {
|
|
82
|
+
encoding: 'application/json',
|
|
83
|
+
body: {
|
|
84
|
+
verifications,
|
|
85
|
+
failedVerifications,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
@@ -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
|
+
}
|
package/src/auth-verifier.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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(
|
|
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 {
|
|
@@ -58,7 +63,7 @@ export class BackgroundQueue {
|
|
|
58
63
|
await task(this.db, abortController.signal)
|
|
59
64
|
} catch (err) {
|
|
60
65
|
if (!isCausedBySignal(err, abortController.signal)) {
|
|
61
|
-
dbLogger.error(err, 'background queue task failed')
|
|
66
|
+
dbLogger.error({ err }, 'background queue task failed')
|
|
62
67
|
}
|
|
63
68
|
} finally {
|
|
64
69
|
abortController.abort()
|
package/src/config/config.ts
CHANGED
|
@@ -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
|
}
|
package/src/daemon/context.ts
CHANGED
|
@@ -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'
|