Optional service config in pds distribution ()

* make appview and mod services optional on pds

* pds: allow configuring a reporting service optionally separate from an administrative mod service

* tidy
This commit is contained in:
devin ivy 2024-02-21 20:14:45 -05:00 committed by GitHub
parent 4e0271bddf
commit 9f90203f20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 280 additions and 103 deletions

@ -3,6 +3,7 @@ import AppContext from '../../../../context'
import { AuthScope } from '../../../../auth-verifier'
export default function (server: Server, ctx: AppContext) {
if (!ctx.cfg.bskyAppView) return
server.app.bsky.actor.getPreferences({
auth: ctx.authVerifier.access,
handler: async ({ auth }) => {

@ -12,13 +12,15 @@ import { pipethrough } from '../../../../pipethrough'
const METHOD_NSID = 'app.bsky.actor.getProfile'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.actor.getProfile({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, auth, params }) => {
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req),

@ -11,13 +11,15 @@ import {
const METHOD_NSID = 'app.bsky.actor.getProfiles'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.actor.getProfiles({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.actor.getSuggestions({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.actor.getSuggestions',
params,
await ctx.appviewAuthHeaders(requester),

@ -4,6 +4,7 @@ import AppContext from '../../../../context'
import { AccountPreference } from '../../../../actor-store/preference/reader'
export default function (server: Server, ctx: AppContext) {
if (!ctx.cfg.bskyAppView) return
server.app.bsky.actor.putPreferences({
auth: ctx.authVerifier.accessCheckTakedown,
handler: async ({ auth, input }) => {

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.actor.searchActors({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.actor.searchActors',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.actor.searchActorsTypeahead({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.actor.searchActorsTypeahead',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getActorFeeds({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getActorFeeds',
params,
await ctx.appviewAuthHeaders(requester),

@ -12,13 +12,15 @@ import { pipethrough } from '../../../../pipethrough'
const METHOD_NSID = 'app.bsky.feed.getActorLikes'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getActorLikes({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, params, auth }) => {
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req),

@ -13,13 +13,15 @@ import { pipethrough } from '../../../../pipethrough'
const METHOD_NSID = 'app.bsky.feed.getAuthorFeed'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getAuthorFeed({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, params, auth }) => {
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req),

@ -3,13 +3,16 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
const { bskyAppView } = ctx.cfg
if (!appViewAgent || !bskyAppView) return
server.app.bsky.feed.getFeed({
auth: ctx.authVerifier.access,
handler: async ({ req, params, auth }) => {
const requester = auth.credentials.did
const { data: feed } =
await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator(
await appViewAgent.api.app.bsky.feed.getFeedGenerator(
{ feed: params.feed },
await ctx.appviewAuthHeaders(requester),
)
@ -21,7 +24,7 @@ export default function (server: Server, ctx: AppContext) {
serviceAuthHeaders.headers['accept-language'] =
req.headers['accept-language']
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getFeed',
params,
serviceAuthHeaders,

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getFeedGenerator({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getFeedGenerator',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getFeedGenerators({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getFeedGenerators',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getLikes({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getLikes',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getListFeed({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getListFeed',
params,
await ctx.appviewAuthHeaders(requester),

@ -1,3 +1,4 @@
import assert from 'node:assert'
import { AtUri } from '@atproto/syntax'
import { Headers, XRPCError } from '@atproto/xrpc'
import { Server } from '../../../../lexicon'
@ -26,6 +27,8 @@ import { pipethrough } from '../../../../pipethrough'
const METHOD_NSID = 'app.bsky.feed.getPostThread'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getPostThread({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, params, auth }) => {
@ -34,7 +37,7 @@ export default function (server: Server, ctx: AppContext) {
if (!requester) {
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
authPassthru(req),
@ -43,7 +46,7 @@ export default function (server: Server, ctx: AppContext) {
try {
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
await ctx.appviewAuthHeaders(requester),
@ -200,6 +203,7 @@ const readAfterWriteNotFound = async (
const highestParent = getHighestParent(thread)
if (highestParent) {
try {
assert(ctx.appViewAgent)
const parentsRes = await ctx.appViewAgent.api.app.bsky.feed.getPostThread(
{ uri: highestParent, parentHeight: params.parentHeight, depth: 0 },
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getPosts({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getPosts',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getRepostedBy({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getRepostedBy',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getSuggestedFeeds({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.getSuggestedFeeds',
params,
await ctx.appviewAuthHeaders(requester),

@ -11,12 +11,14 @@ import { pipethrough } from '../../../../pipethrough'
const METHOD_NSID = 'app.bsky.feed.getTimeline'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.getTimeline({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
const res = await pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
METHOD_NSID,
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.feed.searchPosts({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.feed.searchPosts',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getBlocks({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getBlocks',
params,
await ctx.appviewAuthHeaders(requester),

@ -4,13 +4,15 @@ import { authPassthru } from '../../../proxy'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getFollowers({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, params, auth }) => {
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getFollowers',
params,
requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req),

@ -4,13 +4,15 @@ import { authPassthru } from '../../../proxy'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getFollows({
auth: ctx.authVerifier.accessOrRole,
handler: async ({ req, params, auth }) => {
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getFollows',
params,
requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getList({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getList',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getListBlocks({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getListBlocks',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getListMutes({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getListMutes',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getLists({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getLists',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getMutes({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getMutes',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.graph.getSuggestedFollowsByActor({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.graph.getSuggestedFollowsByActor',
params,
await ctx.appviewAuthHeaders(requester),

@ -2,12 +2,14 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.graph.muteActor({
auth: ctx.authVerifier.access,
handler: async ({ auth, input }) => {
const requester = auth.credentials.did
await ctx.appViewAgent.api.app.bsky.graph.muteActor(input.body, {
await appViewAgent.api.app.bsky.graph.muteActor(input.body, {
...(await ctx.appviewAuthHeaders(requester)),
encoding: 'application/json',
})

@ -2,12 +2,14 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.graph.muteActorList({
auth: ctx.authVerifier.access,
handler: async ({ auth, input }) => {
const requester = auth.credentials.did
await ctx.appViewAgent.api.app.bsky.graph.muteActorList(input.body, {
await appViewAgent.api.app.bsky.graph.muteActorList(input.body, {
...(await ctx.appviewAuthHeaders(requester)),
encoding: 'application/json',
})

@ -2,12 +2,14 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.graph.unmuteActor({
auth: ctx.authVerifier.access,
handler: async ({ auth, input }) => {
const requester = auth.credentials.did
await ctx.appViewAgent.api.app.bsky.graph.unmuteActor(input.body, {
await appViewAgent.api.app.bsky.graph.unmuteActor(input.body, {
...(await ctx.appviewAuthHeaders(requester)),
encoding: 'application/json',
})

@ -2,12 +2,14 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.graph.unmuteActorList({
auth: ctx.authVerifier.access,
handler: async ({ auth, input }) => {
const requester = auth.credentials.did
await ctx.appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, {
await appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, {
...(await ctx.appviewAuthHeaders(requester)),
encoding: 'application/json',
})

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.notification.getUnreadCount({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.notification.getUnreadCount',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,12 +3,14 @@ import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.notification.listNotifications({
auth: ctx.authVerifier.access,
handler: async ({ params, auth }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.notification.listNotifications',
params,
await ctx.appviewAuthHeaders(requester),

@ -6,6 +6,8 @@ import { AtpAgent } from '@atproto/api'
import { getDidDoc } from '../util/resolver'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.notification.registerPush({
auth: ctx.authVerifier.accessDeactived,
handler: async ({ auth, input }) => {
@ -16,14 +18,11 @@ export default function (server: Server, ctx: AppContext) {
const authHeaders = await ctx.serviceAuthHeaders(did, serviceDid)
if (ctx.cfg.bskyAppView.did === serviceDid) {
await ctx.appViewAgent.api.app.bsky.notification.registerPush(
input.body,
{
...authHeaders,
encoding: 'application/json',
},
)
if (ctx.cfg.bskyAppView?.did === serviceDid) {
await appViewAgent.api.app.bsky.notification.registerPush(input.body, {
...authHeaders,
encoding: 'application/json',
})
return
}

@ -2,12 +2,14 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
if (!appViewAgent) return
server.app.bsky.notification.updateSeen({
auth: ctx.authVerifier.access,
handler: async ({ input, auth }) => {
const requester = auth.credentials.did
await ctx.appViewAgent.api.app.bsky.notification.updateSeen(input.body, {
await appViewAgent.api.app.bsky.notification.updateSeen(input.body, {
...(await ctx.appviewAuthHeaders(requester)),
encoding: 'application/json',
})

@ -4,12 +4,14 @@ import { pipethrough } from '../../../../pipethrough'
// THIS IS A TEMPORARY UNSPECCED ROUTE
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.unspecced.getPopularFeedGenerators({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.unspecced.getPopularFeedGenerators',
params,
await ctx.appviewAuthHeaders(requester),

@ -4,12 +4,14 @@ import { pipethrough } from '../../../../pipethrough'
// THIS IS A TEMPORARY UNSPECCED ROUTE
export default function (server: Server, ctx: AppContext) {
const { bskyAppView } = ctx.cfg
if (!bskyAppView) return
server.app.bsky.unspecced.getTaggedSuggestions({
auth: ctx.authVerifier.access,
handler: async ({ auth, params }) => {
const requester = auth.credentials.did
return pipethrough(
ctx.cfg.bskyAppView.url,
bskyAppView.url,
'app.bsky.unspecced.getTaggedSuggestions',
params,
await ctx.appviewAuthHeaders(requester),

@ -3,15 +3,16 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.createCommunicationTemplate({
auth: ctx.authVerifier.role,
handler: async ({ req, input }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.createCommunicationTemplate(
await moderationAgent.com.atproto.admin.createCommunicationTemplate(
input.body,
authPassthru(req, true),
)
return {
encoding: 'application/json',
body: result,

@ -3,10 +3,12 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.deleteCommunicationTemplate({
auth: ctx.authVerifier.role,
handler: async ({ req, input }) => {
await ctx.moderationAgent.com.atproto.admin.deleteCommunicationTemplate(
await moderationAgent.com.atproto.admin.deleteCommunicationTemplate(
input.body,
authPassthru(req, true),
)

@ -3,15 +3,16 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.emitModerationEvent({
auth: ctx.authVerifier.role,
handler: async ({ req, input }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.emitModerationEvent(
await moderationAgent.com.atproto.admin.emitModerationEvent(
input.body,
authPassthru(req, true),
)
return {
encoding: 'application/json',
body: result,

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.getModerationEvent({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const { data } =
await ctx.moderationAgent.com.atproto.admin.getModerationEvent(
await moderationAgent.com.atproto.admin.getModerationEvent(
params,
authPassthru(req),
)

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.getRecord({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const { data: recordDetailAppview } =
await ctx.moderationAgent.com.atproto.admin.getRecord(
await moderationAgent.com.atproto.admin.getRecord(
params,
authPassthru(req),
)

@ -3,10 +3,12 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.getRepo({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const res = await ctx.moderationAgent.com.atproto.admin.getRepo(
const res = await moderationAgent.com.atproto.admin.getRepo(
params,
authPassthru(req),
)

@ -3,15 +3,16 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.listCommunicationTemplates({
auth: ctx.authVerifier.role,
handler: async ({ req }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.listCommunicationTemplates(
await moderationAgent.com.atproto.admin.listCommunicationTemplates(
{},
authPassthru(req, true),
)
return {
encoding: 'application/json',
body: result,

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.queryModerationEvents({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.queryModerationEvents(
await moderationAgent.com.atproto.admin.queryModerationEvents(
params,
authPassthru(req),
)

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.queryModerationStatuses({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const { data } =
await ctx.moderationAgent.com.atproto.admin.queryModerationStatuses(
await moderationAgent.com.atproto.admin.queryModerationStatuses(
params,
authPassthru(req),
)

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.searchRepos({
auth: ctx.authVerifier.role,
handler: async ({ req, params }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.searchRepos(
await moderationAgent.com.atproto.admin.searchRepos(
params,
authPassthru(req),
)

@ -43,21 +43,25 @@ export default function (server: Server, ctx: AppContext) {
{ content },
{ subject, to: account.email },
)
await ctx.moderationAgent.api.com.atproto.admin.emitModerationEvent(
{
event: {
$type: 'com.atproto.admin.defs#modEventEmail',
subjectLine: subject,
comment,
if (ctx.moderationAgent) {
await ctx.moderationAgent.api.com.atproto.admin.emitModerationEvent(
{
event: {
$type: 'com.atproto.admin.defs#modEventEmail',
subjectLine: subject,
comment,
},
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: recipientDid,
},
createdBy: senderDid,
},
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: recipientDid,
},
createdBy: senderDid,
},
{ ...authPassthru(req), encoding: 'application/json' },
)
{ ...authPassthru(req), encoding: 'application/json' },
)
}
return {
encoding: 'application/json',
body: { sent: true },

@ -3,11 +3,13 @@ import AppContext from '../../../../context'
import { authPassthru } from '../../../proxy'
export default function (server: Server, ctx: AppContext) {
const { moderationAgent } = ctx
if (!moderationAgent) return
server.com.atproto.admin.updateCommunicationTemplate({
auth: ctx.authVerifier.role,
handler: async ({ req, input }) => {
const { data: result } =
await ctx.moderationAgent.com.atproto.admin.updateCommunicationTemplate(
await moderationAgent.com.atproto.admin.updateCommunicationTemplate(
input.body,
authPassthru(req, true),
)

@ -33,7 +33,7 @@ export default function (server: Server, ctx: AppContext) {
}
// this is not someone on our server, but we help with resolving anyway
if (!did) {
if (!did && ctx.appViewAgent) {
did = await tryResolveFromAppView(ctx.appViewAgent, handle)
}

@ -1,16 +1,22 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { InvalidRequestError } from '@atproto/xrpc-server'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.moderation.createReport({
auth: ctx.authVerifier.accessCheckTakedown,
handler: async ({ input, auth }) => {
const requester = auth.credentials.did
if (!ctx.reportingAgent) {
throw new InvalidRequestError(
'Your hosting service is not configured with a moderation provider. If this seems in error, reach out to your hosting provider.',
)
}
const { data: result } =
await ctx.moderationAgent.com.atproto.moderation.createReport(
await ctx.reportingAgent.com.atproto.moderation.createReport(
input.body,
{
...(await ctx.moderationAuthHeaders(requester)),
...(await ctx.reportingAuthHeaders(requester)),
encoding: 'application/json',
},
)

@ -28,6 +28,10 @@ export default function (server: Server, ctx: AppContext) {
}
}
if (!ctx.cfg.bskyAppView) {
throw new InvalidRequestError(`Could not locate record`)
}
return await pipethrough(
ctx.cfg.bskyAppView.url,
'com.atproto.repo.getRecord',

@ -97,7 +97,7 @@ export type AuthVerifierOpts = {
dids: {
pds: string
entryway?: string
admin: string
admin?: string
}
}
@ -254,6 +254,9 @@ export class AuthVerifier {
}
adminService = async (reqCtx: ReqCtx): Promise<AdminServiceOutput> => {
if (!this.dids.admin) {
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
}
const payload = await this.verifyServiceJwt(reqCtx, {
aud: this.dids.entryway ?? this.dids.pds,
iss: [this.dids.admin],

@ -168,19 +168,46 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY,
}
assert(env.bskyAppViewUrl)
assert(env.bskyAppViewDid)
const bskyAppViewCfg: ServerConfig['bskyAppView'] = {
url: env.bskyAppViewUrl,
did: env.bskyAppViewDid,
cdnUrlPattern: env.bskyAppViewCdnUrlPattern,
let bskyAppViewCfg: ServerConfig['bskyAppView'] = null
if (env.bskyAppViewUrl) {
assert(
env.bskyAppViewDid,
'if bsky appview service url is configured, must configure its did as well.',
)
bskyAppViewCfg = {
url: env.bskyAppViewUrl,
did: env.bskyAppViewDid,
cdnUrlPattern: env.bskyAppViewCdnUrlPattern,
}
}
assert(env.modServiceUrl)
assert(env.modServiceDid)
const modServiceCfg: ServerConfig['modService'] = {
url: env.modServiceUrl,
did: env.modServiceDid,
let modServiceCfg: ServerConfig['modService'] = null
if (env.modServiceUrl) {
assert(
env.modServiceDid,
'if mod service url is configured, must configure its did as well.',
)
modServiceCfg = {
url: env.modServiceUrl,
did: env.modServiceDid,
}
}
let reportServiceCfg: ServerConfig['reportService'] = null
if (env.reportServiceUrl) {
assert(
env.reportServiceDid,
'if report service url is configured, must configure its did as well.',
)
reportServiceCfg = {
url: env.reportServiceUrl,
did: env.reportServiceDid,
}
}
// if there's a mod service, default report service into it
if (modServiceCfg && !reportServiceCfg) {
reportServiceCfg = modServiceCfg
}
const redisCfg: ServerConfig['redis'] = env.redisScratchAddress
@ -216,6 +243,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
subscription: subscriptionCfg,
bskyAppView: bskyAppViewCfg,
modService: modServiceCfg,
reportService: reportServiceCfg,
redis: redisCfg,
rateLimits: rateLimitsCfg,
crawlers: crawlersCfg,
@ -233,8 +261,9 @@ export type ServerConfig = {
email: EmailConfig | null
moderationEmail: EmailConfig | null
subscription: SubscriptionConfig
bskyAppView: BksyAppViewConfig
modService: ModServiceConfig
bskyAppView: BksyAppViewConfig | null
modService: ModServiceConfig | null
reportService: ReportServiceConfig | null
redis: RedisScratchConfig | null
rateLimits: RateLimitsConfig
crawlers: string[]
@ -344,3 +373,8 @@ export type ModServiceConfig = {
url: string
did: string
}
export type ReportServiceConfig = {
url: string
did: string
}

@ -76,6 +76,10 @@ export const readEnv = (): ServerEnvironment => {
modServiceUrl: envStr('PDS_MOD_SERVICE_URL'),
modServiceDid: envStr('PDS_MOD_SERVICE_DID'),
// report service
reportServiceUrl: envStr('PDS_REPORT_SERVICE_URL'),
reportServiceDid: envStr('PDS_REPORT_SERVICE_DID'),
// rate limits
rateLimitsEnabled: envBool('PDS_RATE_LIMITS_ENABLED'),
rateLimitBypassKey: envStr('PDS_RATE_LIMIT_BYPASS_KEY'),
@ -176,6 +180,10 @@ export type ServerEnvironment = {
modServiceUrl?: string
modServiceDid?: string
// report service
reportServiceUrl?: string
reportServiceDid?: string
// rate limits
rateLimitsEnabled?: boolean
rateLimitBypassKey?: string

@ -1,3 +1,4 @@
import assert from 'node:assert'
import * as nodemailer from 'nodemailer'
import { Redis } from 'ioredis'
import * as plc from '@did-plc/lib'
@ -42,8 +43,9 @@ export type AppContextOptions = {
backgroundQueue: BackgroundQueue
redisScratch?: Redis
crawlers: Crawlers
appViewAgent: AtpAgent
moderationAgent: AtpAgent
appViewAgent?: AtpAgent
moderationAgent?: AtpAgent
reportingAgent?: AtpAgent
entrywayAgent?: AtpAgent
authVerifier: AuthVerifier
plcRotationKey: crypto.Keypair
@ -67,8 +69,9 @@ export class AppContext {
public backgroundQueue: BackgroundQueue
public redisScratch?: Redis
public crawlers: Crawlers
public appViewAgent: AtpAgent
public moderationAgent: AtpAgent
public appViewAgent: AtpAgent | undefined
public moderationAgent: AtpAgent | undefined
public reportingAgent: AtpAgent | undefined
public entrywayAgent: AtpAgent | undefined
public authVerifier: AuthVerifier
public plcRotationKey: crypto.Keypair
@ -90,6 +93,7 @@ export class AppContext {
this.crawlers = opts.crawlers
this.appViewAgent = opts.appViewAgent
this.moderationAgent = opts.moderationAgent
this.reportingAgent = opts.reportingAgent
this.entrywayAgent = opts.entrywayAgent
this.authVerifier = opts.authVerifier
this.plcRotationKey = opts.plcRotationKey
@ -161,9 +165,15 @@ export class AppContext {
? getRedisClient(cfg.redis.address, cfg.redis.password)
: undefined
const appViewAgent = new AtpAgent({ service: cfg.bskyAppView.url })
const moderationAgent = new AtpAgent({ service: cfg.modService.url })
const appViewAgent = cfg.bskyAppView
? new AtpAgent({ service: cfg.bskyAppView.url })
: undefined
const moderationAgent = cfg.modService
? new AtpAgent({ service: cfg.modService.url })
: undefined
const reportingAgent = cfg.reportService
? new AtpAgent({ service: cfg.reportService.url })
: undefined
const entrywayAgent = cfg.entryway
? new AtpAgent({ service: cfg.entryway.url })
: undefined
@ -189,7 +199,7 @@ export class AppContext {
dids: {
pds: cfg.service.did,
entryway: cfg.entryway?.did,
admin: cfg.modService.did,
admin: cfg.modService?.did,
},
})
@ -211,8 +221,8 @@ export class AppContext {
accountManager,
appViewAgent,
pdsHostname: cfg.service.hostname,
appviewDid: cfg.bskyAppView.did,
appviewCdnUrlPattern: cfg.bskyAppView.cdnUrlPattern,
appviewDid: cfg.bskyAppView?.did,
appviewCdnUrlPattern: cfg.bskyAppView?.cdnUrlPattern,
})
return new AppContext({
@ -231,6 +241,7 @@ export class AppContext {
crawlers,
appViewAgent,
moderationAgent,
reportingAgent,
entrywayAgent,
authVerifier,
plcRotationKey,
@ -240,13 +251,20 @@ export class AppContext {
}
async appviewAuthHeaders(did: string) {
assert(this.cfg.bskyAppView)
return this.serviceAuthHeaders(did, this.cfg.bskyAppView.did)
}
async moderationAuthHeaders(did: string) {
assert(this.cfg.modService)
return this.serviceAuthHeaders(did, this.cfg.modService.did)
}
async reportingAuthHeaders(did: string) {
assert(this.cfg.reportService)
return this.serviceAuthHeaders(did, this.cfg.reportService.did)
}
async serviceAuthHeaders(did: string, aud: string) {
const keypair = await this.actorStore.keypair(did)
return createServiceAuthHeaders({

@ -1,4 +1,5 @@
import util from 'util'
import util from 'node:util'
import assert from 'node:assert'
import AtpAgent from '@atproto/api'
import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env'
import basicSeed from '../seeds/basic'
@ -43,6 +44,7 @@ describe('proxy read after write', () => {
})
it('handles image formatting', async () => {
assert(network.pds.ctx.cfg.bskyAppView)
const blob = await sc.uploadFile(
alice,
'../dev-env/src/seed/img/key-landscape-small.jpg',
@ -123,6 +125,7 @@ describe('proxy read after write', () => {
})
it('handles read after write on threads with record embeds', async () => {
assert(network.pds.ctx.cfg.bskyAppView)
const img = await sc.uploadFile(
alice,
'../dev-env/src/seed/img/key-landscape-small.jpg',