diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 0dacf02bcf5..83d1be3f95e 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -16,18 +16,16 @@ import { ModerationService } from '../../../../services/moderation' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfile({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ auth, params, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const modService = ctx.services.moderation(ctx.db.getPrimary()) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const [result, repoRev] = await Promise.allSettled([ getProfile( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, modService }, ), actorService.getRepoRev(viewer), @@ -53,12 +51,12 @@ const skeleton = async ( ctx: Context, ): Promise => { const { actorService, modService } = ctx - const { canViewTakendownProfile } = params + const { canViewTakedowns } = params const actor = await actorService.getActor(params.actor, true) if (!actor) { throw new InvalidRequestError('Profile not found') } - if (!canViewTakendownProfile && softDeleted(actor)) { + if (!canViewTakedowns && softDeleted(actor)) { const isSuspended = await modService.isSubjectSuspended(actor.did) if (isSuspended) { throw new InvalidRequestError( @@ -78,10 +76,10 @@ const skeleton = async ( const hydration = async (state: SkeletonState, ctx: Context) => { const { actorService } = ctx const { params, actor } = state - const { viewer, canViewTakendownProfile } = params + const { viewer, canViewTakedowns } = params const hydration = await actorService.views.profileDetailHydration( [actor.did], - { viewer, includeSoftDeleted: canViewTakendownProfile }, + { viewer, includeSoftDeleted: canViewTakedowns }, ) return { ...state, ...hydration } } @@ -110,7 +108,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { params: Params; actor: Actor } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index f2e0eb3fd50..21ca13949d2 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -13,11 +13,11 @@ import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfiles({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const [result, repoRev] = await Promise.all([ getProfile({ ...params, viewer }, { db, actorService }), diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index f68ba68eb66..df580521af9 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -17,12 +17,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.actor.getSuggestions({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getSuggestions( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 66e934ac0b3..fb95ecd29d3 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -9,10 +9,10 @@ import { export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { cursor, limit } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const rawQuery = params.q ?? params.term const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index da612edcc87..6a3167fd2d0 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,10 +7,10 @@ import { export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { limit } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const rawQuery = params.q ?? params.term const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 7a28e4efe67..bc4ecd7caac 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -6,10 +6,10 @@ import { TimeCidKeyset, paginate } from '../../../../db/pagination' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getActorFeeds({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { actor, limit, cursor } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 36e36b0100b..151e9086ca9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -23,9 +23,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getActorLikes({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 342f371f18d..f2163cd251b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -23,14 +23,13 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) const graphService = ctx.services.graph(db) - const viewer = - auth.credentials.type === 'access' ? auth.credentials.did : null + const { viewer } = ctx.authVerifier.parseCreds(auth) const [result, repoRev] = await Promise.all([ getAuthorFeed( diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index a09258c3163..5100ec0f5ab 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -33,11 +33,11 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeed({ - auth: ctx.authOptionalVerifierAnyAudience, + auth: ctx.authVerifier.standardOptionalAnyAud, handler: async ({ params, auth, req }) => { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const { timerSkele, timerHydr, ...result } = await getFeed( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 14a5688db0d..125af1db9b9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -9,10 +9,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerator({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feed } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index 7b571ab09f6..ed6df5760cb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -14,10 +14,10 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeedGenerators({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feeds } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts index 5d65044f86f..4ab22c3a0b1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts @@ -5,10 +5,10 @@ import { toSkeletonItem } from '../../../../feed-gen/types' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedSkeleton({ - auth: ctx.authVerifierAnyAudience, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feed } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const localAlgo = ctx.algos[feed] if (!localAlgo) { diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 893617f6bb0..8df916f29c9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -13,12 +13,12 @@ import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getLikes = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getLikes({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getLikes( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index fd3f0360ef3..8af7764a6b7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -22,9 +22,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getListFeed({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 873dd311ba0..dd4e8bb19d3 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -31,9 +31,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getPostThread({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { - const viewer = 'did' in auth.credentials ? auth.credentials.did : null + const { viewer } = ctx.authVerifier.parseCreds(auth) const db = ctx.db.getReplica('thread') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 5ec4807accb..9db7cf0a252 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -14,12 +14,12 @@ import { ActorService } from '../../../../services/actor' export default function (server: Server, ctx: AppContext) { const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getPosts({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const results = await getPosts( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index 5ca5c452b63..e84bb745b42 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -18,12 +18,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getRepostedBy({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getRepostedBy( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index 35fac829039..b72a191c9aa 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -4,9 +4,9 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 18cc5c2629a..03ff86fa981 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -21,9 +21,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getTimeline({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica('timeline') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index db143fc5b8c..9598c6ff88c 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -21,9 +21,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.searchPosts({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica('search') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index 66b809d70ce..518fd2d62ec 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -5,10 +5,10 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getBlocks({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 1382c1f87c7..9fb199c7563 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -19,17 +19,15 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getFollowers({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const result = await getFollowers( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, graphService }, ) @@ -46,10 +44,10 @@ const skeleton = async ( ctx: Context, ): Promise => { const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params + const { limit, cursor, actor, canViewTakedowns } = params const { ref } = db.db.dynamic - const subject = await actorService.getActor(actor, canViewTakendownProfile) + const subject = await actorService.getActor(actor, canViewTakedowns) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -58,7 +56,7 @@ const skeleton = async ( .selectFrom('follow') .where('follow.subjectDid', '=', subject.did) .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .if(!canViewTakendownProfile, (qb) => + .if(!canViewTakedowns, (qb) => qb.where(notSoftDeletedClause(ref('creator'))), ) .selectAll('creator') @@ -130,7 +128,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 34b5d72a605..2195824b696 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -19,17 +19,15 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getFollows({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const result = await getFollows( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, graphService }, ) @@ -46,10 +44,10 @@ const skeleton = async ( ctx: Context, ): Promise => { const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params + const { limit, cursor, actor, canViewTakedowns } = params const { ref } = db.db.dynamic - const creator = await actorService.getActor(actor, canViewTakendownProfile) + const creator = await actorService.getActor(actor, canViewTakedowns) if (!creator) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -58,7 +56,7 @@ const skeleton = async ( .selectFrom('follow') .where('follow.creator', '=', creator.did) .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .if(!canViewTakendownProfile, (qb) => + .if(!canViewTakedowns, (qb) => qb.where(notSoftDeletedClause(ref('subject'))), ) .selectAll('subject') @@ -131,7 +129,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 82a70848cd9..08d3f725663 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -13,12 +13,12 @@ import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getList = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getList({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const graphService = ctx.services.graph(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getList( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts index 03fd3496f97..b5a6e97986d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -17,12 +17,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getListBlocks({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const graphService = ctx.services.graph(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getListBlocks( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index ab0ac77f47c..f5f14844e32 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -5,10 +5,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 73deb51900b..888963b3fa3 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -6,10 +6,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getLists({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { actor, limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index e69803d144a..2481e8de240 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -5,10 +5,10 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getMutes({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index eddf0cd5fd6..3aec8ded48e 100644 --- a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -9,10 +9,10 @@ const RESULT_LENGTH = 10 export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getSuggestedFollowsByActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { const { actor } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/graph/muteActor.ts b/packages/bsky/src/api/app/bsky/graph/muteActor.ts index 50a3723db6e..acf72bdd2eb 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActor.ts @@ -4,10 +4,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { actor } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() const subjectDid = await ctx.services.actor(db).getActorDid(actor) diff --git a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts index b6b29796c5c..d732c3cd89f 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts @@ -6,10 +6,10 @@ import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActorList({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { list } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts index 11af919126f..5308aef4f47 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts @@ -4,10 +4,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { actor } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() const subjectDid = await ctx.services.actor(db).getActorDid(actor) diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts index 8b97530c216..059fa5605d9 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts @@ -3,10 +3,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActorList({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { list } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() await ctx.services.graph(db).unmuteActorList({ diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index c23d7683abe..71391457902 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -6,9 +6,9 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.getUnreadCount({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { - const requester = auth.credentials.did + const requester = auth.credentials.iss if (params.seenAt) { throw new InvalidRequestError('The seenAt parameter is unsupported') } diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 672e8c0997a..c0de1925120 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -20,13 +20,13 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.notification.listNotifications({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) const labelService = ctx.services.label(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await listNotifications( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/notification/registerPush.ts b/packages/bsky/src/api/app/bsky/notification/registerPush.ts index be7d373bcd4..9645cd76c83 100644 --- a/packages/bsky/src/api/app/bsky/notification/registerPush.ts +++ b/packages/bsky/src/api/app/bsky/notification/registerPush.ts @@ -5,13 +5,11 @@ import { Platform } from '../../../../notifications' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.registerPush({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { token, platform, serviceDid, appId } = input.body - const { - credentials: { did }, - } = auth - if (serviceDid !== auth.artifacts.aud) { + const did = auth.credentials.iss + if (serviceDid !== auth.credentials.aud) { throw new InvalidRequestError('Invalid serviceDid.') } const { notifServer } = ctx diff --git a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts index b7c705c0889..4b8b614fbad 100644 --- a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts @@ -5,10 +5,10 @@ import { excluded } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ input, auth }) => { const { seenAt } = input.body - const viewer = auth.credentials.did + const viewer = auth.credentials.iss let parsed: string try { diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index e135d2cb7c1..b8456d111a4 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,10 +8,10 @@ import { GeneratorView } from '../../../../lexicon/types/app/bsky/feed/defs' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { limit, cursor, query } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index 821eeda655f..f45b657af1e 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -6,11 +6,11 @@ import { toSkeletonItem } from '../../../../feed-gen/types' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getTimelineSkeleton({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { const db = ctx.db.getReplica('timeline') const feedService = ctx.services.feed(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await skeleton({ ...params, viewer }, { db, feedService }) diff --git a/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts b/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts deleted file mode 100644 index 1cbc957d493..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { - AuthRequiredError, - InvalidRequestError, - UpstreamFailureError, -} from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getSubject } from '../moderation/util' -import { - isModEventLabel, - isModEventReverseTakedown, - isModEventTakedown, -} from '../../../../lexicon/types/com/atproto/admin/defs' -import { TakedownSubjects } from '../../../../services/moderation' -import { retryHttp } from '../../../../util/retry' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.emitModerationEvent({ - auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { - const access = auth.credentials - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const { subject, createdBy, subjectBlobCids, event } = input.body - const isTakedownEvent = isModEventTakedown(event) - const isReverseTakedownEvent = isModEventReverseTakedown(event) - const isLabelEvent = isModEventLabel(event) - - // apply access rules - - // if less than moderator access then can not takedown an account - if (!access.moderator && isTakedownEvent && 'did' in subject) { - throw new AuthRequiredError( - 'Must be a full moderator to perform an account takedown', - ) - } - // if less than moderator access then can only take ack and escalation actions - if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) { - throw new AuthRequiredError( - 'Must be a full moderator to take this type of action', - ) - } - // if less than moderator access then can not apply labels - if (!access.moderator && isLabelEvent) { - throw new AuthRequiredError('Must be a full moderator to label content') - } - - if (isLabelEvent) { - validateLabels([ - ...(event.createLabelVals ?? []), - ...(event.negateLabelVals ?? []), - ]) - } - - const subjectInfo = getSubject(subject) - - if (isTakedownEvent || isReverseTakedownEvent) { - const isSubjectTakendown = await moderationService.isSubjectTakendown( - subjectInfo, - ) - - if (isSubjectTakendown && isTakedownEvent) { - throw new InvalidRequestError(`Subject is already taken down`) - } - - if (!isSubjectTakendown && isReverseTakedownEvent) { - throw new InvalidRequestError(`Subject is not taken down`) - } - } - - const { result: moderationEvent, takenDown } = await db.transaction( - async (dbTxn) => { - const moderationTxn = ctx.services.moderation(dbTxn) - const labelTxn = ctx.services.label(dbTxn) - - const result = await moderationTxn.logEvent({ - event, - subject: subjectInfo, - subjectBlobCids: - subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [], - createdBy, - }) - - let takenDown: TakedownSubjects | undefined - - if ( - result.subjectType === 'com.atproto.admin.defs#repoRef' && - result.subjectDid - ) { - // No credentials to revoke on appview - if (isTakedownEvent) { - takenDown = await moderationTxn.takedownRepo({ - takedownId: result.id.toString(), - did: result.subjectDid, - }) - } - - if (isReverseTakedownEvent) { - await moderationTxn.reverseTakedownRepo({ - did: result.subjectDid, - }) - takenDown = { - subjects: [ - { - $type: 'com.atproto.admin.defs#repoRef', - did: result.subjectDid, - }, - ], - did: result.subjectDid, - } - } - } - - if ( - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri - ) { - const blobCids = subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [] - if (isTakedownEvent) { - takenDown = await moderationTxn.takedownRecord({ - takedownId: result.id.toString(), - uri: new AtUri(result.subjectUri), - // TODO: I think this will always be available for strongRefs? - cid: CID.parse(result.subjectCid as string), - blobCids, - }) - } - - if (isReverseTakedownEvent) { - await moderationTxn.reverseTakedownRecord({ - uri: new AtUri(result.subjectUri), - }) - takenDown = { - did: result.subjectDid, - subjects: [ - { - $type: 'com.atproto.repo.strongRef', - uri: result.subjectUri, - cid: result.subjectCid ?? '', - }, - ...blobCids.map((cid) => ({ - $type: 'com.atproto.admin.defs#repoBlobRef', - did: result.subjectDid, - cid: cid.toString(), - recordUri: result.subjectUri, - })), - ], - } - } - } - - if (isLabelEvent) { - await labelTxn.formatAndCreate( - ctx.cfg.labelerDid, - result.subjectUri ?? result.subjectDid, - result.subjectCid, - { - create: result.createLabelVals?.length - ? result.createLabelVals.split(' ') - : undefined, - negate: result.negateLabelVals?.length - ? result.negateLabelVals.split(' ') - : undefined, - }, - ) - } - - return { result, takenDown } - }, - ) - - if (takenDown && ctx.moderationPushAgent) { - const { did, subjects } = takenDown - if (did && subjects.length > 0) { - const agent = ctx.moderationPushAgent - const results = await Promise.allSettled( - subjects.map((subject) => - retryHttp(() => - agent.api.com.atproto.admin.updateSubjectStatus({ - subject, - takedown: isTakedownEvent - ? { - applied: true, - ref: moderationEvent.id.toString(), - } - : { - applied: false, - }, - }), - ), - ), - ) - const hadFailure = results.some((r) => r.status === 'rejected') - if (hadFailure) { - throw new UpstreamFailureError('failed to apply action on PDS') - } - } - } - - return { - encoding: 'application/json', - body: await moderationService.views.event(moderationEvent), - } - }, - }) -} - -const validateLabels = (labels: string[]) => { - for (const label of labels) { - for (const char of badChars) { - if (label.includes(char)) { - throw new InvalidRequestError(`Invalid label: ${label}`) - } - } - } -} - -const badChars = [' ', ',', ';', `'`, `"`] diff --git a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts index 9a71da9eb7f..9ef66c94c9b 100644 --- a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts +++ b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts @@ -6,7 +6,7 @@ import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfos({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.roleOrAdminService, handler: async ({ params }) => { const { dids } = params const db = ctx.db.getPrimary() diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts b/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts deleted file mode 100644 index 347a450c727..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.getModerationEvent({ - auth: ctx.roleVerifier, - handler: async ({ params }) => { - const { id } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const event = await moderationService.getEventOrThrow(id) - const eventDetail = await moderationService.views.eventDetail(event) - return { - encoding: 'application/json', - body: eventDetail, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/getRecord.ts b/packages/bsky/src/api/com/atproto/admin/getRecord.ts deleted file mode 100644 index 8e459910806..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/getRecord.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { addAccountInfoToRepoView, getPdsAccountInfo } from './util' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.getRecord({ - auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { - const { uri, cid } = params - const db = ctx.db.getPrimary() - const result = await db.db - .selectFrom('record') - .selectAll() - .where('uri', '=', uri) - .if(!!cid, (qb) => qb.where('cid', '=', cid ?? '')) - .executeTakeFirst() - if (!result) { - throw new InvalidRequestError('Record not found', 'RecordNotFound') - } - - const [record, accountInfo] = await Promise.all([ - ctx.services.moderation(db).views.recordDetail(result), - getPdsAccountInfo(ctx, result.did), - ]) - - record.repo = addAccountInfoToRepoView( - record.repo, - accountInfo, - auth.credentials.moderator, - ) - - return { - encoding: 'application/json', - body: record, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/getRepo.ts b/packages/bsky/src/api/com/atproto/admin/getRepo.ts deleted file mode 100644 index 314b345b5e9..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/getRepo.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.getRepo({ - auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { - const { did } = params - const db = ctx.db.getPrimary() - const result = await ctx.services.actor(db).getActor(did, true) - if (!result) { - throw new InvalidRequestError('Repo not found', 'RepoNotFound') - } - const [partialRepo, accountInfo] = await Promise.all([ - ctx.services.moderation(db).views.repoDetail(result), - getPdsAccountInfo(ctx, result.did), - ]) - - const repo = addAccountInfoToRepoViewDetail( - partialRepo, - accountInfo, - auth.credentials.moderator, - ) - return { - encoding: 'application/json', - body: repo, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts index 652f8a26bf2..8ac237240f9 100644 --- a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts @@ -5,7 +5,7 @@ import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSub export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getSubjectStatus({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.roleOrAdminService, handler: async ({ params }) => { const { did, uri, blob } = params const modService = ctx.services.moderation(ctx.db.getPrimary()) diff --git a/packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts deleted file mode 100644 index 1868533295c..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getEventType } from '../moderation/util' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.queryModerationEvents({ - auth: ctx.roleVerifier, - handler: async ({ params }) => { - const { - subject, - limit = 50, - cursor, - sortDirection = 'desc', - types, - includeAllUserRecords = false, - createdBy, - } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const results = await moderationService.getEvents({ - types: types?.length ? types.map(getEventType) : [], - subject, - createdBy, - limit, - cursor, - sortDirection, - includeAllUserRecords, - }) - return { - encoding: 'application/json', - body: { - cursor: results.cursor, - events: await moderationService.views.event(results.events), - }, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts deleted file mode 100644 index 5a74bfca3ae..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getReviewState } from '../moderation/util' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.queryModerationStatuses({ - auth: ctx.roleVerifier, - handler: async ({ params }) => { - const { - subject, - takendown, - reviewState, - reviewedAfter, - reviewedBefore, - reportedAfter, - reportedBefore, - ignoreSubjects, - lastReviewedBy, - sortDirection = 'desc', - sortField = 'lastReportedAt', - includeMuted = false, - limit = 50, - cursor, - } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const results = await moderationService.getSubjectStatuses({ - reviewState: getReviewState(reviewState), - subject, - takendown, - reviewedAfter, - reviewedBefore, - reportedAfter, - reportedBefore, - includeMuted, - ignoreSubjects, - sortDirection, - lastReviewedBy, - sortField, - limit, - cursor, - }) - const subjectStatuses = moderationService.views.subjectStatus( - results.statuses, - ) - return { - encoding: 'application/json', - body: { - cursor: results.cursor, - subjectStatuses, - }, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index ef580f30d67..030f9569321 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.roleOrAdminService, handler: async ({ params }) => { const db = ctx.db.getPrimary() const moderationService = ctx.services.moderation(db) diff --git a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts index bf4a7e588ff..043c4ec061d 100644 --- a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -11,10 +11,10 @@ import { CID } from 'multiformats/cid' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.roleOrAdminService, handler: async ({ input, auth }) => { - // if less than moderator access then cannot perform a takedown - if (!auth.credentials.moderator) { + const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth) + if (!canPerformTakedown) { throw new AuthRequiredError( 'Must be a full moderator to update subject state', ) diff --git a/packages/bsky/src/api/com/atproto/moderation/createReport.ts b/packages/bsky/src/api/com/atproto/moderation/createReport.ts deleted file mode 100644 index b247a319527..00000000000 --- a/packages/bsky/src/api/com/atproto/moderation/createReport.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AuthRequiredError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getReasonType, getSubject } from './util' -import { softDeleted } from '../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.moderation.createReport({ - // @TODO anonymous reports w/ optional auth are a temporary measure - auth: ctx.authOptionalVerifier, - handler: async ({ input, auth }) => { - const { reasonType, reason, subject } = input.body - const requester = auth.credentials.did - - const db = ctx.db.getPrimary() - - if (requester) { - // Don't accept reports from users that are fully taken-down - const actor = await ctx.services.actor(db).getActor(requester, true) - if (actor && softDeleted(actor)) { - throw new AuthRequiredError() - } - } - - const report = await db.transaction(async (dbTxn) => { - const moderationTxn = ctx.services.moderation(dbTxn) - return moderationTxn.report({ - reasonType: getReasonType(reasonType), - reason, - subject: getSubject(subject), - reportedBy: requester || ctx.cfg.serverDid, - }) - }) - - const moderationService = ctx.services.moderation(db) - return { - encoding: 'application/json', - body: moderationService.views.reportPublic(report), - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/moderation/util.ts b/packages/bsky/src/api/com/atproto/moderation/util.ts deleted file mode 100644 index bc0ece2ff9f..00000000000 --- a/packages/bsky/src/api/com/atproto/moderation/util.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { CID } from 'multiformats/cid' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/syntax' -import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport' -import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/emitModerationEvent' -import { - REASONOTHER, - REASONSPAM, - REASONMISLEADING, - REASONRUDE, - REASONSEXUAL, - REASONVIOLATION, -} from '../../../../lexicon/types/com/atproto/moderation/defs' -import { - REVIEWCLOSED, - REVIEWESCALATED, - REVIEWOPEN, -} from '../../../../lexicon/types/com/atproto/admin/defs' -import { ModerationEvent } from '../../../../db/tables/moderation' -import { ModerationSubjectStatusRow } from '../../../../services/moderation/types' - -type SubjectInput = ReportInput['subject'] | ActionInput['subject'] - -export const getSubject = (subject: SubjectInput) => { - if ( - subject.$type === 'com.atproto.admin.defs#repoRef' && - typeof subject.did === 'string' - ) { - return { did: subject.did } - } - if ( - subject.$type === 'com.atproto.repo.strongRef' && - typeof subject.uri === 'string' && - typeof subject.cid === 'string' - ) { - const uri = new AtUri(subject.uri) - return { - uri, - cid: CID.parse(subject.cid), - } - } - throw new InvalidRequestError('Invalid subject') -} - -export const getReasonType = (reasonType: ReportInput['reasonType']) => { - if (reasonTypes.has(reasonType)) { - return reasonType as NonNullable['reportType'] - } - throw new InvalidRequestError('Invalid reason type') -} - -export const getEventType = (type: string) => { - if (eventTypes.has(type)) { - return type as ModerationEvent['action'] - } - throw new InvalidRequestError('Invalid event type') -} - -export const getReviewState = (reviewState?: string) => { - if (!reviewState) return undefined - if (reviewStates.has(reviewState)) { - return reviewState as ModerationSubjectStatusRow['reviewState'] - } - throw new InvalidRequestError('Invalid review state') -} - -const reviewStates = new Set([REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN]) - -const reasonTypes = new Set([ - REASONOTHER, - REASONSPAM, - REASONMISLEADING, - REASONRUDE, - REASONSEXUAL, - REASONVIOLATION, -]) - -const eventTypes = new Set([ - 'com.atproto.admin.defs#modEventTakedown', - 'com.atproto.admin.defs#modEventAcknowledge', - 'com.atproto.admin.defs#modEventEscalate', - 'com.atproto.admin.defs#modEventComment', - 'com.atproto.admin.defs#modEventLabel', - 'com.atproto.admin.defs#modEventReport', - 'com.atproto.admin.defs#modEventMute', - 'com.atproto.admin.defs#modEventUnmute', - 'com.atproto.admin.defs#modEventReverseTakedown', - 'com.atproto.admin.defs#modEventEmail', -]) diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index ff61ed5dbd5..7efba24d12f 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -40,19 +40,12 @@ import updateSeen from './app/bsky/notification/updateSeen' import registerPush from './app/bsky/notification/registerPush' import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators' import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton' -import createReport from './com/atproto/moderation/createReport' import getSubjectStatus from './com/atproto/admin/getSubjectStatus' import updateSubjectStatus from './com/atproto/admin/updateSubjectStatus' -import emitModerationEvent from './com/atproto/admin/emitModerationEvent' import searchRepos from './com/atproto/admin/searchRepos' -import adminGetRecord from './com/atproto/admin/getRecord' -import getRepo from './com/atproto/admin/getRepo' import getAccountInfos from './com/atproto/admin/getAccountInfos' -import queryModerationStatuses from './com/atproto/admin/queryModerationStatuses' import resolveHandle from './com/atproto/identity/resolveHandle' import getRecord from './com/atproto/repo/getRecord' -import queryModerationEvents from './com/atproto/admin/queryModerationEvents' -import getModerationEvent from './com/atproto/admin/getModerationEvent' import fetchLabels from './com/atproto/temp/fetchLabels' export * as health from './health' @@ -104,17 +97,10 @@ export default function (server: Server, ctx: AppContext) { getPopularFeedGenerators(server, ctx) getTimelineSkeleton(server, ctx) // com.atproto - createReport(server, ctx) getSubjectStatus(server, ctx) updateSubjectStatus(server, ctx) - emitModerationEvent(server, ctx) searchRepos(server, ctx) - adminGetRecord(server, ctx) - getRepo(server, ctx) getAccountInfos(server, ctx) - getModerationEvent(server, ctx) - queryModerationEvents(server, ctx) - queryModerationStatuses(server, ctx) resolveHandle(server, ctx) getRecord(server, ctx) fetchLabels(server, ctx) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts new file mode 100644 index 00000000000..95513ea9059 --- /dev/null +++ b/packages/bsky/src/auth-verifier.ts @@ -0,0 +1,307 @@ +import { + AuthRequiredError, + verifyJwt as verifyServiceJwt, +} from '@atproto/xrpc-server' +import { IdResolver } from '@atproto/identity' +import * as ui8 from 'uint8arrays' +import express from 'express' + +type ReqCtx = { + req: express.Request +} + +export enum RoleStatus { + Valid, + Invalid, + Missing, +} + +type NullOutput = { + credentials: { + type: 'null' + iss: null + } +} + +type StandardOutput = { + credentials: { + type: 'standard' + aud: string + iss: string + } +} + +type RoleOutput = { + credentials: { + type: 'role' + admin: boolean + moderator: boolean + triage: boolean + } +} + +type AdminServiceOutput = { + credentials: { + type: 'admin_service' + aud: string + iss: string + } +} + +export type AuthVerifierOpts = { + ownDid: string + adminDid: string + adminPass: string + moderatorPass: string + triagePass: string +} + +export class AuthVerifier { + private _adminPass: string + private _moderatorPass: string + private _triagePass: string + public ownDid: string + public adminDid: string + + constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { + this._adminPass = opts.adminPass + this._moderatorPass = opts.moderatorPass + this._triagePass = opts.triagePass + this.ownDid = opts.ownDid + this.adminDid = opts.adminDid + } + + // verifiers (arrow fns to preserve scope) + + standard = async (ctx: ReqCtx): Promise => { + const { iss, aud } = await this.verifyServiceJwt(ctx, { + aud: this.ownDid, + iss: null, + }) + return { credentials: { type: 'standard', iss, aud } } + } + + standardOptional = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return this.standard(ctx) + } + return this.nullCreds() + } + + standardOptionalAnyAud = async ( + ctx: ReqCtx, + ): Promise => { + if (!isBearerToken(ctx.req)) { + return this.nullCreds() + } + const { iss, aud } = await this.verifyServiceJwt(ctx, { + aud: null, + iss: null, + }) + return { credentials: { type: 'standard', iss, aud } } + } + + role = (ctx: ReqCtx): RoleOutput => { + const creds = this.parseRoleCreds(ctx.req) + if (creds.status !== RoleStatus.Valid) { + throw new AuthRequiredError() + } + return { + credentials: { + ...creds, + type: 'role', + }, + } + } + + standardOrRole = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return this.standard(ctx) + } else { + return this.role(ctx) + } + } + + optionalStandardOrRole = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return await this.standard(ctx) + } else { + const creds = this.parseRoleCreds(ctx.req) + if (creds.status === RoleStatus.Valid) { + return { + credentials: { + ...creds, + type: 'role', + }, + } + } else if (creds.status === RoleStatus.Missing) { + return this.nullCreds() + } else { + throw new AuthRequiredError() + } + } + } + + adminService = async (reqCtx: ReqCtx): Promise => { + const { iss, aud } = await this.verifyServiceJwt(reqCtx, { + aud: this.ownDid, + iss: [this.adminDid], + }) + return { credentials: { type: 'admin_service', aud, iss } } + } + + roleOrAdminService = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.adminService(reqCtx) + } else { + return this.role(reqCtx) + } + } + + parseRoleCreds(req: express.Request) { + const parsed = parseBasicAuth(req.headers.authorization || '') + const { Missing, Valid, Invalid } = RoleStatus + if (!parsed) { + return { status: Missing, admin: false, moderator: false, triage: false } + } + const { username, password } = parsed + if (username === 'admin' && password === this._adminPass) { + return { status: Valid, admin: true, moderator: true, triage: true } + } + if (username === 'admin' && password === this._moderatorPass) { + return { status: Valid, admin: false, moderator: true, triage: true } + } + if (username === 'admin' && password === this._triagePass) { + return { status: Valid, admin: false, moderator: false, triage: true } + } + return { status: Invalid, admin: false, moderator: false, triage: false } + } + + async verifyServiceJwt( + reqCtx: ReqCtx, + opts: { aud: string | null; iss: string[] | null }, + ) { + const getSigningKey = async ( + did: string, + forceRefresh: boolean, + ): Promise => { + if (opts.iss !== null && !opts.iss.includes(did)) { + throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') + } + const atprotoData = await this.idResolver.did.resolveAtprotoData( + did, + forceRefresh, + ) + return atprotoData.signingKey + } + + const jwtStr = bearerTokenFromReq(reqCtx.req) + if (!jwtStr) { + throw new AuthRequiredError('missing jwt', 'MissingJwt') + } + const payload = await verifyServiceJwt(jwtStr, opts.aud, getSigningKey) + return { iss: payload.iss, aud: payload.aud } + } + + nullCreds(): NullOutput { + return { + credentials: { + type: 'null', + iss: null, + }, + } + } + + parseCreds( + creds: StandardOutput | RoleOutput | AdminServiceOutput | NullOutput, + ) { + const viewer = + creds.credentials.type === 'standard' ? creds.credentials.iss : null + const canViewTakedowns = + (creds.credentials.type === 'role' && creds.credentials.triage) || + creds.credentials.type === 'admin_service' + const canPerformTakedown = + (creds.credentials.type === 'role' && creds.credentials.moderator) || + creds.credentials.type === 'admin_service' + return { + viewer, + canViewTakedowns, + canPerformTakedown, + } + } + + // isUserOrAdmin( + // auth: AccessOutput | RoleOutput | NullOutput, + // did: string, + // ): boolean { + // if (!auth.credentials) { + // return false + // } + // if ('did' in auth.credentials) { + // return auth.credentials.did === did + // } + // return auth.credentials.admin + // } +} + +// HELPERS +// --------- + +const BEARER = 'Bearer ' +const BASIC = 'Basic ' + +const isBearerToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BEARER) ?? false +} + +const bearerTokenFromReq = (req: express.Request) => { + const header = req.headers.authorization || '' + if (!header.startsWith(BEARER)) return null + return header.slice(BEARER.length).trim() +} + +export const parseBasicAuth = ( + token: string, +): { username: string; password: string } | null => { + if (!token.startsWith(BASIC)) return null + const b64 = token.slice(BASIC.length) + let parsed: string[] + try { + parsed = ui8.toString(ui8.fromString(b64, 'base64pad'), 'utf8').split(':') + } catch (err) { + return null + } + const [username, password] = parsed + if (!username || !password) return null + return { username, password } +} + +export const ensureValidAdminAud = ( + auth: RoleOutput | AdminServiceOutput, + subjectDid: string, +) => { + if ( + auth.credentials.type === 'admin_service' && + auth.credentials.aud !== subjectDid + ) { + throw new AuthRequiredError( + 'jwt audience does not match account did', + 'BadJwtAudience', + ) + } +} + +export const buildBasicAuth = (username: string, password: string): string => { + return ( + BASIC + + ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad') + ) +} diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts deleted file mode 100644 index ba58638d4f9..00000000000 --- a/packages/bsky/src/auth.ts +++ /dev/null @@ -1,143 +0,0 @@ -import express from 'express' -import * as uint8arrays from 'uint8arrays' -import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' -import { ServerConfig } from './config' - -const BASIC = 'Basic ' -const BEARER = 'Bearer ' - -export const authVerifier = ( - idResolver: IdResolver, - opts: { aud: string | null }, -) => { - const getSigningKey = async ( - did: string, - forceRefresh: boolean, - ): Promise => { - const atprotoData = await idResolver.did.resolveAtprotoData( - did, - forceRefresh, - ) - return atprotoData.signingKey - } - - return async (reqCtx: { req: express.Request; res: express.Response }) => { - const jwtStr = getJwtStrFromReq(reqCtx.req) - if (!jwtStr) { - throw new AuthRequiredError('missing jwt', 'MissingJwt') - } - const payload = await verifyJwt(jwtStr, opts.aud, getSigningKey) - return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } } - } -} - -export const authOptionalVerifier = ( - idResolver: IdResolver, - opts: { aud: string | null }, -) => { - const verifyAccess = authVerifier(idResolver, opts) - return async (reqCtx: { req: express.Request; res: express.Response }) => { - if (!reqCtx.req.headers.authorization) { - return { credentials: { did: null } } - } - return verifyAccess(reqCtx) - } -} - -export const authOptionalAccessOrRoleVerifier = ( - idResolver: IdResolver, - cfg: ServerConfig, -) => { - const verifyAccess = authVerifier(idResolver, { aud: cfg.serverDid }) - const verifyRole = roleVerifier(cfg) - return async (ctx: { req: express.Request; res: express.Response }) => { - const defaultUnAuthorizedCredentials = { - credentials: { did: null, type: 'unauthed' as const }, - } - if (!ctx.req.headers.authorization) { - return defaultUnAuthorizedCredentials - } - // For non-admin tokens, we don't want to consider alternative verifiers and let it fail if it fails - const isRoleAuthToken = ctx.req.headers.authorization?.startsWith(BASIC) - if (isRoleAuthToken) { - const result = await verifyRole(ctx) - return { - ...result, - credentials: { - type: 'role' as const, - ...result.credentials, - }, - } - } - const result = await verifyAccess(ctx) - return { - ...result, - credentials: { - type: 'access' as const, - ...result.credentials, - }, - } - } -} - -export const roleVerifier = - (cfg: ServerConfig) => - async (reqCtx: { req: express.Request; res: express.Response }) => { - const credentials = getRoleCredentials(cfg, reqCtx.req) - if (!credentials.valid) { - throw new AuthRequiredError() - } - return { credentials } - } - -export const getRoleCredentials = (cfg: ServerConfig, req: express.Request) => { - const parsed = parseBasicAuth(req.headers.authorization || '') - const { username, password } = parsed ?? {} - if (username === 'admin' && password === cfg.triagePassword) { - return { valid: true, admin: false, moderator: false, triage: true } - } - if (username === 'admin' && password === cfg.moderatorPassword) { - return { valid: true, admin: false, moderator: true, triage: true } - } - if (username === 'admin' && password === cfg.adminPassword) { - return { valid: true, admin: true, moderator: true, triage: true } - } - return { valid: false, admin: false, moderator: false, triage: false } -} - -export const parseBasicAuth = ( - token: string, -): { username: string; password: string } | null => { - if (!token.startsWith(BASIC)) return null - const b64 = token.slice(BASIC.length) - let parsed: string[] - try { - parsed = uint8arrays - .toString(uint8arrays.fromString(b64, 'base64pad'), 'utf8') - .split(':') - } catch (err) { - return null - } - const [username, password] = parsed - if (!username || !password) return null - return { username, password } -} - -export const buildBasicAuth = (username: string, password: string): string => { - return ( - BASIC + - uint8arrays.toString( - uint8arrays.fromString(`${username}:${password}`, 'utf8'), - 'base64pad', - ) - ) -} - -export const getJwtStrFromReq = (req: express.Request): string | null => { - const { authorization } = req.headers - if (!authorization?.startsWith(BEARER)) { - return null - } - return authorization.slice(BEARER.length).trim() -} diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts index 1d7ee1f429d..2fed066227b 100644 --- a/packages/bsky/src/auto-moderator/index.ts +++ b/packages/bsky/src/auto-moderator/index.ts @@ -6,7 +6,7 @@ import { PrimaryDatabase } from '../db' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' -import { buildBasicAuth } from '../auth' +import { buildBasicAuth } from '../auth-verifier' import { CID } from 'multiformats/cid' import { LabelService } from '../services/label' import { ModerationService } from '../services/moderation' diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 04134e69e21..f99a6fcfca4 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -33,8 +33,8 @@ export interface ServerConfigValues { searchEndpoint?: string labelerDid: string adminPassword: string - moderatorPassword?: string - triagePassword?: string + moderatorPassword: string + triagePassword: string moderationPushUrl?: string rateLimitsEnabled: boolean rateLimitBypassKey?: string @@ -110,9 +110,12 @@ export class ServerConfig { ) const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA assert(dbPrimaryPostgresUrl) - const adminPassword = process.env.ADMIN_PASSWORD || 'admin' + const adminPassword = process.env.ADMIN_PASSWORD || undefined + assert(adminPassword) const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined + assert(moderatorPassword) const triagePassword = process.env.TRIAGE_PASSWORD || undefined + assert(triagePassword) const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const moderationPushUrl = overrides?.moderationPushUrl || diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 8c8db6b2a3c..2bb6cac1ecf 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -7,12 +7,12 @@ import { DatabaseCoordinator } from './db' import { ServerConfig } from './config' import { ImageUriBuilder } from './image/uri' import { Services } from './services' -import * as auth from './auth' import DidRedisCache from './did-cache' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { NotificationServer } from './notifications' import { Redis } from './redis' +import { AuthVerifier, buildBasicAuth } from './auth-verifier' export class AppContext { public moderationPushAgent: AtpAgent | undefined @@ -30,6 +30,7 @@ export class AppContext { searchAgent?: AtpAgent algos: MountedAlgos notifServer: NotificationServer + authVerifier: AuthVerifier }, ) { if (opts.cfg.moderationPushUrl) { @@ -37,7 +38,7 @@ export class AppContext { this.moderationPushAgent = new AtpAgent({ service: url.origin }) this.moderationPushAgent.api.setHeader( 'authorization', - auth.buildBasicAuth(url.username, url.password), + buildBasicAuth(url.username, url.password), ) } } @@ -86,30 +87,8 @@ export class AppContext { return this.opts.searchAgent } - get authVerifier() { - return auth.authVerifier(this.idResolver, { aud: this.cfg.serverDid }) - } - - get authVerifierAnyAudience() { - return auth.authVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifierAnyAudience() { - return auth.authOptionalVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifier() { - return auth.authOptionalVerifier(this.idResolver, { - aud: this.cfg.serverDid, - }) - } - - get authOptionalAccessOrRoleVerifier() { - return auth.authOptionalAccessOrRoleVerifier(this.idResolver, this.cfg) - } - - get roleVerifier() { - return auth.roleVerifier(this.cfg) + get authVerifier(): AuthVerifier { + return this.opts.authVerifier } async serviceAuthJwt(aud: string) { diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 2f83efb3746..b23c984599c 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -33,6 +33,7 @@ import { NotificationServer } from './notifications' import { AtpAgent } from '@atproto/api' import { Keypair } from '@atproto/crypto' import { Redis } from './redis' +import { AuthVerifier } from './auth-verifier' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' @@ -127,6 +128,14 @@ export class BskyAppView { }, }) + const authVerifier = new AuthVerifier(idResolver, { + ownDid: config.serverDid, + adminDid: 'did:example:admin', + adminPass: config.adminPassword, + moderatorPass: config.moderatorPassword, + triagePass: config.triagePassword, + }) + const ctx = new AppContext({ db, cfg: config, @@ -140,6 +149,7 @@ export class BskyAppView { searchAgent, algos, notifServer, + authVerifier, }) const xrpcOpts: XrpcServerOptions = { diff --git a/packages/bsky/tests/admin/__snapshots__/get-record.test.ts.snap b/packages/bsky/tests/admin/__snapshots__/get-record.test.ts.snap deleted file mode 100644 index 14a83f9dfda..00000000000 --- a/packages/bsky/tests/admin/__snapshots__/get-record.test.ts.snap +++ /dev/null @@ -1,173 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`admin get record view gets a record by uri and cid. 1`] = ` -Object { - "blobCids": Array [], - "blobs": Array [], - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", - "reviewState": "com.atproto.admin.defs#reviewClosed", - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": true, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - }, - "repo": Object { - "did": "user(0)", - "email": "alice@test.com", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(1)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], - }, - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, -} -`; - -exports[`admin get record view gets a record by uri, even when taken down. 1`] = ` -Object { - "blobCids": Array [], - "blobs": Array [], - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", - "reviewState": "com.atproto.admin.defs#reviewClosed", - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": true, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - }, - "repo": Object { - "did": "user(0)", - "email": "alice@test.com", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(1)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], - }, - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, -} -`; diff --git a/packages/bsky/tests/admin/__snapshots__/get-repo.test.ts.snap b/packages/bsky/tests/admin/__snapshots__/get-repo.test.ts.snap deleted file mode 100644 index 4ffd7e3564a..00000000000 --- a/packages/bsky/tests/admin/__snapshots__/get-repo.test.ts.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`admin get repo view gets a repo by did, even when taken down. 1`] = ` -Object { - "did": "user(0)", - "email": "alice@test.com", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invites": Array [], - "invitesDisabled": false, - "labels": Array [], - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", - "reviewState": "com.atproto.admin.defs#reviewClosed", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": true, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], -} -`; diff --git a/packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap b/packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap deleted file mode 100644 index 8fa16b311f2..00000000000 --- a/packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap +++ /dev/null @@ -1,146 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`moderation-events get event gets an event by specific id 1`] = ` -Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(2)", - "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonMisleading", - }, - "id": 1, - "subject": Object { - "$type": "com.atproto.admin.defs#repoView", - "did": "user(0)", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "user(1)", - "reviewState": "com.atproto.admin.defs#reviewEscalated", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], - }, - "subjectBlobCids": Array [], - "subjectBlobs": Array [], -} -`; - -exports[`moderation-events query events returns all events for record or repo 1`] = ` -Array [ - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(1)", - "creatorHandle": "alice.test", - "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonSpam", - }, - "id": 7, - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectHandle": "bob.test", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(1)", - "creatorHandle": "alice.test", - "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonSpam", - }, - "id": 3, - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectHandle": "bob.test", - }, -] -`; - -exports[`moderation-events query events returns all events for record or repo 2`] = ` -Array [ - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(0)", - "creatorHandle": "bob.test", - "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonSpam", - }, - "id": 6, - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - "subjectHandle": "alice.test", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(0)", - "creatorHandle": "bob.test", - "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonSpam", - }, - "id": 2, - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - "subjectHandle": "alice.test", - }, -] -`; diff --git a/packages/bsky/tests/admin/__snapshots__/moderation-statuses.test.ts.snap b/packages/bsky/tests/admin/__snapshots__/moderation-statuses.test.ts.snap deleted file mode 100644 index a4939733d1a..00000000000 --- a/packages/bsky/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`moderation-statuses query statuses returns statuses for subjects that received moderation events 1`] = ` -Array [ - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 4, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "reviewState": "com.atproto.admin.defs#reviewOpen", - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "bob.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 3, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "reviewState": "com.atproto.admin.defs#reviewOpen", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "bob.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 2, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "reviewState": "com.atproto.admin.defs#reviewOpen", - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(1)", - "uri": "record(1)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "reviewState": "com.atproto.admin.defs#reviewOpen", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(1)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", - }, -] -`; diff --git a/packages/bsky/tests/admin/get-record.test.ts b/packages/bsky/tests/admin/get-record.test.ts deleted file mode 100644 index 3807724fa6c..00000000000 --- a/packages/bsky/tests/admin/get-record.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SeedClient, TestNetwork } from '@atproto/dev-env' -import AtpAgent from '@atproto/api' -import { AtUri } from '@atproto/syntax' -import { - REASONOTHER, - REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' - -describe('admin get record view', () => { - let network: TestNetwork - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'views_admin_get_record', - }) - agent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - }) - - afterAll(async () => { - await network.close() - }) - - beforeAll(async () => { - await sc.emitModerationEvent({ - event: { $type: 'com.atproto.admin.defs#modEventFlag' }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - }) - await sc.createReport({ - reportedBy: sc.dids.bob, - reasonType: REASONSPAM, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - }) - await sc.createReport({ - reportedBy: sc.dids.carol, - reasonType: REASONOTHER, - reason: 'defamation', - subject: { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - }) - await sc.emitModerationEvent({ - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - }) - }) - - it('gets a record by uri, even when taken down.', async () => { - const result = await agent.api.com.atproto.admin.getRecord( - { uri: sc.posts[sc.dids.alice][0].ref.uriStr }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(forSnapshot(result.data)).toMatchSnapshot() - }) - - it('gets a record by uri and cid.', async () => { - const result = await agent.api.com.atproto.admin.getRecord( - { - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(forSnapshot(result.data)).toMatchSnapshot() - }) - - it('fails when record does not exist.', async () => { - const promise = agent.api.com.atproto.admin.getRecord( - { - uri: AtUri.make( - sc.dids.alice, - 'app.bsky.feed.post', - 'badrkey', - ).toString(), - }, - { headers: network.pds.adminAuthHeaders() }, - ) - await expect(promise).rejects.toThrow('Record not found') - }) - - it('fails when record cid does not exist.', async () => { - const promise = agent.api.com.atproto.admin.getRecord( - { - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][1].ref.cidStr, // Mismatching cid - }, - { headers: network.pds.adminAuthHeaders() }, - ) - await expect(promise).rejects.toThrow('Record not found') - }) -}) diff --git a/packages/bsky/tests/admin/get-repo.test.ts b/packages/bsky/tests/admin/get-repo.test.ts deleted file mode 100644 index 1e95f8cc0fc..00000000000 --- a/packages/bsky/tests/admin/get-repo.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SeedClient, TestNetwork } from '@atproto/dev-env' -import AtpAgent from '@atproto/api' -import { - REASONOTHER, - REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' - -describe('admin get repo view', () => { - let network: TestNetwork - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'views_admin_get_repo', - }) - agent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - }) - - afterAll(async () => { - await network.close() - }) - - beforeAll(async () => { - await sc.emitModerationEvent({ - event: { $type: 'com.atproto.admin.defs#modEventAcknowledge' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - }, - }) - await sc.createReport({ - reportedBy: sc.dids.bob, - reasonType: REASONSPAM, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - }, - }) - await sc.createReport({ - reportedBy: sc.dids.carol, - reasonType: REASONOTHER, - reason: 'defamation', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - }, - }) - await sc.emitModerationEvent({ - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - }, - }) - }) - - it('gets a repo by did, even when taken down.', async () => { - const result = await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.alice }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(forSnapshot(result.data)).toMatchSnapshot() - }) - - it('does not include account emails for triage mods.', async () => { - const { data: admin } = await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, - ) - const { data: moderator } = await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders('moderator') }, - ) - const { data: triage } = await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders('triage') }, - ) - expect(admin.email).toEqual('bob@test.com') - expect(moderator.email).toEqual('bob@test.com') - expect(triage.email).toBeUndefined() - expect(triage).toEqual({ ...admin, email: undefined }) - }) - - it('includes emailConfirmedAt timestamp', async () => { - const { data: beforeEmailVerification } = - await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, - ) - - expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined() - const timestampBeforeVerification = Date.now() - const bobsAccount = sc.accounts[sc.dids.bob] - const verificationToken = - await network.pds.ctx.accountManager.createEmailToken( - sc.dids.bob, - 'confirm_email', - ) - await agent.api.com.atproto.server.confirmEmail( - { email: bobsAccount.email, token: verificationToken }, - { - encoding: 'application/json', - - headers: sc.getHeaders(sc.dids.bob), - }, - ) - const { data: afterEmailVerification } = - await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, - ) - - expect(afterEmailVerification.emailConfirmedAt).toBeTruthy() - expect( - new Date(afterEmailVerification.emailConfirmedAt as string).getTime(), - ).toBeGreaterThan(timestampBeforeVerification) - }) - - it('fails when repo does not exist.', async () => { - const promise = agent.api.com.atproto.admin.getRepo( - { did: 'did:plc:doesnotexist' }, - { headers: network.pds.adminAuthHeaders() }, - ) - await expect(promise).rejects.toThrow('Repo not found') - }) -}) diff --git a/packages/bsky/tests/admin/moderation-events.test.ts b/packages/bsky/tests/admin/moderation-events.test.ts deleted file mode 100644 index 174167034db..00000000000 --- a/packages/bsky/tests/admin/moderation-events.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import AtpAgent, { ComAtprotoAdminDefs } from '@atproto/api' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' -import { - REASONMISLEADING, - REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' - -describe('moderation-events', () => { - let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent - let sc: SeedClient - - const emitModerationEvent = async (eventData) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), - }) - } - - const queryModerationEvents = (eventQuery) => - agent.api.com.atproto.admin.queryModerationEvents(eventQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), - }) - - const seedEvents = async () => { - const bobsAccount = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - } - const alicesAccount = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - } - const bobsPost = { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.bob][0].ref.uriStr, - cid: sc.posts[sc.dids.bob][0].ref.cidStr, - } - const alicesPost = { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - } - - for (let i = 0; i < 4; i++) { - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: i % 2 ? REASONSPAM : REASONMISLEADING, - comment: 'X', - }, - // Report bob's account by alice and vice versa - subject: i % 2 ? bobsAccount : alicesAccount, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, - }) - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONSPAM, - comment: 'X', - }, - // Report bob's post by alice and vice versa - subject: i % 2 ? bobsPost : alicesPost, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, - }) - } - } - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation_events', - }) - agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - await seedEvents() - }) - - afterAll(async () => { - await network.close() - }) - - describe('query events', () => { - it('returns all events for record or repo', async () => { - const [bobsEvents, alicesPostEvents] = await Promise.all([ - queryModerationEvents({ - subject: sc.dids.bob, - }), - queryModerationEvents({ - subject: sc.posts[sc.dids.alice][0].ref.uriStr, - }), - ]) - - expect(forSnapshot(bobsEvents.data.events)).toMatchSnapshot() - expect(forSnapshot(alicesPostEvents.data.events)).toMatchSnapshot() - }) - - it('filters events by types', async () => { - const alicesAccount = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - } - await Promise.all([ - emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventComment', - comment: 'X', - }, - subject: alicesAccount, - createdBy: 'did:plc:moderator', - }), - emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventEscalate', - comment: 'X', - }, - subject: alicesAccount, - createdBy: 'did:plc:moderator', - }), - ]) - const [allEvents, reportEvents] = await Promise.all([ - queryModerationEvents({ - subject: sc.dids.alice, - }), - queryModerationEvents({ - subject: sc.dids.alice, - types: ['com.atproto.admin.defs#modEventReport'], - }), - ]) - - expect(allEvents.data.events.length).toBeGreaterThan( - reportEvents.data.events.length, - ) - expect( - [...new Set(reportEvents.data.events.map((e) => e.event.$type))].length, - ).toEqual(1) - - expect( - [...new Set(allEvents.data.events.map((e) => e.event.$type))].length, - ).toEqual(3) - }) - - it('returns events for all content by user', async () => { - const [forAccount, forPost] = await Promise.all([ - queryModerationEvents({ - subject: sc.dids.bob, - includeAllUserRecords: true, - }), - queryModerationEvents({ - subject: sc.posts[sc.dids.bob][0].ref.uriStr, - includeAllUserRecords: true, - }), - ]) - - expect(forAccount.data.events.length).toEqual(forPost.data.events.length) - // Save events are returned from both requests - expect(forPost.data.events.map(({ id }) => id).sort()).toEqual( - forAccount.data.events.map(({ id }) => id).sort(), - ) - }) - - it('returns paginated list of events with cursor', async () => { - const allEvents = await queryModerationEvents({ - subject: sc.dids.bob, - includeAllUserRecords: true, - }) - - const getPaginatedEvents = async ( - sortDirection: 'asc' | 'desc' = 'desc', - ) => { - let defaultCursor: undefined | string = undefined - const events: ComAtprotoAdminDefs.ModEventView[] = [] - let count = 0 - do { - // get 1 event at a time and check we get all events - const { data } = await queryModerationEvents({ - limit: 1, - subject: sc.dids.bob, - includeAllUserRecords: true, - cursor: defaultCursor, - sortDirection, - }) - events.push(...data.events) - defaultCursor = data.cursor - count++ - // The count is a circuit breaker to prevent infinite loop in case of failing test - } while (defaultCursor && count < 10) - - return events - } - - const defaultEvents = await getPaginatedEvents() - const reversedEvents = await getPaginatedEvents('asc') - - expect(allEvents.data.events.length).toEqual(4) - expect(defaultEvents.length).toEqual(allEvents.data.events.length) - expect(reversedEvents.length).toEqual(allEvents.data.events.length) - expect(reversedEvents[0].id).toEqual(defaultEvents[3].id) - }) - }) - - describe('get event', () => { - it('gets an event by specific id', async () => { - const { data } = await pdsAgent.api.com.atproto.admin.getModerationEvent( - { - id: 1, - }, - { - headers: network.bsky.adminAuthHeaders('moderator'), - }, - ) - - expect(forSnapshot(data)).toMatchSnapshot() - }) - }) -}) diff --git a/packages/bsky/tests/admin/moderation-statuses.test.ts b/packages/bsky/tests/admin/moderation-statuses.test.ts deleted file mode 100644 index 5109cc43b0e..00000000000 --- a/packages/bsky/tests/admin/moderation-statuses.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminDefs, - ComAtprotoAdminQueryModerationStatuses, -} from '@atproto/api' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' -import { - REASONMISLEADING, - REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' - -describe('moderation-statuses', () => { - let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent - let sc: SeedClient - - const emitModerationEvent = async (eventData) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), - }) - } - - const queryModerationStatuses = (statusQuery) => - agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), - }) - - const seedEvents = async () => { - const bobsAccount = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - } - const carlasAccount = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.alice, - } - const bobsPost = { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.bob][1].ref.uriStr, - cid: sc.posts[sc.dids.bob][1].ref.cidStr, - } - const alicesPost = { - $type: 'com.atproto.repo.strongRef', - uri: sc.posts[sc.dids.alice][1].ref.uriStr, - cid: sc.posts[sc.dids.alice][1].ref.cidStr, - } - - for (let i = 0; i < 4; i++) { - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: i % 2 ? REASONSPAM : REASONMISLEADING, - comment: 'X', - }, - // Report bob's account by alice and vice versa - subject: i % 2 ? bobsAccount : carlasAccount, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, - }) - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONSPAM, - comment: 'X', - }, - // Report bob's post by alice and vice versa - subject: i % 2 ? bobsPost : alicesPost, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, - }) - } - } - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation_statuses', - }) - agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - await seedEvents() - }) - - afterAll(async () => { - await network.close() - }) - - describe('query statuses', () => { - it('returns statuses for subjects that received moderation events', async () => { - const response = await queryModerationStatuses({}) - - expect(forSnapshot(response.data.subjectStatuses)).toMatchSnapshot() - }) - - it('returns paginated statuses', async () => { - // We know there will be exactly 4 statuses in db - const getPaginatedStatuses = async ( - params: ComAtprotoAdminQueryModerationStatuses.QueryParams, - ) => { - let cursor: string | undefined = '' - const statuses: ComAtprotoAdminDefs.SubjectStatusView[] = [] - let count = 0 - do { - const results = await queryModerationStatuses({ - limit: 1, - cursor, - ...params, - }) - cursor = results.data.cursor - statuses.push(...results.data.subjectStatuses) - count++ - // The count is just a brake-check to prevent infinite loop - } while (cursor && count < 10) - - return statuses - } - - const list = await getPaginatedStatuses({}) - expect(list[0].id).toEqual(4) - expect(list[list.length - 1].id).toEqual(1) - - await emitModerationEvent({ - subject: list[1].subject, - event: { - $type: 'com.atproto.admin.defs#modEventAcknowledge', - comment: 'X', - }, - createdBy: sc.dids.bob, - }) - - const listReviewedFirst = await getPaginatedStatuses({ - sortDirection: 'desc', - sortField: 'lastReviewedAt', - }) - - // Verify that the item that was recently reviewed comes up first when sorted descendingly - // while the result set always contains same number of items regardless of sorting - expect(listReviewedFirst[0].id).toEqual(list[1].id) - expect(listReviewedFirst.length).toEqual(list.length) - }) - }) -}) diff --git a/packages/bsky/tests/admin/repo-search.test.ts b/packages/bsky/tests/admin/repo-search.test.ts index 9e643ba12e0..a6a3fee4289 100644 --- a/packages/bsky/tests/admin/repo-search.test.ts +++ b/packages/bsky/tests/admin/repo-search.test.ts @@ -3,7 +3,7 @@ import AtpAgent from '@atproto/api' import { paginateAll } from '../_util' import usersBulkSeed from '../seeds/users-bulk' -describe('admin repo search view', () => { +describe.skip('admin repo search view', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index aceecec3204..e54fd486264 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -157,16 +157,17 @@ describe('feed generation', () => { sc.getHeaders(alice), ) await network.processAll() - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: prime.uri, cid: prime.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 70f8862f7d7..2a79387c0e4 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -239,15 +239,16 @@ describe.skip('pds actor search views', () => { }) it('search blocks by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids['cara-wiegand69.test'], }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index c5d863bfb92..e3a58b7685a 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -147,15 +147,16 @@ describe('pds author feed views', () => { expect(preBlock.feed.length).toBeGreaterThan(0) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -170,15 +171,15 @@ describe('pds author feed views', () => { await expect(attempt).rejects.toThrow('Profile not found') // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -197,16 +198,17 @@ describe('pds author feed views', () => { const post = preBlock.feed[0].post - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -223,16 +225,16 @@ describe('pds author feed views', () => { expect(postBlock.feed.map((item) => item.post.uri)).not.toContain(post.uri) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index f290ec622d5..3aa610dd663 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -120,15 +120,16 @@ describe('pds follow views', () => { }) it('blocks followers by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -145,15 +146,15 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -252,15 +253,16 @@ describe('pds follow views', () => { }) it('blocks follows by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -277,15 +279,15 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts index b8cd977922b..a3449e874f2 100644 --- a/packages/bsky/tests/views/list-feed.test.ts +++ b/packages/bsky/tests/views/list-feed.test.ts @@ -112,15 +112,16 @@ describe('list feed views', () => { }) it('blocks posts by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -135,15 +136,15 @@ describe('list feed views', () => { expect(hasBob).toBe(false) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -154,16 +155,17 @@ describe('list feed views', () => { it('blocks posts by record takedown.', async () => { const postRef = sc.replies[bob][0].ref // Post and reply parent - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -180,16 +182,16 @@ describe('list feed views', () => { expect(hasPost).toBe(false) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index c75ee7b699e..4e976155eec 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -241,16 +241,17 @@ describe('notification views', () => { const postRef2 = sc.posts[sc.dids.dan][1].ref // Mention await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -277,16 +278,16 @@ describe('notification views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index fe3f689894b..1d579e7f191 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -184,15 +184,16 @@ describe('pds profile views', () => { }) it('blocked by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -207,61 +208,15 @@ describe('pds profile views', () => { await expect(promise).rejects.toThrow('Account has been taken down') // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - }) - - it('blocked by actor suspension', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - durationInHours: 1, - }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - const promise = agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: await network.serviceHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow( - 'Account has been temporarily suspended', - ) - - // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, + takedown: { + applied: false, }, - createdBy: 'did:example:admin', - reason: 'Y', }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index d42378aec6e..dbec0e3ab5d 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -165,19 +165,20 @@ describe('pds thread views', () => { describe('takedown', () => { it('blocks post by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -192,37 +193,38 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks replies by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: carol, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -235,37 +237,38 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: carol, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks ancestors by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -278,39 +281,40 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks post by record', async () => { const postRef = sc.posts[alice][1].ref - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -324,20 +328,20 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -350,20 +354,21 @@ describe('pds thread views', () => { const parent = threadPreTakedown.data.thread.parent?.['post'] - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: parent.uri, cid: parent.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -376,20 +381,20 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: parent.uri, cid: parent.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -404,20 +409,21 @@ describe('pds thread views', () => { await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ), ), @@ -434,7 +440,7 @@ describe('pds thread views', () => { // Cleanup await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', @@ -444,12 +450,13 @@ describe('pds thread views', () => { uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ), ), diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index 5410d792a1f..7f1210bec07 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -183,15 +183,16 @@ describe('timeline views', () => { it('blocks posts, reposts, replies by actor takedown', async () => { await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -211,15 +212,15 @@ describe('timeline views', () => { // Cleanup await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -235,16 +236,17 @@ describe('timeline views', () => { const postRef2 = sc.replies[bob][0].ref // Post and reply parent await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -264,16 +266,16 @@ describe('timeline views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index c52ada4a9e9..1864d6e2bf0 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -47,6 +47,8 @@ export class TestPds { bskyAppViewUrl: 'https://appview.invalid', bskyAppViewDid: 'did:example:invalid', bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', + modServiceUrl: 'https:/moderator.invalid', + modServiceDid: 'did:example:invalid', plcRotationKeyK256PrivateKeyHex: plcRotationPriv, inviteRequired: false, ...config, diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts index b5c25bed321..51f476c0167 100644 --- a/packages/ozone/src/api/moderation/createReport.ts +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -6,7 +6,7 @@ import { subjectFromInput } from '../../mod-service/subject' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ // @TODO anonymous reports w/ optional auth are a temporary measure - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ input, auth }) => { const requester = auth.credentials.did const { reasonType, reason } = input.body