From 6a26396b03ae3038e389417f83315e5bfe92f7ce Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 11:46:00 -0600 Subject: [PATCH 01/34] tidy bsky auth --- packages/bsky/src/auth-verifier.ts | 24 ++++++++++++------------ packages/bsky/src/index.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 7798efa99b2..69335772390 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -45,9 +45,9 @@ type RoleOutput = { } } -type AdminServiceOutput = { +type ModServiceOutput = { credentials: { - type: 'admin_service' + type: 'mod_service' aud: string iss: string } @@ -55,18 +55,18 @@ type AdminServiceOutput = { export type AuthVerifierOpts = { ownDid: string - adminDid: string + modServiceDid: string adminPasses: string[] } export class AuthVerifier { public ownDid: string - public adminDid: string + public modServiceDid: string private adminPasses: Set constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) { this.ownDid = opts.ownDid - this.adminDid = opts.adminDid + this.modServiceDid = opts.modServiceDid this.adminPasses = new Set(opts.adminPasses) } @@ -159,17 +159,17 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { + adminService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.adminDid], + iss: [this.modServiceDid], }) - return { credentials: { type: 'admin_service', aud, iss } } + return { credentials: { type: 'mod_service', aud, iss } } } roleOrAdminService = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { return this.adminService(reqCtx) } else { @@ -236,16 +236,16 @@ export class AuthVerifier { } parseCreds( - creds: StandardOutput | RoleOutput | AdminServiceOutput | NullOutput, + creds: StandardOutput | RoleOutput | ModServiceOutput | NullOutput, ) { const viewer = creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'admin_service' + creds.credentials.type === 'mod_service' const canPerformTakedown = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'admin_service' + creds.credentials.type === 'mod_service' return { viewer, canViewTakedowns, diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index a968d85b584..b11fb42a453 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -100,7 +100,7 @@ export class BskyAppView { const authVerifier = new AuthVerifier(dataplane, { ownDid: config.serverDid, - adminDid: config.modServiceDid, + modServiceDid: config.modServiceDid, adminPasses: config.adminPasswords, }) From 125f721e851190427c1f6d7f2d9d76cbed5c9a88 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 11:46:15 -0600 Subject: [PATCH 02/34] hook up new auth verifier --- packages/ozone/src/auth-verifier.ts | 202 ++++++++++++++++++++++++++++ packages/ozone/src/config/config.ts | 12 ++ packages/ozone/src/config/env.ts | 6 +- packages/ozone/src/context.ts | 43 ++---- 4 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 packages/ozone/src/auth-verifier.ts diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts new file mode 100644 index 00000000000..a2fe5955f51 --- /dev/null +++ b/packages/ozone/src/auth-verifier.ts @@ -0,0 +1,202 @@ +import express from 'express' +import * as ui8 from 'uint8arrays' +import { IdResolver } from '@atproto/identity' +import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' + +type ReqCtx = { + req: express.Request +} + +type RoleOutput = { + credentials: { + type: 'role' + isModerator: boolean + isTriage: true + } +} + +type ModeratorOutput = { + credentials: { + type: 'moderator' + aud: string + iss: string + isModerator: boolean + isTriage: true + } +} + +type StandardOutput = { + credentials: { + type: 'standard' + aud: string + iss: string + isModerator: boolean + isTriage: boolean + } +} + +type NullOutput = { + credentials: { + type: 'null' + iss: null + } +} + +export type AuthVerifierOpts = { + serviceDid: string + moderators: string[] + triage: string[] + adminPassword: string + moderatorPassword: string + triagePassword: string +} + +export class AuthVerifier { + serviceDid: string + moderators: string[] + triage: string[] + private adminPassword: string + private moderatorPassword: string + private triagePassword: string + + constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { + this.serviceDid = opts.serviceDid + this.moderators = opts.moderators + this.triage = opts.triage + this.adminPassword = opts.adminPassword + this.moderatorPassword = opts.moderatorPassword + this.triagePassword = opts.triagePassword + } + + modOrRole = async (reqCtx: ReqCtx): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.moderator(reqCtx) + } else { + return this.role(reqCtx) + } + } + + moderator = async (reqCtx: ReqCtx): Promise => { + const creds = await this.standard(reqCtx) + if (!creds.credentials.isTriage) { + throw new AuthRequiredError('not a moderator account') + } + return { + credentials: { + ...creds.credentials, + type: 'moderator', + isTriage: true, + }, + } + } + + standard = async (reqCtx: ReqCtx): Promise => { + const getSigningKey = async ( + did: string, + forceRefresh: boolean, + ): Promise => { + const atprotoData = await this.idResolver.did.resolveAtprotoData( + did, + forceRefresh, + ) + return atprotoData.signingKey + } + + const jwtStr = getJwtStrFromReq(reqCtx.req) + if (!jwtStr) { + throw new AuthRequiredError('missing jwt', 'MissingJwt') + } + const payload = await verifyJwt(jwtStr, this.serviceDid, getSigningKey) + const iss = payload.iss + const isModerator = this.moderators.includes(iss) + const isTriage = isModerator || this.triage.includes(iss) + return { + credentials: { + type: 'standard', + iss, + aud: payload.aud, + isModerator, + isTriage, + }, + } + } + + standardOptional = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.standard(reqCtx) + } + return this.nullCreds() + } + + role = async (reqCtx: ReqCtx): Promise => { + const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '') + const { username, password } = parsed ?? {} + if (username === 'admin') { + throw new AuthRequiredError() + } + if (password === this.triagePassword) { + return { + credentials: { + type: 'role', + isModerator: false, + isTriage: true, + }, + } + } else if ( + password === this.moderatorPassword || + password === this.adminPassword + ) { + return { + credentials: { + type: 'role', + isModerator: true, + isTriage: true, + }, + } + } else { + throw new AuthRequiredError() + } + } + + nullCreds(): NullOutput { + return { + credentials: { + type: 'null', + iss: null, + }, + } + } +} + +const BEARER = 'Bearer ' +const BASIC = 'Basic ' + +const isBearerToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BEARER) ?? false +} + +export const getJwtStrFromReq = (req: express.Request): string | null => { + const { authorization } = req.headers + if (!authorization?.startsWith(BEARER)) { + return null + } + return authorization.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 } +} diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 32ed8ba5cb5..3f9c38f96c8 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -43,12 +43,18 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { plcUrl: env.didPlcUrl, } + const accessCfg: OzoneConfig['access'] = { + moderators: env.moderatorDids, + triage: env.triageDids, + } + return { service: serviceCfg, db: dbCfg, appview: appviewCfg, pds: pdsCfg, identity: identityCfg, + access: accessCfg, } } @@ -58,6 +64,7 @@ export type OzoneConfig = { appview: AppviewConfig pds: PdsConfig | null identity: IdentityConfig + access: AccessConfig } export type ServiceConfig = { @@ -88,3 +95,8 @@ export type PdsConfig = { export type IdentityConfig = { plcUrl: string } + +export type AccessConfig = { + moderators: string[] + triage: string[] +} diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index b0ad10074eb..fcb6eb31d26 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -1,4 +1,4 @@ -import { envInt, envStr } from '@atproto/common' +import { envInt, envList, envStr } from '@atproto/common' export const readEnv = (): OzoneEnvironment => { return { @@ -17,6 +17,8 @@ export const readEnv = (): OzoneEnvironment => { dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'), dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'), didPlcUrl: envStr('OZONE_DID_PLC_URL'), + moderatorDids: envList('OZONE_MODERATOR_DIDS'), + triageDids: envList('OZONE_TRIAGE_DIDS'), adminPassword: envStr('OZONE_ADMIN_PASSWORD'), moderatorPassword: envStr('OZONE_MODERATOR_PASSWORD'), triagePassword: envStr('OZONE_TRIAGE_PASSWORD'), @@ -40,6 +42,8 @@ export type OzoneEnvironment = { dbPoolMaxUses?: number dbPoolIdleTimeoutMs?: number didPlcUrl?: string + moderatorDids: string[] + triageDids: string[] adminPassword?: string moderatorPassword?: string triagePassword?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 00ef4bd71ba..726a500ea38 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -6,7 +6,6 @@ import { createServiceAuthHeaders } from '@atproto/xrpc-server' import { Database } from './db' import { OzoneConfig, OzoneSecrets } from './config' import { ModerationService, ModerationServiceCreator } from './mod-service' -import * as auth from './auth' import { BackgroundQueue } from './background' import assert from 'assert' import { EventPusher } from './daemon' @@ -15,6 +14,7 @@ import { CommunicationTemplateService, CommunicationTemplateServiceCreator, } from './communication-service/template' +import { AuthVerifier } from './auth-verifier' export type AppContextOptions = { db: Database @@ -27,6 +27,7 @@ export type AppContextOptions = { idResolver: IdResolver backgroundQueue: BackgroundQueue sequencer: Sequencer + authVerifier: AuthVerifier } export class AppContext { @@ -81,6 +82,15 @@ export class AppContext { const sequencer = new Sequencer(db) + const authVerifier = new AuthVerifier(idResolver, { + serviceDid: cfg.service.did, + moderators: cfg.access.moderators, + triage: cfg.access.triage, + adminPassword: secrets.adminPassword, + moderatorPassword: secrets.moderatorPassword, + triagePassword: secrets.triagePassword, + }) + return new AppContext( { db, @@ -93,6 +103,7 @@ export class AppContext { idResolver, backgroundQueue, sequencer, + authVerifier, ...(overrides ?? {}), }, secrets, @@ -151,34 +162,8 @@ export class AppContext { return this.opts.sequencer } - get authVerifier() { - return auth.authVerifier(this.idResolver, { aud: this.cfg.service.did }) - } - - 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.service.did, - }) - } - - get authOptionalAccessOrRoleVerifier() { - return auth.authOptionalAccessOrRoleVerifier( - this.idResolver, - this.secrets, - this.cfg.service.did, - ) - } - - get roleVerifier() { - return auth.roleVerifier(this.secrets) + get authVerifier(): AuthVerifier { + return this.opts.authVerifier } async serviceAuthHeaders(aud: string) { From 36da1d90d48736afe93645c433dcf669a9bc74c4 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 12:12:15 -0600 Subject: [PATCH 03/34] update auth throughout ozone --- packages/dev-env/src/ozone.ts | 3 + .../api/admin/createCommunicationTemplate.ts | 4 +- .../api/admin/deleteCommunicationTemplate.ts | 4 +- .../src/api/admin/emitModerationEvent.ts | 8 +- .../ozone/src/api/admin/getModerationEvent.ts | 2 +- packages/ozone/src/api/admin/getRecord.ts | 4 +- packages/ozone/src/api/admin/getRepo.ts | 4 +- .../api/admin/listCommunicationTemplates.ts | 4 +- .../src/api/admin/queryModerationEvents.ts | 2 +- .../src/api/admin/queryModerationStatuses.ts | 2 +- packages/ozone/src/api/admin/searchRepos.ts | 2 +- .../api/admin/updateCommunicationTemplate.ts | 4 +- packages/ozone/src/api/index.ts | 2 + .../ozone/src/api/moderation/createReport.ts | 4 +- packages/ozone/src/api/proxied.ts | 74 +++++++++ packages/ozone/src/api/temp/fetchLabels.ts | 4 +- packages/ozone/src/auth-verifier.ts | 60 ++++--- packages/ozone/src/auth.ts | 147 ------------------ packages/ozone/src/config/config.ts | 2 + packages/ozone/src/config/env.ts | 2 + packages/ozone/src/context.ts | 1 + 21 files changed, 146 insertions(+), 193 deletions(-) create mode 100644 packages/ozone/src/api/proxied.ts delete mode 100644 packages/ozone/src/auth.ts diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 4988a888f61..b14fe308fc2 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -44,6 +44,9 @@ export class TestOzone { adminPassword: ADMIN_PASSWORD, moderatorPassword: MOD_PASSWORD, triagePassword: TRIAGE_PASSWORD, + adminDids: [], + moderatorDids: [], + triageDids: [], } // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." diff --git a/packages/ozone/src/api/admin/createCommunicationTemplate.ts b/packages/ozone/src/api/admin/createCommunicationTemplate.ts index 94d0f4325d9..f05db2d71f2 100644 --- a/packages/ozone/src/api/admin/createCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/createCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.createCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { createdBy, ...template } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to create a communication template', ) diff --git a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts index cb72bb46216..b70028e710d 100644 --- a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.deleteCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { id } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to delete a communication template', ) diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index ef4c5fd2822..473269bffde 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -11,7 +11,7 @@ import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db @@ -29,7 +29,7 @@ export default function (server: Server, ctx: AppContext) { // if less than moderator access then can only take ack and escalation actions if (isTakedownEvent || isReverseTakedownEvent) { - if (!access.moderator) { + if (!access.isModerator) { throw new AuthRequiredError( 'Must be a full moderator to take this type of action', ) @@ -37,7 +37,7 @@ export default function (server: Server, ctx: AppContext) { // Non admins should not be able to take down feed generators if ( - !access.admin && + !access.isAdmin && subject.recordPath?.includes('app.bsky.feed.generator/') ) { throw new AuthRequiredError( @@ -46,7 +46,7 @@ export default function (server: Server, ctx: AppContext) { } } // if less than moderator access then can not apply labels - if (!access.moderator && isLabelEvent) { + if (!access.isModerator && isLabelEvent) { throw new AuthRequiredError('Must be a full moderator to label content') } diff --git a/packages/ozone/src/api/admin/getModerationEvent.ts b/packages/ozone/src/api/admin/getModerationEvent.ts index e02757c79a3..fc6b433789e 100644 --- a/packages/ozone/src/api/admin/getModerationEvent.ts +++ b/packages/ozone/src/api/admin/getModerationEvent.ts @@ -3,7 +3,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationEvent({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const { id } = params const db = ctx.db diff --git a/packages/ozone/src/api/admin/getRecord.ts b/packages/ozone/src/api/admin/getRecord.ts index fbe9a1229d5..061fc87a0d6 100644 --- a/packages/ozone/src/api/admin/getRecord.ts +++ b/packages/ozone/src/api/admin/getRecord.ts @@ -6,7 +6,7 @@ import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params, auth }) => { const db = ctx.db @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { record.repo = addAccountInfoToRepoView( record.repo, accountInfo, - auth.credentials.moderator, + auth.credentials.isModerator, ) return { diff --git a/packages/ozone/src/api/admin/getRepo.ts b/packages/ozone/src/api/admin/getRepo.ts index 5da30f24524..bd0c03c13c6 100644 --- a/packages/ozone/src/api/admin/getRepo.ts +++ b/packages/ozone/src/api/admin/getRepo.ts @@ -5,7 +5,7 @@ import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params, auth }) => { const { did } = params const db = ctx.db @@ -20,7 +20,7 @@ export default function (server: Server, ctx: AppContext) { const repo = addAccountInfoToRepoViewDetail( partialRepo, accountInfo, - auth.credentials.moderator, + auth.credentials.isModerator, ) return { encoding: 'application/json', diff --git a/packages/ozone/src/api/admin/listCommunicationTemplates.ts b/packages/ozone/src/api/admin/listCommunicationTemplates.ts index b2b4d191b39..d8a88947895 100644 --- a/packages/ozone/src/api/admin/listCommunicationTemplates.ts +++ b/packages/ozone/src/api/admin/listCommunicationTemplates.ts @@ -4,12 +4,12 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.listCommunicationTemplates({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ auth }) => { const access = auth.credentials const db = ctx.db - if (!access.moderator) { + if (!access.isModerator) { throw new AuthRequiredError( 'Must be a full moderator to view list of communication template', ) diff --git a/packages/ozone/src/api/admin/queryModerationEvents.ts b/packages/ozone/src/api/admin/queryModerationEvents.ts index 670cda96cbc..959ee2dcd37 100644 --- a/packages/ozone/src/api/admin/queryModerationEvents.ts +++ b/packages/ozone/src/api/admin/queryModerationEvents.ts @@ -4,7 +4,7 @@ import { getEventType } from '../moderation/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.queryModerationEvents({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/queryModerationStatuses.ts b/packages/ozone/src/api/admin/queryModerationStatuses.ts index fc491339ffa..2c4e0d0dd10 100644 --- a/packages/ozone/src/api/admin/queryModerationStatuses.ts +++ b/packages/ozone/src/api/admin/queryModerationStatuses.ts @@ -4,7 +4,7 @@ import { getReviewState } from '../moderation/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.queryModerationStatuses({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/searchRepos.ts b/packages/ozone/src/api/admin/searchRepos.ts index fcdfc1b6d85..6026a5ccdc9 100644 --- a/packages/ozone/src/api/admin/searchRepos.ts +++ b/packages/ozone/src/api/admin/searchRepos.ts @@ -4,7 +4,7 @@ import { mapDefined } from '@atproto/common' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const modService = ctx.modService(ctx.db) diff --git a/packages/ozone/src/api/admin/updateCommunicationTemplate.ts b/packages/ozone/src/api/admin/updateCommunicationTemplate.ts index 3800748fc30..1b1b124e7f9 100644 --- a/packages/ozone/src/api/admin/updateCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/updateCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { id, updatedBy, ...template } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to update a communication template', ) diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index 54e52ffe292..c0bb0649f71 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -15,6 +15,7 @@ import createCommunicationTemplate from './admin/createCommunicationTemplate' import updateCommunicationTemplate from './admin/updateCommunicationTemplate' import deleteCommunicationTemplate from './admin/deleteCommunicationTemplate' import listCommunicationTemplates from './admin/listCommunicationTemplates' +import proxied from './proxied' export * as health from './health' @@ -36,5 +37,6 @@ export default function (server: Server, ctx: AppContext) { createCommunicationTemplate(server, ctx) updateCommunicationTemplate(server, ctx) deleteCommunicationTemplate(server, ctx) + proxied(server, ctx) return server } diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts index e87b957e8d0..3abf5080e7d 100644 --- a/packages/ozone/src/api/moderation/createReport.ts +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -9,10 +9,10 @@ import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ // @TODO anonymous reports w/ optional auth are a temporary measure - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.standardOptionalOrRole, handler: async ({ input, auth }) => { const requester = - 'did' in auth.credentials ? auth.credentials.did : ctx.cfg.service.did + 'iss' in auth.credentials ? auth.credentials.iss : ctx.cfg.service.did const { reasonType, reason } = input.body const subject = subjectFromInput(input.body.subject) diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts new file mode 100644 index 00000000000..b7eef005650 --- /dev/null +++ b/packages/ozone/src/api/proxied.ts @@ -0,0 +1,74 @@ +import { Server } from '../lexicon' +import AppContext from '../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getProfile({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getAuthorFeed({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getPostThread({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollows({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollowers({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index fd0331487d1..890fafdf95c 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -8,13 +8,13 @@ import { export default function (server: Server, ctx: AppContext) { server.com.atproto.temp.fetchLabels({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.standardOptionalOrRole, handler: async ({ auth, params }) => { const { limit } = params const since = params.since !== undefined ? new Date(params.since).toISOString() : '' const includeUnspeccedTakedowns = - auth.credentials.type === 'role' && auth.credentials.admin + auth.credentials.type === 'null' ? false : auth.credentials.isAdmin const labelRes = await ctx.db.db .selectFrom('label') .selectAll() diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index a2fe5955f51..8ea3c58c59c 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -10,6 +10,7 @@ type ReqCtx = { type RoleOutput = { credentials: { type: 'role' + isAdmin: boolean isModerator: boolean isTriage: true } @@ -20,6 +21,7 @@ type ModeratorOutput = { type: 'moderator' aud: string iss: string + isAdmin: boolean isModerator: boolean isTriage: true } @@ -30,6 +32,7 @@ type StandardOutput = { type: 'standard' aud: string iss: string + isAdmin: boolean isModerator: boolean isTriage: boolean } @@ -44,6 +47,7 @@ type NullOutput = { export type AuthVerifierOpts = { serviceDid: string + admins: string[] moderators: string[] triage: string[] adminPassword: string @@ -53,6 +57,7 @@ export type AuthVerifierOpts = { export class AuthVerifier { serviceDid: string + admins: string[] moderators: string[] triage: string[] private adminPassword: string @@ -61,6 +66,7 @@ export class AuthVerifier { constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { this.serviceDid = opts.serviceDid + this.admins = opts.admins this.moderators = opts.moderators this.triage = opts.triage this.adminPassword = opts.adminPassword @@ -108,13 +114,15 @@ export class AuthVerifier { } const payload = await verifyJwt(jwtStr, this.serviceDid, getSigningKey) const iss = payload.iss - const isModerator = this.moderators.includes(iss) + const isAdmin = this.admins.includes(iss) + const isModerator = isAdmin || this.moderators.includes(iss) const isTriage = isModerator || this.triage.includes(iss) return { credentials: { type: 'standard', iss, aud: payload.aud, + isAdmin, isModerator, isTriage, }, @@ -130,34 +138,38 @@ export class AuthVerifier { return this.nullCreds() } + standardOptionalOrRole = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.standard(reqCtx) + } else if (isBasicToken(reqCtx.req)) { + return this.role(reqCtx) + } else { + return this.nullCreds() + } + } + role = async (reqCtx: ReqCtx): Promise => { const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '') const { username, password } = parsed ?? {} - if (username === 'admin') { + if (username !== 'admin') { throw new AuthRequiredError() } - if (password === this.triagePassword) { - return { - credentials: { - type: 'role', - isModerator: false, - isTriage: true, - }, - } - } else if ( - password === this.moderatorPassword || - password === this.adminPassword - ) { - return { - credentials: { - type: 'role', - isModerator: true, - isTriage: true, - }, - } - } else { + const isAdmin = password === this.adminPassword + const isModerator = isAdmin || password === this.moderatorPassword + const isTriage = isModerator || password === this.triagePassword + if (!isTriage) { throw new AuthRequiredError() } + return { + credentials: { + type: 'role', + isAdmin, + isModerator, + isTriage, + }, + } } nullCreds(): NullOutput { @@ -177,6 +189,10 @@ const isBearerToken = (req: express.Request): boolean => { return req.headers.authorization?.startsWith(BEARER) ?? false } +const isBasicToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BASIC) ?? false +} + export const getJwtStrFromReq = (req: express.Request): string | null => { const { authorization } = req.headers if (!authorization?.startsWith(BEARER)) { diff --git a/packages/ozone/src/auth.ts b/packages/ozone/src/auth.ts deleted file mode 100644 index e996f068c49..00000000000 --- a/packages/ozone/src/auth.ts +++ /dev/null @@ -1,147 +0,0 @@ -import express from 'express' -import * as uint8arrays from 'uint8arrays' -import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' -import { OzoneSecrets } 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, - secrets: OzoneSecrets, - serverDid: string, -) => { - const verifyAccess = authVerifier(idResolver, { aud: serverDid }) - const verifyRole = roleVerifier(secrets) - 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 = - (secrets: OzoneSecrets) => - async (reqCtx: { req: express.Request; res: express.Response }) => { - const credentials = getRoleCredentials(secrets, reqCtx.req) - if (!credentials.valid) { - throw new AuthRequiredError() - } - return { credentials } - } - -export const getRoleCredentials = ( - secrets: OzoneSecrets, - req: express.Request, -) => { - const parsed = parseBasicAuth(req.headers.authorization || '') - const { username, password } = parsed ?? {} - if (username === 'admin' && password === secrets.triagePassword) { - return { valid: true, admin: false, moderator: false, triage: true } - } - if (username === 'admin' && password === secrets.moderatorPassword) { - return { valid: true, admin: false, moderator: true, triage: true } - } - if (username === 'admin' && password === secrets.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/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 3f9c38f96c8..e9ffc942b99 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -44,6 +44,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { } const accessCfg: OzoneConfig['access'] = { + admins: env.adminDids, moderators: env.moderatorDids, triage: env.triageDids, } @@ -97,6 +98,7 @@ export type IdentityConfig = { } export type AccessConfig = { + admins: string[] moderators: string[] triage: string[] } diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index fcb6eb31d26..c0e99a4625b 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -17,6 +17,7 @@ export const readEnv = (): OzoneEnvironment => { dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'), dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'), didPlcUrl: envStr('OZONE_DID_PLC_URL'), + adminDids: envList('OZONE_ADMIN_DIDS'), moderatorDids: envList('OZONE_MODERATOR_DIDS'), triageDids: envList('OZONE_TRIAGE_DIDS'), adminPassword: envStr('OZONE_ADMIN_PASSWORD'), @@ -42,6 +43,7 @@ export type OzoneEnvironment = { dbPoolMaxUses?: number dbPoolIdleTimeoutMs?: number didPlcUrl?: string + adminDids: string[] moderatorDids: string[] triageDids: string[] adminPassword?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 726a500ea38..75de011b4bb 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -84,6 +84,7 @@ export class AppContext { const authVerifier = new AuthVerifier(idResolver, { serviceDid: cfg.service.did, + admins: cfg.access.admins, moderators: cfg.access.moderators, triage: cfg.access.triage, adminPassword: secrets.adminPassword, From 32b3de8556fb1641fff12ada398da913c5ce171f Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 12:38:25 -0600 Subject: [PATCH 04/34] handle mod signing keys --- .../api/com/atproto/admin/getAccountInfos.ts | 2 +- .../api/com/atproto/admin/getSubjectStatus.ts | 2 +- .../com/atproto/admin/updateSubjectStatus.ts | 2 +- packages/bsky/src/auth-verifier.ts | 39 ++++++++++++++----- packages/common-web/src/did-doc.ts | 21 ++++++++-- .../api/com/atproto/admin/getAccountInfo.ts | 2 +- .../api/com/atproto/admin/getSubjectStatus.ts | 2 +- .../com/atproto/admin/updateSubjectStatus.ts | 2 +- packages/pds/src/auth-verifier.ts | 37 +++++++++++------- packages/xrpc-server/src/auth.ts | 2 +- 10 files changed, 78 insertions(+), 33 deletions(-) diff --git a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts index e01be2d6383..cee84d53177 100644 --- a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts +++ b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts @@ -5,7 +5,7 @@ import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfos({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { dids } = params const actors = await ctx.hydrator.actor.getActors(dids, true) diff --git a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts index 2ca7bcdc2c9..3be4c1c0185 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.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { did, uri, blob } = params diff --git a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts index bb78832aa93..8256efbe7e5 100644 --- a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -10,7 +10,7 @@ import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/rep export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ input, auth }) => { const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth) if (!canPerformTakedown) { diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 69335772390..02d89680636 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -83,13 +83,21 @@ export class AuthVerifier { if (!this.parseRoleCreds(ctx.req).admin) { throw new AuthRequiredError('bad credentials') } - return { credentials: { type: 'standard', iss, aud } } + return { + credentials: { type: 'standard', iss, aud }, + } } const { iss, aud } = await this.verifyServiceJwt(ctx, { aud: this.ownDid, iss: null, }) - return { credentials: { type: 'standard', iss, aud } } + return { + credentials: { + type: 'standard', + iss, + aud, + }, + } } standardOptional = async ( @@ -159,19 +167,19 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { + modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto-mod`], }) return { credentials: { type: 'mod_service', aud, iss } } } - roleOrAdminService = async ( + roleOrModService = async ( reqCtx: ReqCtx, ): Promise => { if (isBearerToken(reqCtx.req)) { - return this.adminService(reqCtx) + return this.modService(reqCtx) } else { return this.role(reqCtx) } @@ -195,12 +203,13 @@ export class AuthVerifier { opts: { aud: string | null; iss: string[] | null }, ) { const getSigningKey = async ( - did: string, + iss: string, _forceRefresh: boolean, // @TODO consider propagating to dataplane ): Promise => { - if (opts.iss !== null && !opts.iss.includes(did)) { + if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } + const [did, serviceId] = iss.split('#') let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -211,7 +220,8 @@ export class AuthVerifier { throw err } const keys = unpackIdentityKeys(identity.keys) - const didKey = getKeyAsDidKey(keys, { id: 'atproto' }) + const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' + const didKey = getKeyAsDidKey(keys, { id: keyId }) if (!didKey) { throw new AuthRequiredError('missing or bad key') } @@ -226,6 +236,12 @@ export class AuthVerifier { return { iss: payload.iss, aud: payload.aud } } + isModService(iss: string): boolean { + return [this.modServiceDid, `${this.modServiceDid}#atproto-mod`].includes( + iss, + ) + } + nullCreds(): NullOutput { return { credentials: { @@ -242,10 +258,13 @@ export class AuthVerifier { creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'mod_service' + creds.credentials.type === 'mod_service' || + (creds.credentials.type === 'standard' && + this.isModService(creds.credentials.iss)) const canPerformTakedown = (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'mod_service' + return { viewer, canViewTakedowns, diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 541e10d0937..1b84c1787a4 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -27,6 +27,13 @@ export const getHandle = (doc: DidDocument): string | undefined => { // @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto export const getSigningKey = ( doc: DidDocument, +): { type: string; publicKeyMultibase: string } | undefined => { + return getVerificationMaterial(doc, 'atproto') +} + +export const getVerificationMaterial = ( + doc: DidDocument, + keyId: string, ): { type: string; publicKeyMultibase: string } | undefined => { const did = getDid(doc) let keys = doc.verificationMethod @@ -36,7 +43,7 @@ export const getSigningKey = ( keys = [keys] } const found = keys.find( - (key) => key.id === '#atproto' || key.id === `${did}#atproto`, + (key) => key.id === `#${keyId}` || key.id === `${did}#${keyId}`, ) if (!found?.publicKeyMultibase) return undefined return { @@ -44,12 +51,20 @@ export const getSigningKey = ( publicKeyMultibase: found.publicKeyMultibase, } } -export const getSigningDidKey = (doc: DidDocument): string | undefined => { - const parsed = getSigningKey(doc) + +export const getDidKeyForId = ( + doc: DidDocument, + keyId: string, +): string | undefined => { + const parsed = getVerificationMaterial(doc, keyId) if (!parsed) return return `did:key:${parsed.publicKeyMultibase}` } +export const getSigningDidKey = (doc: DidDocument): string | undefined => { + return getDidKeyForId(doc, 'atproto') +} + export const getPdsEndpoint = (doc: DidDocument): string | undefined => { return getServiceEndpoint(doc, { id: '#atproto_pds', diff --git a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts index e258f9714b2..7269bc0dfa9 100644 --- a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts +++ b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts @@ -5,7 +5,7 @@ import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfo({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const [account, invites, invitedBy] = await Promise.all([ ctx.accountManager.getAccount(params.did, { diff --git a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts index 767714cec36..505e3dde4e9 100644 --- a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts @@ -7,7 +7,7 @@ import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSub export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getSubjectStatus({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { did, uri, blob } = params let body: OutputSchema | null = null diff --git a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts index 29991da2b2c..018d447d6eb 100644 --- a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -11,7 +11,7 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ input, auth }) => { // if less than moderator access then cannot perform a takedown if (auth.credentials.type === 'role' && !auth.credentials.moderator) { diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 668791c187f..410b254d5b6 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -12,6 +12,7 @@ import * as jose from 'jose' import KeyEncoder from 'key-encoder' import { AccountManager } from './account-manager' import { softDeleted } from './db' +import { getDidKeyForId } from '@atproto/common' type ReqCtx = { req: express.Request @@ -44,9 +45,9 @@ type RoleOutput = { } } -type AdminServiceOutput = { +type ModServiceOutput = { credentials: { - type: 'service' + type: 'mod_service' aud: string iss: string } @@ -97,7 +98,7 @@ export type AuthVerifierOpts = { dids: { pds: string entryway?: string - admin?: string + modService?: string } } @@ -253,28 +254,28 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { - if (!this.dids.admin) { + modService = async (reqCtx: ReqCtx): Promise => { + if (!this.dids.modService) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.admin], + iss: [this.dids.modService, `${this.dids.modService}#atproto-mod`], }) return { credentials: { - type: 'service', + type: 'mod_service', aud: payload.aud, iss: payload.iss, }, } } - roleOrAdminService = async ( + roleOrModService = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { - return this.adminService(reqCtx) + return this.modService(reqCtx) } else { return this.role(reqCtx) } @@ -337,13 +338,23 @@ export class AuthVerifier { opts: { aud: string | null; iss: string[] | null }, ) { const getSigningKey = async ( - did: string, + iss: string, forceRefresh: boolean, ): Promise => { - if (opts.iss !== null && !opts.iss.includes(did)) { + if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) + const [did, serviceId] = iss.split('#') + const didDoc = await this.idResolver.did.resolve(did, forceRefresh) + if (!didDoc) { + throw new AuthRequiredError('could not resolve iss did') + } + const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' + const didKey = getDidKeyForId(didDoc, keyId) + if (!didKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + return didKey } const jwtStr = bearerTokenFromReq(reqCtx.req) diff --git a/packages/xrpc-server/src/auth.ts b/packages/xrpc-server/src/auth.ts index db6471aa23e..32373248f51 100644 --- a/packages/xrpc-server/src/auth.ts +++ b/packages/xrpc-server/src/auth.ts @@ -48,7 +48,7 @@ const jsonToB64Url = (json: Record): string => { export const verifyJwt = async ( jwtStr: string, ownDid: string | null, // null indicates to skip the audience check - getSigningKey: (did: string, forceRefresh: boolean) => Promise, + getSigningKey: (iss: string, forceRefresh: boolean) => Promise, ): Promise => { const parts = jwtStr.split('.') if (parts.length !== 3) { From b1f07d54471d11902c6d52edbc5a2005de606032 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 13:18:56 -0600 Subject: [PATCH 05/34] add client proxy heads to pds --- packages/common-web/src/did-doc.ts | 4 +- .../pds/src/api/app/bsky/actor/getProfile.ts | 13 ++--- .../pds/src/api/app/bsky/actor/getProfiles.ts | 9 +--- .../src/api/app/bsky/actor/getSuggestions.ts | 7 +-- .../src/api/app/bsky/actor/searchActors.ts | 7 +-- .../app/bsky/actor/searchActorsTypeahead.ts | 7 +-- .../src/api/app/bsky/feed/getActorFeeds.ts | 7 +-- .../src/api/app/bsky/feed/getActorLikes.ts | 13 ++--- .../src/api/app/bsky/feed/getAuthorFeed.ts | 13 ++--- packages/pds/src/api/app/bsky/feed/getFeed.ts | 13 ++--- packages/pds/src/pipethrough.ts | 47 +++++++++++++++---- 11 files changed, 72 insertions(+), 68 deletions(-) diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 541e10d0937..29699ec2d1c 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -73,7 +73,7 @@ export const getNotifEndpoint = (doc: DidDocument): string | undefined => { export const getServiceEndpoint = ( doc: DidDocument, - opts: { id: string; type: string }, + opts: { id: string; type?: string }, ) => { const did = getDid(doc) let services = doc.service @@ -86,7 +86,7 @@ export const getServiceEndpoint = ( (service) => service.id === opts.id || service.id === `${did}${opts.id}`, ) if (!found) return undefined - if (found.type !== opts.type) { + if (opts.type && found.type !== opts.type) { return undefined } if (typeof found.serviceEndpoint !== 'string') { diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index 74de7f3af6d..a63e2a556d7 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfile' import { LocalViewer, @@ -15,16 +14,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.actor.getProfile({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, auth, params }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index 3ab9338c7c5..67a304280f8 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -15,15 +15,10 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfiles({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts index 6bfd65adf74..d085dd097bd 100644 --- a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.getSuggestions', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts index 53c97566818..fd3c2cd731b 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActors({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.searchActors', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index 51e778b24ee..fbbb911e3bd 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.searchActorsTypeahead', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 123bce1785b..1bd9fd4fd17 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorFeeds({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getActorFeeds', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index d8e2f839904..9cc5223937d 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { LocalViewer, @@ -15,16 +14,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getActorLikes({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index c90760bbfd9..2296fa2221c 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { isReasonRepost } from '../../../../lexicon/types/app/bsky/feed/defs' import { @@ -16,16 +15,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index 89c84b18ba8..a785304e4b0 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -16,18 +16,13 @@ export default function (server: Server, ctx: AppContext) { { feed: params.feed }, await ctx.appviewAuthHeaders(requester), ) - const serviceAuthHeaders = await ctx.serviceAuthHeaders( - requester, - feed.view.did, - ) - // forward accept-language header to upstream services - serviceAuthHeaders.headers['accept-language'] = - req.headers['accept-language'] return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeed', params, - serviceAuthHeaders, + requester, + feed.view.did, ) }, }) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a4c11856502..a8123cbbb0a 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,22 +1,35 @@ +import express from 'express' import * as ui8 from 'uint8arrays' import { jsonToLex } from '@atproto/lexicon' -import { HandlerPipeThrough } from '@atproto/xrpc-server' -import { CallOptions, ResponseType, XRPCError } from '@atproto/xrpc' +import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' +import { ResponseType, XRPCError } from '@atproto/xrpc' import { lexicons } from './lexicon/lexicons' import { httpLogger } from './logger' -import { noUndefinedVals } from '@atproto/common' +import { getServiceEndpoint, noUndefinedVals } from '@atproto/common' +import AppContext from './context' export const pipethrough = async ( - serviceUrl: string, + ctx: AppContext, + req: express.Request, nsid: string, params: Record, - opts?: CallOptions, + requester: string, + audOverride?: string, ): Promise => { + const proxyTo = await parseProxyHeader(ctx, req) + const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url + const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did + if (!serviceUrl || !aud) { + throw new InvalidRequestError(`No service configured for ${nsid}`) + } + const reqHeaders = await ctx.serviceAuthHeaders(requester, aud) + // forward accept-language header to upstream services + reqHeaders.headers['accept-language'] = req.headers['accept-language'] const url = constructUrl(serviceUrl, nsid, params) let res: Response let buffer: ArrayBuffer try { - res = await fetch(url, opts) + res = await fetch(url, reqHeaders) buffer = await res.arrayBuffer() } catch (err) { httpLogger.warn({ err }, 'pipethrough network error') @@ -35,11 +48,29 @@ export const pipethrough = async ( const encoding = res.headers.get('content-type') ?? 'application/json' const repoRevHeader = res.headers.get('atproto-repo-rev') const contentLanguage = res.headers.get('content-language') - const headers = noUndefinedVals({ + const resHeaders = noUndefinedVals({ ['atproto-repo-rev']: repoRevHeader ?? undefined, ['content-language']: contentLanguage ?? undefined, }) - return { encoding, buffer, headers } + return { encoding, buffer, headers: resHeaders } +} + +export const parseProxyHeader = async ( + ctx: AppContext, + req: express.Request, +): Promise<{ did: string; serviceUrl: string } | undefined> => { + const proxyTo = req.header('atproto-proxy') + if (!proxyTo) return + const [did, serviceId] = proxyTo.split('#') + const didDoc = await ctx.idResolver.did.resolve(did) + if (!didDoc) { + throw new InvalidRequestError('could not resolve proxy did') + } + const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) + if (!serviceUrl) { + throw new InvalidRequestError('could not resolve proxy did service url') + } + return { did, serviceUrl } } export const constructUrl = ( From ef236a40ab403724c4fc90616efeead745f27b28 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 13:26:52 -0600 Subject: [PATCH 06/34] hook up rest of routes --- .../src/api/app/bsky/feed/getFeedGenerator.ts | 7 +++--- .../api/app/bsky/feed/getFeedGenerators.ts | 7 +++--- .../pds/src/api/app/bsky/feed/getLikes.ts | 9 ++------ .../pds/src/api/app/bsky/feed/getListFeed.ts | 7 +++--- .../src/api/app/bsky/feed/getPostThread.ts | 22 +++---------------- .../pds/src/api/app/bsky/feed/getPosts.ts | 9 ++------ .../src/api/app/bsky/feed/getRepostedBy.ts | 7 +++--- .../api/app/bsky/feed/getSuggestedFeeds.ts | 7 +++--- .../pds/src/api/app/bsky/feed/getTimeline.ts | 9 ++------ .../pds/src/api/app/bsky/feed/searchPosts.ts | 7 +++--- .../pds/src/api/app/bsky/graph/getBlocks.ts | 7 +++--- .../src/api/app/bsky/graph/getFollowers.ts | 11 +++++----- .../pds/src/api/app/bsky/graph/getFollows.ts | 11 +++++----- .../pds/src/api/app/bsky/graph/getList.ts | 9 ++------ .../src/api/app/bsky/graph/getListBlocks.ts | 7 +++--- .../src/api/app/bsky/graph/getListMutes.ts | 7 +++--- .../pds/src/api/app/bsky/graph/getLists.ts | 9 ++------ .../pds/src/api/app/bsky/graph/getMutes.ts | 9 ++------ .../bsky/graph/getSuggestedFollowsByActor.ts | 7 +++--- .../app/bsky/notification/getUnreadCount.ts | 7 +++--- .../bsky/notification/listNotifications.ts | 7 +++--- .../unspecced/getPopularFeedGenerators.ts | 7 +++--- .../bsky/unspecced/getTaggedSuggestions.ts | 7 +++--- .../pds/src/api/com/atproto/repo/getRecord.ts | 8 ++----- packages/pds/src/pipethrough.ts | 6 +++-- 25 files changed, 87 insertions(+), 123 deletions(-) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index f71ea74117f..e46eff5cf40 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerator({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeedGenerator', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index c07c3dac228..828f6cc204d 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeedGenerators', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 90a96681c85..263864fb286 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getLikes({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.feed.getLikes', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.feed.getLikes', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 3447a721904..91bf837f3dc 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getListFeed({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getListFeed', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index da09523875b..e8dc8557626 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -3,7 +3,6 @@ import { AtUri } from '@atproto/syntax' import { Headers, XRPCError } from '@atproto/xrpc' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { ThreadViewPost, isThreadViewPost, @@ -30,27 +29,12 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getPostThread({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - - if (!requester) { - return pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - authPassthru(req), - ) - } + const requester = auth.credentials.did try { - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return await handleReadAfterWrite( ctx, diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 89d0d08587d..2d3ee4b5b8c 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPosts({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.feed.getPosts', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.feed.getPosts', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 971d150824c..5e8a7900dd4 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getRepostedBy({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getRepostedBy', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 6da81787533..92b68e597b8 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getSuggestedFeeds', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index 90fc5bac42f..d2c2a8e6019 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -15,14 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return await handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/feed/searchPosts.ts b/packages/pds/src/api/app/bsky/feed/searchPosts.ts index 7cc09c864e5..99ca276cddc 100644 --- a/packages/pds/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/searchPosts.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.searchPosts({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.searchPosts', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 1b29f9b62d2..96899f2e907 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getBlocks', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index 0a158f2bbe5..a5b5305dd3e 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -1,21 +1,20 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -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, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null + const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getFollowers', params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index 6802acda888..e67deeb7dda 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -1,21 +1,20 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -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, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null + const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getFollows', params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 6ef1dbf7ee0..3a7ec9bcae8 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getList({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getList', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getList', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index d9aed6e7cd6..b2e982f4e95 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getListBlocks', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index 575c09d5b1a..38ef27df8c0 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getListMutes', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index c824c9cdb4b..b9d9df274bb 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getLists({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getLists', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getLists', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index d422237dd0f..0a1a87f31de 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getMutes({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getMutes', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getMutes', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index dfe453be8f6..21c77ebc212 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getSuggestedFollowsByActor', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index d6b8a235ba3..b845942234c 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.notification.getUnreadCount', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 005473eb6f4..88492ef2322 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.notification.listNotifications', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index da7be6fb649..0045c8d59e4 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,13 +8,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.unspecced.getPopularFeedGenerators', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts index 68e84985441..d1b05bad671 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -8,13 +8,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getTaggedSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.unspecced.getTaggedSuggestions', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 3d8b44099d4..de517dd807f 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -5,7 +5,7 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - server.com.atproto.repo.getRecord(async ({ params }) => { + server.com.atproto.repo.getRecord(async ({ req, params }) => { const { repo, collection, rkey, cid } = params const did = await ctx.accountManager.getDidForActor(repo) @@ -32,10 +32,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not locate record`) } - return await pipethrough( - ctx.cfg.bskyAppView.url, - 'com.atproto.repo.getRecord', - params, - ) + return await pipethrough(ctx, req, 'com.atproto.repo.getRecord', params) }) } diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a8123cbbb0a..a25ac4604c9 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -13,7 +13,7 @@ export const pipethrough = async ( req: express.Request, nsid: string, params: Record, - requester: string, + requester?: string, audOverride?: string, ): Promise => { const proxyTo = await parseProxyHeader(ctx, req) @@ -22,7 +22,9 @@ export const pipethrough = async ( if (!serviceUrl || !aud) { throw new InvalidRequestError(`No service configured for ${nsid}`) } - const reqHeaders = await ctx.serviceAuthHeaders(requester, aud) + const reqHeaders = requester + ? await ctx.serviceAuthHeaders(requester, aud) + : { headers: {} } // forward accept-language header to upstream services reqHeaders.headers['accept-language'] = req.headers['accept-language'] const url = constructUrl(serviceUrl, nsid, params) From bfbb58674be02af26568dc4710e62421a7ee9439 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 14:54:35 -0600 Subject: [PATCH 07/34] simplify pipethrough & add some SSRF protection --- packages/dev-env/src/pds.ts | 1 + .../pds/src/api/app/bsky/actor/getProfile.ts | 4 ++-- .../pds/src/api/app/bsky/actor/getProfiles.ts | 4 ++-- .../src/api/app/bsky/actor/getSuggestions.ts | 10 ++-------- .../pds/src/api/app/bsky/actor/searchActors.ts | 10 ++-------- .../api/app/bsky/actor/searchActorsTypeahead.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getActorFeeds.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getActorLikes.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getAuthorFeed.ts | 4 ++-- packages/pds/src/api/app/bsky/feed/getFeed.ts | 9 +-------- .../src/api/app/bsky/feed/getFeedGenerator.ts | 10 ++-------- .../src/api/app/bsky/feed/getFeedGenerators.ts | 10 ++-------- packages/pds/src/api/app/bsky/feed/getLikes.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getListFeed.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getPostThread.ts | 4 ++-- packages/pds/src/api/app/bsky/feed/getPosts.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getRepostedBy.ts | 10 ++-------- .../src/api/app/bsky/feed/getSuggestedFeeds.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getTimeline.ts | 4 ++-- .../pds/src/api/app/bsky/feed/searchPosts.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getBlocks.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getFollowers.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getFollows.ts | 10 ++-------- packages/pds/src/api/app/bsky/graph/getList.ts | 4 ++-- .../pds/src/api/app/bsky/graph/getListBlocks.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getListMutes.ts | 10 ++-------- packages/pds/src/api/app/bsky/graph/getLists.ts | 4 ++-- packages/pds/src/api/app/bsky/graph/getMutes.ts | 4 ++-- .../bsky/graph/getSuggestedFollowsByActor.ts | 10 ++-------- .../api/app/bsky/notification/getUnreadCount.ts | 10 ++-------- .../app/bsky/notification/listNotifications.ts | 10 ++-------- .../bsky/unspecced/getPopularFeedGenerators.ts | 10 ++-------- .../app/bsky/unspecced/getTaggedSuggestions.ts | 10 ++-------- .../pds/src/api/com/atproto/repo/getRecord.ts | 2 +- packages/pds/src/config/config.ts | 2 ++ packages/pds/src/config/env.ts | 2 ++ packages/pds/src/pipethrough.ts | 17 +++++++++++++---- 37 files changed, 82 insertions(+), 195 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 44b8a063fce..1e76b4341a1 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -35,6 +35,7 @@ export class TestPds { await fs.mkdir(dataDirectory, { recursive: true }) const env: pds.ServerEnvironment = { + devMode: true, port, dataDirectory: dataDirectory, blobstoreDiskLocation: blobstoreLoc, diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index a63e2a556d7..f05465d267a 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfile({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index 67a304280f8..3d1b2f1f579 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -15,10 +15,10 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfiles({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts index d085dd097bd..fadcac2e9fc 100644 --- a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.getSuggestions', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts index fd3c2cd731b..777f4e7a2d0 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActors({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.searchActors', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index fbbb911e3bd..58d5df0d049 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.searchActorsTypeahead', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 1bd9fd4fd17..16082a916e8 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorFeeds({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getActorFeeds', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index 9cc5223937d..3d17bff8e05 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorLikes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 2296fa2221c..88f646842a5 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -16,9 +16,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getAuthorFeed({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index a785304e4b0..aedbe5b4f23 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -16,14 +16,7 @@ export default function (server: Server, ctx: AppContext) { { feed: params.feed }, await ctx.appviewAuthHeaders(requester), ) - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeed', - params, - requester, - feed.view.did, - ) + return pipethrough(ctx, req, requester, feed.view.did) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index e46eff5cf40..278ee9af7c2 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerator({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeedGenerator', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index 828f6cc204d..2aa79b03853 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeedGenerators', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 263864fb286..226b10feb9d 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getLikes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.feed.getLikes', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 91bf837f3dc..2d059eb442f 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getListFeed({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getListFeed', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index e8dc8557626..c6231d4bf04 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -30,11 +30,11 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPostThread({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did try { - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return await handleReadAfterWrite( ctx, diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 2d3ee4b5b8c..a07be55a516 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPosts({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.feed.getPosts', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 5e8a7900dd4..3b8cff2f4cf 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getRepostedBy({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getRepostedBy', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 92b68e597b8..002b24ef3f0 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getSuggestedFeeds', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index d2c2a8e6019..05f0a0ea90f 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return await handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/feed/searchPosts.ts b/packages/pds/src/api/app/bsky/feed/searchPosts.ts index 99ca276cddc..45ad78ad401 100644 --- a/packages/pds/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/searchPosts.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.searchPosts({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.searchPosts', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 96899f2e907..12cc5603eb7 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getBlocks', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index a5b5305dd3e..78f433598aa 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getFollowers({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getFollowers', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index e67deeb7dda..3c62584ee98 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getFollows({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getFollows', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 3a7ec9bcae8..0b39c8eeae5 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getList({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getList', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index b2e982f4e95..472a7c6f24c 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getListBlocks', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index 38ef27df8c0..f77f35155a5 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getListMutes', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index b9d9df274bb..8aabf93ba78 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getLists({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getLists', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index 0a1a87f31de..0d3c131d28e 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getMutes({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getMutes', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index 21c77ebc212..1c3a7d4ed86 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getSuggestedFollowsByActor', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index b845942234c..d5f09f92bb3 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.notification.getUnreadCount', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 88492ef2322..68d08fde137 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.notification.listNotifications', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 0045c8d59e4..0d53ffff5ad 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,15 +8,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.unspecced.getPopularFeedGenerators', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts index d1b05bad671..c9d9ed90dfe 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -8,15 +8,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getTaggedSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.unspecced.getTaggedSuggestions', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index de517dd807f..5a0d0fae441 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -32,6 +32,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not locate record`) } - return await pipethrough(ctx, req, 'com.atproto.repo.getRecord', params) + return await pipethrough(ctx, req) }) } diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 8ca807d39b5..bbdb5ee797c 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -24,6 +24,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { termsOfServiceUrl: env.termsOfServiceUrl, acceptingImports: env.acceptingImports ?? true, blobUploadLimit: env.blobUploadLimit ?? 5 * 1024 * 1024, // 5mb + devMode: env.devMode ?? false, } const dbLoc = (name: string) => { @@ -280,6 +281,7 @@ export type ServiceConfig = { termsOfServiceUrl?: string acceptingImports: boolean blobUploadLimit: number + devMode: boolean } export type DatabaseConfig = { diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index fb5aed8232f..a334c0c51d3 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -11,6 +11,7 @@ export const readEnv = (): ServerEnvironment => { termsOfServiceUrl: envStr('PDS_TERMS_OF_SERVICE_URL'), acceptingImports: envBool('PDS_ACCEPTING_REPO_IMPORTS'), blobUploadLimit: envInt('PDS_BLOB_UPLOAD_LIMIT'), + devMode: envBool('PDS_DEV_MODE'), // database dataDirectory: envStr('PDS_DATA_DIRECTORY'), @@ -118,6 +119,7 @@ export type ServerEnvironment = { termsOfServiceUrl?: string acceptingImports?: boolean blobUploadLimit?: number + devMode?: boolean // database dataDirectory?: string diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a25ac4604c9..5dcffd31d99 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,5 +1,6 @@ import express from 'express' import * as ui8 from 'uint8arrays' +import net from 'node:net' import { jsonToLex } from '@atproto/lexicon' import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' @@ -11,8 +12,6 @@ import AppContext from './context' export const pipethrough = async ( ctx: AppContext, req: express.Request, - nsid: string, - params: Record, requester?: string, audOverride?: string, ): Promise => { @@ -20,14 +19,17 @@ export const pipethrough = async ( const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did if (!serviceUrl || !aud) { - throw new InvalidRequestError(`No service configured for ${nsid}`) + throw new InvalidRequestError(`No service configured for ${req.path}`) + } + const url = new URL(req.originalUrl, serviceUrl) + if (!ctx.cfg.service.devMode && !isSafeUrl(url)) { + throw new InvalidRequestError(`Invalid service url: ${url.toString()}`) } const reqHeaders = requester ? await ctx.serviceAuthHeaders(requester, aud) : { headers: {} } // forward accept-language header to upstream services reqHeaders.headers['accept-language'] = req.headers['accept-language'] - const url = constructUrl(serviceUrl, nsid, params) let res: Response let buffer: ArrayBuffer try { @@ -98,6 +100,13 @@ export const constructUrl = ( return uri.toString() } +const isSafeUrl = (url: URL) => { + if (url.protocol !== 'https:') return false + if (!url.hostname || url.hostname === 'localhost') return false + if (net.isIP(url.hostname) === 0) return false + return true +} + export const parseRes = (nsid: string, res: HandlerPipeThrough): T => { const buffer = new Uint8Array(res.buffer) const json = safeParseJson(ui8.toString(buffer, 'utf8')) From 92d92685032fc640a4fb53340ff583a3686bd2bf Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 15:40:03 -0600 Subject: [PATCH 08/34] tests --- packages/pds/src/pipethrough.ts | 3 + .../pds/tests/proxied/proxy-header.test.ts | 168 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 packages/pds/tests/proxied/proxy-header.test.ts diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 5dcffd31d99..90929499a47 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -66,6 +66,9 @@ export const parseProxyHeader = async ( const proxyTo = req.header('atproto-proxy') if (!proxyTo) return const [did, serviceId] = proxyTo.split('#') + if (!serviceId) { + throw new InvalidRequestError('no service id specified') + } const didDoc = await ctx.idResolver.did.resolve(did) if (!didDoc) { throw new InvalidRequestError('could not resolve proxy did') diff --git a/packages/pds/tests/proxied/proxy-header.test.ts b/packages/pds/tests/proxied/proxy-header.test.ts new file mode 100644 index 00000000000..d00dc3bb342 --- /dev/null +++ b/packages/pds/tests/proxied/proxy-header.test.ts @@ -0,0 +1,168 @@ +import http from 'node:http' +import assert from 'node:assert' +import express from 'express' +import axios from 'axios' +import * as plc from '@did-plc/lib' +import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env' +import getPort from 'get-port' +import { Keypair } from '@atproto/crypto' +import { verifyJwt } from '@atproto/xrpc-server' + +describe('proxy header', () => { + let network: TestNetworkNoAppView + let sc: SeedClient + + let alice: string + + let proxyServer: ProxyServer + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'proxy_header', + }) + sc = network.getSeedClient() + await usersSeed(sc) + + proxyServer = await ProxyServer.create( + network.pds.ctx.plcClient, + network.pds.ctx.plcRotationKey, + 'atproto_test', + ) + + alice = sc.dids.alice + await network.processAll() + }) + + afterAll(async () => { + await proxyServer.close() + await network.close() + }) + + const assertAxiosErr = async (promise: Promise, msg: string) => { + try { + await promise + } catch (err) { + if (!axios.isAxiosError(err)) { + throw err + } + expect(err.response?.data?.['message']).toEqual(msg) + return + } + throw new Error('no error thrown') + } + + it('proxies requests based on header', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + await axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `${proxyServer.did}#atproto_test`, + }, + }) + const req = proxyServer.requests.at(-1) + assert(req) + expect(req.url).toEqual(path) + assert(req.auth) + const verified = await verifyJwt( + req.auth.replace('Bearer ', ''), + proxyServer.did, + (iss) => network.pds.ctx.idResolver.did.resolveAtprotoKey(iss, true), + ) + expect(verified.aud).toBe(proxyServer.did) + expect(verified.iss).toBe(alice) + }) + + it('fails on a non-existant did', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `did:plc:12345678123456781234578#atproto_test`, + }, + }) + await assertAxiosErr(attempt, 'could not resolve proxy did') + expect(proxyServer.requests.length).toBe(1) + }) + + it('fails when a service is not specified', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': proxyServer.did, + }, + }) + await assertAxiosErr(attempt, 'no service id specified') + expect(proxyServer.requests.length).toBe(1) + }) + + it('fails on a non-existant service', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `${proxyServer.did}#atproto_bad`, + }, + }) + await assertAxiosErr(attempt, 'could not resolve proxy did service url') + expect(proxyServer.requests.length).toBe(1) + }) +}) + +type ProxyReq = { + url: string + auth: string | undefined +} + +class ProxyServer { + constructor( + public server: http.Server, + public url: string, + public did: string, + public requests: ProxyReq[], + ) {} + + static async create( + plcClient: plc.Client, + keypair: Keypair, + serviceId: string, + ): Promise { + const requests: ProxyReq[] = [] + const app = express() + app.get('*', (req, res) => { + requests.push({ + url: req.url, + auth: req.header('authorization'), + }) + res.sendStatus(200) + }) + const port = await getPort() + const server = app.listen(port) + const url = `http://localhost:${port}` + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + rotationKeys: [keypair.did()], + alsoKnownAs: [], + verificationMethods: {}, + services: { + [serviceId]: { + type: 'TestAtprotoService', + endpoint: url, + }, + }, + prev: null, + }, + keypair, + ) + const did = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(did, plcOp) + return new ProxyServer(server, url, did, requests) + } + + close(): Promise { + return new Promise((resolve) => { + this.server.close(() => resolve()) + }) + } +} From 199b754fbcf2f902633ce100a9b8af382b5d4a12 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 16:12:31 -0600 Subject: [PATCH 09/34] fix bad var --- packages/pds/src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index a9fe996056f..4f852af1497 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -199,7 +199,7 @@ export class AppContext { dids: { pds: cfg.service.did, entryway: cfg.entryway?.did, - admin: cfg.modService?.did, + modService: cfg.modService?.did, }, }) From cb53fdca79fca0c9789b1eefe170540538abde21 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 17:55:55 -0600 Subject: [PATCH 10/34] fix key parsing in pds --- packages/common-web/src/did-doc.ts | 11 ++--------- packages/pds/src/auth-verifier.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 1b84c1787a4..715c364b3ca 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -52,19 +52,12 @@ export const getVerificationMaterial = ( } } -export const getDidKeyForId = ( - doc: DidDocument, - keyId: string, -): string | undefined => { - const parsed = getVerificationMaterial(doc, keyId) +export const getSigningDidKey = (doc: DidDocument): string | undefined => { + const parsed = getSigningKey(doc) if (!parsed) return return `did:key:${parsed.publicKeyMultibase}` } -export const getSigningDidKey = (doc: DidDocument): string | undefined => { - return getDidKeyForId(doc, 'atproto') -} - export const getPdsEndpoint = (doc: DidDocument): string | undefined => { return getServiceEndpoint(doc, { id: '#atproto_pds', diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 410b254d5b6..ea2ebeb868d 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -5,14 +5,14 @@ import { InvalidRequestError, verifyJwt as verifyServiceJwt, } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' +import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity' import * as ui8 from 'uint8arrays' import express from 'express' import * as jose from 'jose' import KeyEncoder from 'key-encoder' import { AccountManager } from './account-manager' import { softDeleted } from './db' -import { getDidKeyForId } from '@atproto/common' +import { getVerificationMaterial } from '@atproto/common' type ReqCtx = { req: express.Request @@ -350,7 +350,11 @@ export class AuthVerifier { throw new AuthRequiredError('could not resolve iss did') } const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' - const didKey = getDidKeyForId(didDoc, keyId) + const parsedKey = getVerificationMaterial(didDoc, keyId) + if (!parsedKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + const didKey = getDidKeyFromMultibase(parsedKey) if (!didKey) { throw new AuthRequiredError('missing or bad key in did doc') } From e3bfb17202edce5e16b5025f435cf90645e86a69 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 19:24:06 -0600 Subject: [PATCH 11/34] fix admin auth test --- packages/bsky/src/auth-verifier.ts | 7 ++-- packages/ozone/src/api/well-known.ts | 4 +-- packages/pds/src/auth-verifier.ts | 5 ++- packages/pds/tests/admin-auth.test.ts | 49 +++++++++++++++++++-------- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 02d89680636..4c5311d73fe 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -170,7 +170,7 @@ export class AuthVerifier { modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid, `${this.modServiceDid}#atproto-mod`], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto_mod`], }) return { credentials: { type: 'mod_service', aud, iss } } } @@ -209,7 +209,7 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, serviceId] = iss.split('#') + const [did, keyId = 'atproto'] = iss.split('#') let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -220,7 +220,6 @@ export class AuthVerifier { throw err } const keys = unpackIdentityKeys(identity.keys) - const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' const didKey = getKeyAsDidKey(keys, { id: keyId }) if (!didKey) { throw new AuthRequiredError('missing or bad key') @@ -237,7 +236,7 @@ export class AuthVerifier { } isModService(iss: string): boolean { - return [this.modServiceDid, `${this.modServiceDid}#atproto-mod`].includes( + return [this.modServiceDid, `${this.modServiceDid}#atproto_mod`].includes( iss, ) } diff --git a/packages/ozone/src/api/well-known.ts b/packages/ozone/src/api/well-known.ts index 9cbfa9efe53..29a9f6d3636 100644 --- a/packages/ozone/src/api/well-known.ts +++ b/packages/ozone/src/api/well-known.ts @@ -15,7 +15,7 @@ export const createRouter = (ctx: AppContext): express.Router => { id: ctx.cfg.service.did, verificationMethod: [ { - id: `${ctx.cfg.service.did}#atproto`, + id: `${ctx.cfg.service.did}#atproto_mod`, type: 'Multikey', controller: ctx.cfg.service.did, publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''), @@ -23,7 +23,7 @@ export const createRouter = (ctx: AppContext): express.Router => { ], service: [ { - id: '#atproto_mod', + id: '#atproto_mod_srvc', type: 'AtprotoModerationService', serviceEndpoint: `https://${hostname}`, }, diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index ea2ebeb868d..5f1b11fd29c 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -260,7 +260,7 @@ export class AuthVerifier { } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.modService, `${this.dids.modService}#atproto-mod`], + iss: [this.dids.modService, `${this.dids.modService}#atproto_mod`], }) return { credentials: { @@ -344,12 +344,11 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, serviceId] = iss.split('#') + const [did, keyId = 'atproto'] = iss.split('#') const didDoc = await this.idResolver.did.resolve(did, forceRefresh) if (!didDoc) { throw new AuthRequiredError('could not resolve iss did') } - const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' const parsedKey = getVerificationMaterial(didDoc, keyId) if (!parsedKey) { throw new AuthRequiredError('missing or bad key in did doc') diff --git a/packages/pds/tests/admin-auth.test.ts b/packages/pds/tests/admin-auth.test.ts index dffd9261874..191f7a5daf4 100644 --- a/packages/pds/tests/admin-auth.test.ts +++ b/packages/pds/tests/admin-auth.test.ts @@ -1,7 +1,8 @@ import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import * as plc from '@did-plc/lib' import usersSeed from './seeds/users' import { RepoRef } from '../src/lexicon/types/com/atproto/admin/defs' @@ -12,32 +13,50 @@ describe('admin auth', () => { let repoSubject: RepoRef - const modServiceDid = 'did:example:mod' - const altModDid = 'did:example:alt' + let modServiceDid: string + let altModDid: string let modServiceKey: Secp256k1Keypair let pdsDid: string + const opAndDid = async (handle: string, key: Keypair) => { + const op = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [handle], + verificationMethods: { + atproto: key.did(), + }, + rotationKeys: [key.did()], + services: {}, + prev: null, + }, + key, + ) + const did = await plc.didForCreateOp(op) + return { op, did } + } + beforeAll(async () => { + // kinda goofy but we need to know the dids before creating the testnet for the PDS's config + modServiceKey = await Secp256k1Keypair.create() + const modServiceInfo = await opAndDid('mod.test', modServiceKey) + const altModInfo = await opAndDid('alt-mod.test', modServiceKey) + modServiceDid = modServiceInfo.did + altModDid = altModInfo.did + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'pds_admin_auth', pds: { - modServiceDid, + modServiceDid: modServiceInfo.did, + modServiceUrl: 'https://mod.invalid', }, }) pdsDid = network.pds.ctx.cfg.service.did - modServiceKey = await Secp256k1Keypair.create() - const origResolve = network.pds.ctx.idResolver.did.resolveAtprotoKey - network.pds.ctx.idResolver.did.resolveAtprotoKey = async ( - did: string, - forceRefresh?: boolean, - ) => { - if (did === modServiceDid || did === altModDid) { - return modServiceKey.did() - } - return origResolve(did, forceRefresh) - } + const plcClient = network.plc.getClient() + await plcClient.sendOperation(modServiceInfo.did, modServiceInfo.op) + await plcClient.sendOperation(altModInfo.did, altModInfo.op) agent = network.pds.getClient() sc = network.getSeedClient() From a642063979df3e9d10f8e0cdc250fd9132046084 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 19:24:41 -0600 Subject: [PATCH 12/34] rename test --- .../pds/tests/{admin-auth.test.ts => moderator-auth.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/pds/tests/{admin-auth.test.ts => moderator-auth.test.ts} (98%) diff --git a/packages/pds/tests/admin-auth.test.ts b/packages/pds/tests/moderator-auth.test.ts similarity index 98% rename from packages/pds/tests/admin-auth.test.ts rename to packages/pds/tests/moderator-auth.test.ts index 191f7a5daf4..0b89b4f0fd7 100644 --- a/packages/pds/tests/admin-auth.test.ts +++ b/packages/pds/tests/moderator-auth.test.ts @@ -6,7 +6,7 @@ import * as plc from '@did-plc/lib' import usersSeed from './seeds/users' import { RepoRef } from '../src/lexicon/types/com/atproto/admin/defs' -describe('admin auth', () => { +describe('moderator auth', () => { let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient @@ -45,7 +45,7 @@ describe('admin auth', () => { altModDid = altModInfo.did network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'pds_admin_auth', + dbPostgresSchema: 'pds_moderator_auth', pds: { modServiceDid: modServiceInfo.did, modServiceUrl: 'https://mod.invalid', From 88c24121783c09a7a869c2b5513b5a6abc5a5edd Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 22:01:02 -0600 Subject: [PATCH 13/34] add pipethrough to write routes --- .../pds/src/api/app/bsky/graph/muteActor.ts | 11 +- .../src/api/app/bsky/graph/muteActorList.ts | 11 +- .../pds/src/api/app/bsky/graph/unmuteActor.ts | 11 +- .../src/api/app/bsky/graph/unmuteActorList.ts | 11 +- .../api/app/bsky/notification/updateSeen.ts | 11 +- .../admin/createCommunicationTemplate.ts | 19 +-- .../admin/deleteCommunicationTemplate.ts | 14 +- .../com/atproto/admin/emitModerationEvent.ts | 19 +-- .../com/atproto/admin/getModerationEvent.ts | 19 +-- .../src/api/com/atproto/admin/getRecord.ts | 19 +-- .../pds/src/api/com/atproto/admin/getRepo.ts | 18 +- .../admin/listCommunicationTemplates.ts | 19 +-- .../atproto/admin/queryModerationEvents.ts | 19 +-- .../atproto/admin/queryModerationStatuses.ts | 19 +-- .../src/api/com/atproto/admin/searchRepos.ts | 19 +-- .../admin/updateCommunicationTemplate.ts | 20 +-- .../com/atproto/moderation/createReport.ts | 22 +-- packages/pds/src/pipethrough.ts | 154 ++++++++++++------ 18 files changed, 174 insertions(+), 261 deletions(-) diff --git a/packages/pds/src/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts index c88a05b9eaf..9613e8736d0 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' 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 }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/api/app/bsky/graph/muteActorList.ts index 74c2357d3d9..41e74ccd713 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' 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 }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts index e73c5d08e5a..8d9898f442e 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' 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 }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts index e36afeaf0a3..dc6e254fbb4 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' 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 }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts index 18a0ea3fa11..e7a97f3eddd 100644 --- a/packages/pds/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' 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 }) => { + handler: async ({ input, auth, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.notification.updateSeen(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts index 7b6939270a2..d3d1ebfd968 100644 --- a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.createCommunicationTemplate( - input.body, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts index d10c2564571..7a612dc1162 100644 --- a/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts @@ -1,17 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.deleteCommunicationTemplate( - input.body, - authPassthru(req, true), - ) + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts index 65bb4c36d0e..1658720d674 100644 --- a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.emitModerationEvent( - input.body, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts index a5e579baa58..9d81d7dee59 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.getModerationEvent( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 3cff5508683..52c7686b5b1 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.getRecord( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: recordDetailAppview, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index 880b407ce79..d380570c16b 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -1,21 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.getRepo( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: res.data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts index dfe3a74bce8..520e0b68c97 100644 --- a/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts +++ b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.listCommunicationTemplates( - {}, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts index 2d33ca6d466..87e173917a6 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.queryModerationEvents( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts index c31125ce114..b10c4c0efc0 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.queryModerationStatuses( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index d09ff7b2327..e3d67470e29 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.searchRepos( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts index c548a83bf03..ef7d05667b2 100644 --- a/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts @@ -1,23 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' 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 moderationAgent.com.atproto.admin.updateCommunicationTemplate( - input.body, - authPassthru(req, true), - ) - - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/moderation/createReport.ts b/packages/pds/src/api/com/atproto/moderation/createReport.ts index 64ed5c20005..f5b65fbd0cb 100644 --- a/packages/pds/src/api/com/atproto/moderation/createReport.ts +++ b/packages/pds/src/api/com/atproto/moderation/createReport.ts @@ -1,29 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ auth: ctx.authVerifier.accessCheckTakedown, - handler: async ({ input, auth }) => { + handler: async ({ req, 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.reportingAgent.com.atproto.moderation.createReport( - input.body, - { - ...(await ctx.reportingAuthHeaders(requester)), - encoding: 'application/json', - }, - ) - return { - encoding: 'application/json', - body: result, - } + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 90929499a47..595584b9388 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,23 +1,110 @@ import express from 'express' import * as ui8 from 'uint8arrays' import net from 'node:net' -import { jsonToLex } from '@atproto/lexicon' +import { LexValue, jsonToLex, stringifyLex } from '@atproto/lexicon' import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' -import { lexicons } from './lexicon/lexicons' +import { ids, lexicons } from './lexicon/lexicons' import { httpLogger } from './logger' import { getServiceEndpoint, noUndefinedVals } from '@atproto/common' import AppContext from './context' +const defaultService = ( + ctx: AppContext, + path: string, +): { url: string; did: string } | null => { + const nsid = path.replace('/xrpc/', '') + switch (nsid) { + case ids.ComAtprotoAdminCreateCommunicationTemplate: + case ids.ComAtprotoAdminDeleteCommunicationTemplate: + case ids.ComAtprotoAdminEmitModerationEvent: + case ids.ComAtprotoAdminGetModerationEvent: + case ids.ComAtprotoAdminGetRecord: + case ids.ComAtprotoAdminGetRepo: + case ids.ComAtprotoAdminListCommunicationTemplates: + case ids.ComAtprotoAdminQueryModerationEvents: + case ids.ComAtprotoAdminQueryModerationStatuses: + case ids.ComAtprotoAdminSearchRepos: + case ids.ComAtprotoAdminUpdateCommunicationTemplate: + return ctx.cfg.modService + case ids.ComAtprotoModerationCreateReport: + return ctx.cfg.reportService + default: + return ctx.cfg.bskyAppView + } +} + export const pipethrough = async ( ctx: AppContext, req: express.Request, requester?: string, audOverride?: string, ): Promise => { + const { url, headers } = await createUrlAndHeaders( + ctx, + req, + requester, + audOverride, + ) + const reqInit: RequestInit = { + headers, + } + return doProxy(url, reqInit) +} + +export const pipethroughProcedure = async ( + ctx: AppContext, + req: express.Request, + body: LexValue, + requester?: string, + audOverride?: string, +) => { + const { url, headers } = await createUrlAndHeaders( + ctx, + req, + requester, + audOverride, + ) + const reqInit: RequestInit & { duplex: string } = { + method: 'post', + headers, + body: new TextEncoder().encode(stringifyLex(body)), + duplex: 'half', + } + return doProxy(url, reqInit) +} + +export const parseProxyHeader = async ( + ctx: AppContext, + req: express.Request, +): Promise<{ did: string; serviceUrl: string } | undefined> => { + const proxyTo = req.header('atproto-proxy') + if (!proxyTo) return + const [did, serviceId] = proxyTo.split('#') + if (!serviceId) { + throw new InvalidRequestError('no service id specified') + } + const didDoc = await ctx.idResolver.did.resolve(did) + if (!didDoc) { + throw new InvalidRequestError('could not resolve proxy did') + } + const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) + if (!serviceUrl) { + throw new InvalidRequestError('could not resolve proxy did service url') + } + return { did, serviceUrl } +} + +export const createUrlAndHeaders = async ( + ctx: AppContext, + req: express.Request, + requester?: string, + audOverride?: string, +): Promise<{ url: URL; headers: { authorization?: string } }> => { const proxyTo = await parseProxyHeader(ctx, req) - const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url - const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did + const defaultProxy = defaultService(ctx, req.path) + const serviceUrl = proxyTo?.serviceUrl ?? defaultProxy?.url + const aud = audOverride ?? proxyTo?.did ?? defaultProxy?.did if (!serviceUrl || !aud) { throw new InvalidRequestError(`No service configured for ${req.path}`) } @@ -25,15 +112,20 @@ export const pipethrough = async ( if (!ctx.cfg.service.devMode && !isSafeUrl(url)) { throw new InvalidRequestError(`Invalid service url: ${url.toString()}`) } - const reqHeaders = requester - ? await ctx.serviceAuthHeaders(requester, aud) - : { headers: {} } + const headers = requester + ? (await ctx.serviceAuthHeaders(requester, aud)).headers + : {} // forward accept-language header to upstream services - reqHeaders.headers['accept-language'] = req.headers['accept-language'] + headers['accept-language'] = req.headers['accept-language'] + headers['content-type'] = req.headers['content-type'] + return { url, headers } +} + +export const doProxy = async (url: URL, reqInit: RequestInit) => { let res: Response let buffer: ArrayBuffer try { - res = await fetch(url, reqHeaders) + res = await fetch(url, reqInit) buffer = await res.arrayBuffer() } catch (err) { httpLogger.warn({ err }, 'pipethrough network error') @@ -59,50 +151,6 @@ export const pipethrough = async ( return { encoding, buffer, headers: resHeaders } } -export const parseProxyHeader = async ( - ctx: AppContext, - req: express.Request, -): Promise<{ did: string; serviceUrl: string } | undefined> => { - const proxyTo = req.header('atproto-proxy') - if (!proxyTo) return - const [did, serviceId] = proxyTo.split('#') - if (!serviceId) { - throw new InvalidRequestError('no service id specified') - } - const didDoc = await ctx.idResolver.did.resolve(did) - if (!didDoc) { - throw new InvalidRequestError('could not resolve proxy did') - } - const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) - if (!serviceUrl) { - throw new InvalidRequestError('could not resolve proxy did service url') - } - return { did, serviceUrl } -} - -export const constructUrl = ( - serviceUrl: string, - nsid: string, - params?: Record, -): string => { - const uri = new URL(serviceUrl) - uri.pathname = `/xrpc/${nsid}` - - for (const [key, value] of Object.entries(params ?? {})) { - if (value === undefined) { - continue - } else if (Array.isArray(value)) { - for (const item of value) { - uri.searchParams.append(key, String(item)) - } - } else { - uri.searchParams.set(key, String(value)) - } - } - - return uri.toString() -} - const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false From f7ef546f7b9f51d4af4e2c3ef58eb50da8f80214 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 16:05:53 -0600 Subject: [PATCH 14/34] update did doc id values --- packages/bsky/src/auth-verifier.ts | 13 ++++++++----- packages/ozone/src/api/well-known.ts | 6 +++--- packages/pds/src/auth-verifier.ts | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 4c5311d73fe..03766c55151 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -170,7 +170,7 @@ export class AuthVerifier { modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid, `${this.modServiceDid}#atproto_mod`], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto_labeler`], }) return { credentials: { type: 'mod_service', aud, iss } } } @@ -209,7 +209,9 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, keyId = 'atproto'] = iss.split('#') + const [did, serviceId] = iss.split('#') + const keyId = + serviceId === 'atproto-labeler' ? 'atproto-label' : 'atproto' let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -236,9 +238,10 @@ export class AuthVerifier { } isModService(iss: string): boolean { - return [this.modServiceDid, `${this.modServiceDid}#atproto_mod`].includes( - iss, - ) + return [ + this.modServiceDid, + `${this.modServiceDid}#atproto_labeler`, + ].includes(iss) } nullCreds(): NullOutput { diff --git a/packages/ozone/src/api/well-known.ts b/packages/ozone/src/api/well-known.ts index 29a9f6d3636..17ee9ad3752 100644 --- a/packages/ozone/src/api/well-known.ts +++ b/packages/ozone/src/api/well-known.ts @@ -15,7 +15,7 @@ export const createRouter = (ctx: AppContext): express.Router => { id: ctx.cfg.service.did, verificationMethod: [ { - id: `${ctx.cfg.service.did}#atproto_mod`, + id: `${ctx.cfg.service.did}#atproto_label`, type: 'Multikey', controller: ctx.cfg.service.did, publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''), @@ -23,8 +23,8 @@ export const createRouter = (ctx: AppContext): express.Router => { ], service: [ { - id: '#atproto_mod_srvc', - type: 'AtprotoModerationService', + id: '#atproto_labeler', + type: 'AtprotoLabeler', serviceEndpoint: `https://${hostname}`, }, ], diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 5f1b11fd29c..6625ea9629c 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -260,7 +260,7 @@ export class AuthVerifier { } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.modService, `${this.dids.modService}#atproto_mod`], + iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`], }) return { credentials: { @@ -344,7 +344,9 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, keyId = 'atproto'] = iss.split('#') + const [did, serviceId] = iss.split('#') + const keyId = + serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto' const didDoc = await this.idResolver.did.resolve(did, forceRefresh) if (!didDoc) { throw new AuthRequiredError('could not resolve iss did') From 0482a92fe46f301560d898977509694deef52e3a Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 16:13:34 -0600 Subject: [PATCH 15/34] null creds string -> `none` --- packages/bsky/src/auth-verifier.ts | 4 ++-- packages/ozone/src/auth-verifier.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 03766c55151..b305aad1ef1 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -25,7 +25,7 @@ export enum RoleStatus { type NullOutput = { credentials: { - type: 'null' + type: 'none' iss: null } } @@ -247,7 +247,7 @@ export class AuthVerifier { nullCreds(): NullOutput { return { credentials: { - type: 'null', + type: 'none', iss: null, }, } diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index 8ea3c58c59c..48ca241e6ef 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -40,7 +40,7 @@ type StandardOutput = { type NullOutput = { credentials: { - type: 'null' + type: 'none' iss: null } } @@ -175,7 +175,7 @@ export class AuthVerifier { nullCreds(): NullOutput { return { credentials: { - type: 'null', + type: 'none', iss: null, }, } From 5df31de214d873d0fb40cd7fb42d130a8b046e04 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 18:49:03 -0600 Subject: [PATCH 16/34] fix fetchLabels auth check --- packages/ozone/src/api/temp/fetchLabels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index 890fafdf95c..b2fbfbb846a 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { const since = params.since !== undefined ? new Date(params.since).toISOString() : '' const includeUnspeccedTakedowns = - auth.credentials.type === 'null' ? false : auth.credentials.isAdmin + auth.credentials.type === 'none' ? false : auth.credentials.isAdmin const labelRes = await ctx.db.db .selectFrom('label') .selectAll() From dd891d4608a0fa28144ffcc786339365af40f490 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 5 Mar 2024 18:01:05 +0000 Subject: [PATCH 17/34] :sparkles: Add a couple more proxied requests that we use in ozone ui --- packages/ozone/src/api/proxied.ts | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts index b7eef005650..a2d5040989e 100644 --- a/packages/ozone/src/api/proxied.ts +++ b/packages/ozone/src/api/proxied.ts @@ -16,6 +16,20 @@ export default function (server: Server, ctx: AppContext) { }, }) + server.app.bsky.actor.getProfiles({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + server.app.bsky.feed.getAuthorFeed({ auth: ctx.authVerifier.modOrRole, handler: async (request) => { @@ -44,6 +58,20 @@ export default function (server: Server, ctx: AppContext) { }, }) + server.app.bsky.feed.getFeedGenerator({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + server.app.bsky.graph.getFollows({ auth: ctx.authVerifier.modOrRole, handler: async (request) => { @@ -71,4 +99,18 @@ export default function (server: Server, ctx: AppContext) { } }, }) + + server.app.bsky.graph.getList({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) } From 2ca4feebba7fb36ab95fb6f0701be896a62d153f Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:16:50 -0800 Subject: [PATCH 18/34] Add runit to the services/bsky Dockerfile (#2254) add runit to the services/bsky Dockerfile --- services/bsky/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/bsky/Dockerfile b/services/bsky/Dockerfile index 84422945ae0..a15e6aa2ee7 100644 --- a/services/bsky/Dockerfile +++ b/services/bsky/Dockerfile @@ -35,7 +35,10 @@ WORKDIR services/bsky # Uses assets from build stage to reduce build size FROM node:20.11-alpine -RUN apk add --update dumb-init +# dumb-init is used to handle signals properly. +# runit is installed so it can be (optionally) used for logging via svlogd. +RUN apk add --update dumb-init runit + # Avoid zombie processes, handle signal forwarding ENTRYPOINT ["dumb-init", "--"] From 6ba5f6c67fb7fc8a8fd5b3fc4f1fbc84638f21b3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 1 Mar 2024 12:44:52 -0600 Subject: [PATCH 19/34] Improve tag detection (#2260) * Allow tags to lead with and contain only numbers * Break tags on other whitespace characters * Export regexes from rich text detection * Add test * Add test * Disallow number-only tags * Avoid combining enclosing screen chars * Allow full-width number sign * Clarify tests * Fix punctuation edge case * Reorder * Simplify, add another test * Another test, comment --- .changeset/chatty-cows-kick.md | 5 +++ .changeset/lovely-pandas-pretend.md | 5 +++ .changeset/quick-ducks-joke.md | 5 +++ packages/api/src/index.ts | 1 + packages/api/src/rich-text/detection.ts | 18 ++++++++--- packages/api/src/rich-text/util.ts | 11 +++++++ .../api/tests/rich-text-detection.test.ts | 31 +++++++++++++++++-- 7 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 .changeset/chatty-cows-kick.md create mode 100644 .changeset/lovely-pandas-pretend.md create mode 100644 .changeset/quick-ducks-joke.md create mode 100644 packages/api/src/rich-text/util.ts diff --git a/.changeset/chatty-cows-kick.md b/.changeset/chatty-cows-kick.md new file mode 100644 index 00000000000..76bd82f015d --- /dev/null +++ b/.changeset/chatty-cows-kick.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Export regex from rich text detection diff --git a/.changeset/lovely-pandas-pretend.md b/.changeset/lovely-pandas-pretend.md new file mode 100644 index 00000000000..3a75be2877b --- /dev/null +++ b/.changeset/lovely-pandas-pretend.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Disallow rare unicode whitespace characters from tags diff --git a/.changeset/quick-ducks-joke.md b/.changeset/quick-ducks-joke.md new file mode 100644 index 00000000000..923b2fe2fe0 --- /dev/null +++ b/.changeset/quick-ducks-joke.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Allow tags to lead with numbers diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 9e407142aba..87cf1ccf01a 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -14,6 +14,7 @@ export * from './agent' export * from './rich-text/rich-text' export * from './rich-text/sanitization' export * from './rich-text/unicode' +export * from './rich-text/util' export * from './moderation' export * from './moderation/types' export { LABELS } from './moderation/const/labels' diff --git a/packages/api/src/rich-text/detection.ts b/packages/api/src/rich-text/detection.ts index 7b5444a68a5..22c5db1b087 100644 --- a/packages/api/src/rich-text/detection.ts +++ b/packages/api/src/rich-text/detection.ts @@ -1,6 +1,12 @@ import TLDs from 'tlds' import { AppBskyRichtextFacet } from '../client' import { UnicodeString } from './unicode' +import { + URL_REGEX, + MENTION_REGEX, + TAG_REGEX, + TRAILING_PUNCTUATION_REGEX, +} from './util' export type Facet = AppBskyRichtextFacet.Main @@ -9,7 +15,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { const facets: Facet[] = [] { // mentions - const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g + const re = MENTION_REGEX while ((match = re.exec(text.utf16))) { if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) { continue // probably not a handle @@ -33,8 +39,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { } { // links - const re = - /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim + const re = URL_REGEX while ((match = re.exec(text.utf16))) { let uri = match[2] if (!uri.startsWith('http')) { @@ -70,11 +75,14 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { } } { - const re = /(^|\s)#((?!\ufe0f)[^\d\s]\S*)(?=\s)?/g + const re = TAG_REGEX while ((match = re.exec(text.utf16))) { let [, leading, tag] = match - tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation + if (!tag) continue + + // strip ending punctuation and any spaces + tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '') if (tag.length === 0 || tag.length > 64) continue diff --git a/packages/api/src/rich-text/util.ts b/packages/api/src/rich-text/util.ts new file mode 100644 index 00000000000..ab50c66212d --- /dev/null +++ b/packages/api/src/rich-text/util.ts @@ -0,0 +1,11 @@ +export const MENTION_REGEX = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g +export const URL_REGEX = + /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim +export const TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu + +/** + * `\ufe0f` emoji modifier + * `\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` zero-width spaces (likely incomplete) + */ +export const TAG_REGEX = + /(^|\s)[##]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*)?/gu diff --git a/packages/api/tests/rich-text-detection.test.ts b/packages/api/tests/rich-text-detection.test.ts index 0eafb65a3b1..084b5440a48 100644 --- a/packages/api/tests/rich-text-detection.test.ts +++ b/packages/api/tests/rich-text-detection.test.ts @@ -218,7 +218,7 @@ describe('detectFacets', () => { } }) - it('correctly detects tags inline', async () => { + describe('correctly detects tags inline', () => { const inputs: [ string, string[], @@ -234,11 +234,13 @@ describe('detectFacets', () => { ], ], ['#1', [], []], + ['#1a', ['1a'], [{ byteStart: 0, byteEnd: 3 }]], ['#tag', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], ['body #tag', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], ['#tag body', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], ['body #tag body', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], ['body #1', [], []], + ['body #1a', ['1a'], [{ byteStart: 5, byteEnd: 8 }]], ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]], ['#', [], []], ['#?', [], []], @@ -254,12 +256,18 @@ describe('detectFacets', () => { [], [], ], + [ + 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!', + ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], + [{ byteStart: 5, byteEnd: 70 }], + ], [ 'its a #double#rainbow', ['double#rainbow'], [{ byteStart: 6, byteEnd: 21 }], ], ['##hashash', ['#hashash'], [{ byteStart: 0, byteEnd: 9 }]], + ['##', [], []], ['some #n0n3s@n5e!', ['n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]], [ 'works #with,punctuation', @@ -319,9 +327,26 @@ describe('detectFacets', () => { }, ], ], + ['no match (\\u200B): #​', [], []], + ['no match (\\u200Ba): #​a', [], []], + ['match (a\\u200Bb): #a​b', ['a'], [{ byteStart: 18, byteEnd: 20 }]], + ['match (ab\\u200B): #ab​', ['ab'], [{ byteStart: 18, byteEnd: 21 }]], + ['no match (\\u20e2tag): #⃢tag', [], []], + ['no match (a\\u20e2b): #a⃢b', ['a'], [{ byteStart: 21, byteEnd: 23 }]], + [ + 'match full width number sign (tag): #tag', + ['tag'], + [{ byteStart: 36, byteEnd: 42 }], + ], + [ + 'match full width number sign (tag): ##️⃣tag', + ['#️⃣tag'], + [{ byteStart: 36, byteEnd: 49 }], + ], + ['no match 1?: #1?', [], []], ] - for (const [input, tags, indices] of inputs) { + it.each(inputs)('%s', async (input, tags, indices) => { const rt = new RichText({ text: input }) await rt.detectFacets(agent) @@ -340,7 +365,7 @@ describe('detectFacets', () => { expect(detectedTags).toEqual(tags) expect(detectedIndices).toEqual(indices) - } + }) }) }) From 9b2500e895f1109f42efcf7d456f4cf606e50901 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:03:04 -0600 Subject: [PATCH 20/34] Version packages (#2261) Co-authored-by: github-actions[bot] --- .changeset/chatty-cows-kick.md | 5 ----- .changeset/lovely-pandas-pretend.md | 5 ----- .changeset/quick-ducks-joke.md | 5 ----- packages/api/CHANGELOG.md | 10 ++++++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 10 ++++++++++ packages/dev-env/package.json | 2 +- packages/ozone/CHANGELOG.md | 7 +++++++ packages/ozone/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 13 files changed, 46 insertions(+), 20 deletions(-) delete mode 100644 .changeset/chatty-cows-kick.md delete mode 100644 .changeset/lovely-pandas-pretend.md delete mode 100644 .changeset/quick-ducks-joke.md diff --git a/.changeset/chatty-cows-kick.md b/.changeset/chatty-cows-kick.md deleted file mode 100644 index 76bd82f015d..00000000000 --- a/.changeset/chatty-cows-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Export regex from rich text detection diff --git a/.changeset/lovely-pandas-pretend.md b/.changeset/lovely-pandas-pretend.md deleted file mode 100644 index 3a75be2877b..00000000000 --- a/.changeset/lovely-pandas-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Disallow rare unicode whitespace characters from tags diff --git a/.changeset/quick-ducks-joke.md b/.changeset/quick-ducks-joke.md deleted file mode 100644 index 923b2fe2fe0..00000000000 --- a/.changeset/quick-ducks-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Allow tags to lead with numbers diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 48e4edc018b..a5bac9a4eaa 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/api +## 0.10.4 + +### Patch Changes + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Export regex from rich text detection + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Disallow rare unicode whitespace characters from tags + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Allow tags to lead with numbers + ## 0.10.3 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 4f07d5a01a1..dbb18da1786 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.10.3", + "version": "0.10.4", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 27155388ef9..102c40050eb 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.36 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.0.35 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 8ffa5e0911f..dd081d3c209 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.35", + "version": "0.0.36", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 814f89dd619..15a6ad3171f 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/dev-env +## 0.2.36 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + - @atproto/bsky@0.0.36 + - @atproto/ozone@0.0.15 + - @atproto/pds@0.4.4 + ## 0.2.35 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 5924c18bfd0..46899cdb763 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.35", + "version": "0.2.36", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/ozone/CHANGELOG.md b/packages/ozone/CHANGELOG.md index 7f27390ce91..0953945b4b2 100644 --- a/packages/ozone/CHANGELOG.md +++ b/packages/ozone/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/ozone +## 0.0.15 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.0.14 ### Patch Changes diff --git a/packages/ozone/package.json b/packages/ozone/package.json index 3a3d3a690ff..3840f4b49ed 100644 --- a/packages/ozone/package.json +++ b/packages/ozone/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/ozone", - "version": "0.0.14", + "version": "0.0.15", "license": "MIT", "description": "Backend service for moderating the Bluesky network.", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 3a323988102..1a3e87286f3 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.4.4 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.4.3 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index f345bbcbf63..062578aa531 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.4.3", + "version": "0.4.4", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From c76fd03caa1eed36d9d26feba1271521c51629a4 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 4 Mar 2024 17:31:50 +0100 Subject: [PATCH 21/34] :bug: Increment attempt count after each attempt to push ozone event (#2239) --- packages/ozone/src/daemon/event-pusher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ozone/src/daemon/event-pusher.ts b/packages/ozone/src/daemon/event-pusher.ts index faaee4529ed..01570595c0d 100644 --- a/packages/ozone/src/daemon/event-pusher.ts +++ b/packages/ozone/src/daemon/event-pusher.ts @@ -205,7 +205,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectDid', '=', evt.subjectDid) @@ -244,7 +244,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectUri', '=', evt.subjectUri) @@ -284,7 +284,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectDid', '=', evt.subjectDid) From 87f00f21938655322be58ab66a838dd9edc22bc6 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 5 Mar 2024 14:27:25 -0500 Subject: [PATCH 22/34] Ozone delegates email sending to actor's pds (#2272) * ozone delegates email sending to user's pds * lexicon: add content field to mod email event * test email sending via mod event --- lexicons/com/atproto/admin/defs.json | 4 ++ packages/api/src/client/lexicons.ts | 4 ++ .../client/types/com/atproto/admin/defs.ts | 2 + packages/bsky/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + packages/dev-env/src/ozone.ts | 1 + .../src/api/admin/emitModerationEvent.ts | 19 +++++ packages/ozone/src/config/config.ts | 2 + packages/ozone/src/config/env.ts | 4 +- packages/ozone/src/context.ts | 14 ++-- packages/ozone/src/daemon/context.ts | 14 ++-- packages/ozone/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + packages/ozone/src/mod-service/index.ts | 71 +++++++++++++++++-- packages/ozone/src/mod-service/views.ts | 16 ++--- .../ozone/tests/moderation-events.test.ts | 49 +++++++++++++ .../src/api/com/atproto/admin/sendEmail.ts | 44 ++++-------- packages/pds/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + 19 files changed, 205 insertions(+), 57 deletions(-) diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index e1315eb7473..d2056f77cf8 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -585,6 +585,10 @@ "type": "string", "description": "The subject line of the email sent to the user." }, + "content": { + "type": "string", + "description": "The content of the email sent to the user." + }, "comment": { "type": "string", "description": "Additional comment about the outgoing comm." diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/api/src/client/types/com/atproto/admin/defs.ts b/packages/api/src/client/types/com/atproto/admin/defs.ts index 4e3d35a869f..2de71524005 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 0e0578c7935..278ad8d2b2b 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -34,6 +34,7 @@ export class TestOzone { const port = config.port || (await getPort()) const url = `http://localhost:${port}` const env: ozone.OzoneEnvironment = { + devMode: true, version: '0.0.0', port, didPlcUrl: config.plcUrl, diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index 473269bffde..f1bdbab3462 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -2,12 +2,14 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../lexicon' import AppContext from '../../context' import { + isModEventEmail, isModEventLabel, isModEventReverseTakedown, isModEventTakedown, } from '../../lexicon/types/com/atproto/admin/defs' import { subjectFromInput } from '../../mod-service/subject' import { ModerationLangService } from '../../mod-service/lang' +import { retryHttp } from '../../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ @@ -75,6 +77,23 @@ export default function (server: Server, ctx: AppContext) { } } + if (isModEventEmail(event) && event.content) { + // sending email prior to logging the event to avoid a long transaction below + if (!subject.isRepo()) { + throw new InvalidRequestError( + 'Email can only be sent to a repo subject', + ) + } + const { content, subjectLine } = event + await retryHttp(() => + ctx.modService(db).sendEmail({ + subject: subjectLine, + content, + recipientDid: subject.did, + }), + ) + } + const moderationEvent = await db.transaction(async (dbTxn) => { const moderationTxn = ctx.modService(dbTxn) diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 54abb29e21e..3a3cc62f663 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -13,6 +13,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { publicUrl: env.publicUrl, did: env.serverDid, version: env.version, + devMode: env.devMode, } assert(env.dbPostgresUrl) @@ -79,6 +80,7 @@ export type ServiceConfig = { publicUrl: string did: string version?: string + devMode?: boolean } export type DatabaseConfig = { diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index e86b520815d..554a0f38ba9 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -1,8 +1,9 @@ -import { envInt, envList, envStr } from '@atproto/common' +import { envBool, envInt, envList, envStr } from '@atproto/common' export const readEnv = (): OzoneEnvironment => { return { nodeEnv: envStr('NODE_ENV'), + devMode: envBool('OZONE_DEV_MODE'), version: envStr('OZONE_VERSION'), port: envInt('OZONE_PORT'), publicUrl: envStr('OZONE_PUBLIC_URL'), @@ -30,6 +31,7 @@ export const readEnv = (): OzoneEnvironment => { export type OzoneEnvironment = { nodeEnv?: string + devMode?: boolean version?: string port?: number publicUrl?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index e9332620993..db8d123d5e0 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -59,8 +59,6 @@ export class AppContext { aud, keypair: signingKey, }) - const appviewAuth = async () => - cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined const backgroundQueue = new BackgroundQueue(db) const eventPusher = new EventPusher(db, createAuthHeaders, { @@ -68,11 +66,17 @@ export class AppContext { pds: cfg.pds ?? undefined, }) + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + }) + const modService = ModerationService.creator( + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, cfg.service.did, overrides?.imgInvalidator, cfg.cdn.paths, @@ -80,10 +84,6 @@ export class AppContext { const communicationTemplateService = CommunicationTemplateService.creator() - const idResolver = new IdResolver({ - plcUrl: cfg.identity.plcUrl, - }) - const sequencer = new Sequencer(db) const authVerifier = new AuthVerifier(idResolver, { diff --git a/packages/ozone/src/daemon/context.ts b/packages/ozone/src/daemon/context.ts index 5af19d89bc4..3ed0596c2ed 100644 --- a/packages/ozone/src/daemon/context.ts +++ b/packages/ozone/src/daemon/context.ts @@ -7,6 +7,7 @@ import { EventPusher } from './event-pusher' import { EventReverser } from './event-reverser' import { ModerationService, ModerationServiceCreator } from '../mod-service' import { BackgroundQueue } from '../background' +import { IdResolver } from '@atproto/identity' export type DaemonContextOptions = { db: Database @@ -39,21 +40,26 @@ export class DaemonContext { keypair: signingKey, }) - const appviewAuth = async () => - cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined - const eventPusher = new EventPusher(db, createAuthHeaders, { appview: cfg.appview, pds: cfg.pds ?? undefined, }) + const backgroundQueue = new BackgroundQueue(db) + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + }) + const modService = ModerationService.creator( + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, cfg.service.did, ) + const eventReverser = new EventReverser(db, modService) return new DaemonContext({ diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index d0690fce57a..1ca793d5601 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -1,9 +1,13 @@ +import net from 'node:net' +import { Insertable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri, INVALID_HANDLE } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { addHoursToDate } from '@atproto/common' +import { IdResolver } from '@atproto/identity' +import AtpAgent from '@atproto/api' import { Database } from '../db' -import { AppviewAuth, ModerationViews } from './views' +import { AuthHeaders, ModerationViews } from './views' import { Main as StrongRef } from '../lexicon/types/com/atproto/repo/strongRef' import { isModEventComment, @@ -30,9 +34,7 @@ import { } from './types' import { ModerationEvent } from '../db/schema/moderation_event' import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination' -import AtpAgent from '@atproto/api' import { Label } from '../lexicon/types/com/atproto/label/defs' -import { Insertable, sql } from 'kysely' import { ModSubject, RecordSubject, @@ -46,26 +48,31 @@ import { BackgroundQueue } from '../background' import { EventPusher } from '../daemon' import { ImageInvalidator } from '../image-invalidator' import { httpLogger as log } from '../logger' +import { OzoneConfig } from '../config' export type ModerationServiceCreator = (db: Database) => ModerationService export class ModerationService { constructor( public db: Database, + public cfg: OzoneConfig, public backgroundQueue: BackgroundQueue, + public idResolver: IdResolver, public eventPusher: EventPusher, public appviewAgent: AtpAgent, - private appviewAuth: AppviewAuth, + private createAuthHeaders: (aud: string) => Promise, public serverDid: string, public imgInvalidator?: ImageInvalidator, public cdnPaths?: string[], ) {} static creator( + cfg: OzoneConfig, backgroundQueue: BackgroundQueue, + idResolver: IdResolver, eventPusher: EventPusher, appviewAgent: AtpAgent, - appviewAuth: AppviewAuth, + createAuthHeaders: (aud: string) => Promise, serverDid: string, imgInvalidator?: ImageInvalidator, cdnPaths?: string[], @@ -73,17 +80,21 @@ export class ModerationService { return (db: Database) => new ModerationService( db, + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, serverDid, imgInvalidator, cdnPaths, ) } - views = new ModerationViews(this.db, this.appviewAgent, this.appviewAuth) + views = new ModerationViews(this.db, this.appviewAgent, () => + this.createAuthHeaders(this.cfg.appview.did), + ) async getEvent(id: number): Promise { return await this.db.db @@ -291,6 +302,9 @@ export class ModerationService { if (isModEventEmail(event)) { meta.subjectLine = event.subjectLine + if (event.content) { + meta.content = event.content + } } const subjectInfo = subject.info() @@ -903,6 +917,49 @@ export class ModerationService { ) .execute() } + + async sendEmail(opts: { + content: string + recipientDid: string + subject: string + }) { + const { subject, content, recipientDid } = opts + const { pds } = await this.idResolver.did.resolveAtprotoData(recipientDid) + const url = new URL(pds) + if (!this.cfg.service.devMode && !isSafeUrl(url)) { + throw new InvalidRequestError('Invalid pds service in DID doc') + } + const agent = new AtpAgent({ service: url }) + const { data: serverInfo } = + await agent.api.com.atproto.server.describeServer() + if (serverInfo.did !== `did:web:${url.hostname}`) { + // @TODO do bidirectional check once implemented. in the meantime, + // matching did to hostname we're talking to is pretty good. + throw new InvalidRequestError('Invalid pds service in DID doc') + } + const { data: delivery } = await agent.api.com.atproto.admin.sendEmail( + { + subject, + content, + recipientDid, + senderDid: this.cfg.service.did, + }, + { + encoding: 'application/json', + ...(await this.createAuthHeaders(serverInfo.did)), + }, + ) + if (!delivery.sent) { + throw new InvalidRequestError('Email was accepted but not sent') + } + } +} + +const isSafeUrl = (url: URL) => { + if (url.protocol !== 'https:') return false + if (!url.hostname || url.hostname === 'localhost') return false + if (net.isIP(url.hostname) === 0) return false + return true } const TAKEDOWNS = ['pds_takedown' as const, 'appview_takedown' as const] diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index f1188968cbe..498091a8bd0 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -26,20 +26,17 @@ import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' import { subjectFromEventRow, subjectFromStatusRow } from './subject' import { formatLabel } from './util' -export type AppviewAuth = () => Promise< - | { - headers: { - authorization: string - } - } - | undefined -> +export type AuthHeaders = { + headers: { + authorization: string + } +} export class ModerationViews { constructor( private db: Database, private appviewAgent: AtpAgent, - private appviewAuth: AppviewAuth, + private appviewAuth: () => Promise, ) {} async getAccoutInfosByDid(dids: string[]): Promise> { @@ -154,6 +151,7 @@ export class ModerationViews { eventView.event = { ...eventView.event, subjectLine: event.meta?.subjectLine ?? '', + content: event.meta?.content, } } diff --git a/packages/ozone/tests/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts index fbe571a8172..1212a6049e6 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -1,4 +1,6 @@ import assert from 'node:assert' +import EventEmitter, { once } from 'node:events' +import Mail from 'nodemailer/lib/mailer' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import AtpAgent, { ComAtprotoAdminDefs, @@ -422,4 +424,51 @@ describe('moderation-events', () => { }) }) }) + + describe('email event', () => { + let sendMailOriginal + const mailCatcher = new EventEmitter() + const getMailFrom = async (promise): Promise => { + const result = await Promise.all([once(mailCatcher, 'mail'), promise]) + return result[0][0] + } + + beforeAll(() => { + const mailer = network.pds.ctx.moderationMailer + // Catch emails for use in tests + sendMailOriginal = mailer.transporter.sendMail + mailer.transporter.sendMail = async (opts) => { + const result = await sendMailOriginal.call(mailer.transporter, opts) + mailCatcher.emit('mail', opts) + return result + } + }) + + afterAll(() => { + network.pds.ctx.moderationMailer.transporter.sendMail = sendMailOriginal + }) + + it('sends email via pds.', async () => { + const mail = await getMailFrom( + emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventEmail', + comment: 'Reaching out to Alice', + subjectLine: 'Hello', + content: 'Hey Alice, how are you?', + }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.alice, + }, + createdBy: sc.dids.bob, + }), + ) + expect(mail).toEqual({ + to: 'alice@test.com', + subject: 'Hello', + html: 'Hey Alice, how are you?', + }) + }) + }) }) diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index e23d6bea5c1..f6d8cce8d19 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -1,23 +1,23 @@ +import assert from 'node:assert' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru, resultPassthru } from '../../../proxy' +import { resultPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.role, - handler: async ({ req, input, auth }) => { - if (!auth.credentials.admin && !auth.credentials.moderator) { + auth: ctx.authVerifier.roleOrAdminService, + handler: async ({ input, auth }) => { + if (auth.credentials.type === 'role' && !auth.credentials.moderator) { throw new AuthRequiredError('Insufficient privileges') } const { content, recipientDid, - senderDid, - subject = 'Message from Bluesky moderator', - comment, + subject = 'Message via your PDS', } = input.body + const account = await ctx.accountManager.getAccount(recipientDid, { includeDeactivated: true, includeTakenDown: true, @@ -27,11 +27,15 @@ export default function (server: Server, ctx: AppContext) { } if (ctx.entrywayAgent) { + assert(ctx.cfg.entryway) return resultPassthru( - await ctx.entrywayAgent.com.atproto.admin.sendEmail( - input.body, - authPassthru(req, true), - ), + await ctx.entrywayAgent.com.atproto.admin.sendEmail(input.body, { + encoding: 'application/json', + ...(await ctx.serviceAuthHeaders( + recipientDid, + ctx.cfg.entryway?.did, + )), + }), ) } @@ -44,24 +48,6 @@ export default function (server: Server, ctx: AppContext) { { subject, to: account.email }, ) - 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, - }, - { ...authPassthru(req), encoding: 'application/json' }, - ) - } - return { encoding: 'application/json', body: { sent: true }, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown From 8341c7a10b53c5d3fa477199152486e62a37e241 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:34:49 -0600 Subject: [PATCH 23/34] fix auth verifier method --- packages/pds/src/api/com/atproto/admin/sendEmail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index f6d8cce8d19..6e30159c204 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -6,7 +6,7 @@ import { resultPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ input, auth }) => { if (auth.credentials.type === 'role' && !auth.credentials.moderator) { throw new AuthRequiredError('Insufficient privileges') From f9361057b3892b7f504a5ec0fb4db1086c450004 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:36:14 -0600 Subject: [PATCH 24/34] build branch --- .github/workflows/build-and-push-bsky-ghcr.yaml | 2 +- .github/workflows/build-and-push-ozone-aws.yaml | 2 +- .github/workflows/build-and-push-pds-ghcr.yaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index f1bf0bd10f5..cfa76dc5a46 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - appview-v2 + - pds-proxy-headers env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.github/workflows/build-and-push-ozone-aws.yaml b/.github/workflows/build-and-push-ozone-aws.yaml index b934d192b6f..34c461a0b42 100644 --- a/.github/workflows/build-and-push-ozone-aws.yaml +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - ozone-cdn-invalidation + - pds-proxy-headers env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-and-push-pds-ghcr.yaml b/.github/workflows/build-and-push-pds-ghcr.yaml index b11230ab531..988e6b02f84 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - pds-proxy-headers env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} From 5f3c91b63b79255188e7e3eab173d0586a736bfc Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:58:18 -0600 Subject: [PATCH 25/34] fix url check --- packages/pds/src/pipethrough.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 595584b9388..a33e719eda8 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -154,7 +154,7 @@ export const doProxy = async (url: URL, reqInit: RequestInit) => { const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false - if (net.isIP(url.hostname) === 0) return false + if (net.isIP(url.hostname) !== 0) return false return true } From 037f163cdb9dffb418a8363b3c7d3551fd2eebf4 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:32:30 -0600 Subject: [PATCH 26/34] better error handling for get account infos --- packages/ozone/src/mod-service/views.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index 498091a8bd0..f65fd68b480 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -25,6 +25,7 @@ import { import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' import { subjectFromEventRow, subjectFromStatusRow } from './subject' import { formatLabel } from './util' +import { httpLogger as log } from '../logger' export type AuthHeaders = { headers: { @@ -43,15 +44,20 @@ export class ModerationViews { if (dids.length === 0) return new Map() const auth = await this.appviewAuth() if (!auth) return new Map() - const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos( - { - dids: dedupeStrs(dids), - }, - auth, - ) - return res.data.infos.reduce((acc, cur) => { - return acc.set(cur.did, cur) - }, new Map()) + try { + const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos( + { + dids: dedupeStrs(dids), + }, + auth, + ) + return res.data.infos.reduce((acc, cur) => { + return acc.set(cur.did, cur) + }, new Map()) + } catch (err) { + log.error({ err, dids }, 'failed to resolve account infos from appview') + return new Map() + } } async repos(dids: string[]): Promise> { From fc1c40dff8bb14d8a6cfbba7a0b18a8efaa76057 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:33:12 -0600 Subject: [PATCH 27/34] fix labeler service id --- packages/bsky/src/auth-verifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index b305aad1ef1..2936b1cd28b 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -211,7 +211,7 @@ export class AuthVerifier { } const [did, serviceId] = iss.split('#') const keyId = - serviceId === 'atproto-labeler' ? 'atproto-label' : 'atproto' + serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto' let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) From 5e1c5fd7d3d7acebbf5c03d9a765f6f45082469c Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:50:18 -0600 Subject: [PATCH 28/34] fix iss on auth headers --- packages/ozone/src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index db8d123d5e0..e7168881bac 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -55,7 +55,7 @@ export class AppContext { const createAuthHeaders = (aud: string) => createServiceAuthHeaders({ - iss: cfg.service.did, + iss: `${cfg.service.did}#atproto_labeler`, aud, keypair: signingKey, }) From 82acea2356fc21a7cb180d9813b6bf7014a92448 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:58:58 -0600 Subject: [PATCH 29/34] fix dev-env ozone did --- packages/dev-env/src/ozone.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 278ad8d2b2b..27e240b0518 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -3,7 +3,7 @@ import * as ui8 from 'uint8arrays' import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' -import { Client as PlcClient } from '@did-plc/lib' +import * as plc from '@did-plc/lib' import { OzoneConfig } from './types' import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' @@ -21,18 +21,32 @@ export class TestOzone { const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex') let serverDid = config.serverDid if (!serverDid) { - const plcClient = new PlcClient(config.plcUrl) - serverDid = await plcClient.createDid({ - signingKey: serviceKeypair.did(), - rotationKeys: [serviceKeypair.did()], - handle: 'ozone.test', - pds: `https://pds.invalid`, - signer: serviceKeypair, - }) + const plcClient = new plc.Client(config.plcUrl) + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [], + rotationKeys: [serviceKeypair.did()], + verificationMethods: { + atproto_label: serviceKeypair.did(), + }, + services: { + atproto_labeler: { + type: 'AtprotoLabeler', + endpoint: 'https://ozone.public.url', + }, + }, + prev: null, + }, + serviceKeypair, + ) + serverDid = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(serverDid, plcOp) } const port = config.port || (await getPort()) const url = `http://localhost:${port}` + const env: ozone.OzoneEnvironment = { devMode: true, version: '0.0.0', From 4c7db5cc644a91f092cccbc95d895e9080100106 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 16:15:10 -0600 Subject: [PATCH 30/34] fix tests & another jwt issuer --- packages/dev-env/src/network.ts | 11 ++----- packages/dev-env/src/ozone.ts | 52 +++++++++++++++++++-------------- packages/ozone/src/context.ts | 2 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index c90e2c181f5..06fbd780060 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -3,12 +3,11 @@ import * as uint8arrays from 'uint8arrays' import getPort from 'get-port' import { wait } from '@atproto/common-web' import { createServiceJwt } from '@atproto/xrpc-server' -import { Client as PlcClient } from '@did-plc/lib' import { TestServerParams } from './types' import { TestPlc } from './plc' import { TestPds } from './pds' import { TestBsky } from './bsky' -import { TestOzone } from './ozone' +import { TestOzone, createOzoneDid } from './ozone' import { mockNetworkUtilities } from './util' import { TestNetworkNoAppView } from './network-no-appview' import { Secp256k1Keypair } from '@atproto/crypto' @@ -43,13 +42,7 @@ export class TestNetwork extends TestNetworkNoAppView { const ozonePort = params.ozone?.port ?? (await getPort()) const ozoneKey = await Secp256k1Keypair.create({ exportable: true }) - const ozoneDid = await new PlcClient(plc.url).createDid({ - signingKey: ozoneKey.did(), - rotationKeys: [ozoneKey.did()], - handle: 'ozone.test', - pds: `http://pds.invalid`, - signer: ozoneKey, - }) + const ozoneDid = await createOzoneDid(plc.url, ozoneKey) const bsky = await TestBsky.create({ port: bskyPort, diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 27e240b0518..d06e45eba13 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -2,7 +2,7 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' import * as plc from '@did-plc/lib' import { OzoneConfig } from './types' import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' @@ -21,27 +21,7 @@ export class TestOzone { const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex') let serverDid = config.serverDid if (!serverDid) { - const plcClient = new plc.Client(config.plcUrl) - const plcOp = await plc.signOperation( - { - type: 'plc_operation', - alsoKnownAs: [], - rotationKeys: [serviceKeypair.did()], - verificationMethods: { - atproto_label: serviceKeypair.did(), - }, - services: { - atproto_labeler: { - type: 'AtprotoLabeler', - endpoint: 'https://ozone.public.url', - }, - }, - prev: null, - }, - serviceKeypair, - ) - serverDid = await plc.didForCreateOp(plcOp) - await plcClient.sendOperation(serverDid, plcOp) + serverDid = await createOzoneDid(config.plcUrl, serviceKeypair) } const port = config.port || (await getPort()) @@ -130,3 +110,31 @@ export class TestOzone { await this.server.destroy() } } + +export const createOzoneDid = async ( + plcUrl: string, + keypair: Keypair, +): Promise => { + const plcClient = new plc.Client(plcUrl) + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [], + rotationKeys: [keypair.did()], + verificationMethods: { + atproto_label: keypair.did(), + }, + services: { + atproto_labeler: { + type: 'AtprotoLabeler', + endpoint: 'https://ozone.public.url', + }, + }, + prev: null, + }, + keypair, + ) + const did = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(did, plcOp) + return did +} diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index e7168881bac..5205e54f848 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -172,7 +172,7 @@ export class AppContext { } async serviceAuthHeaders(aud: string) { - const iss = this.cfg.service.did + const iss = `${this.cfg.service.did}#atproto_labeler` return createServiceAuthHeaders({ iss, aud, From 81f9d693afcd7e354f84c2d3254353bed84dbb43 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 5 Mar 2024 17:55:34 -0500 Subject: [PATCH 31/34] ozone: fix ip check --- packages/ozone/src/mod-service/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index 1ca793d5601..dba57baa7b9 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -958,7 +958,7 @@ export class ModerationService { const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false - if (net.isIP(url.hostname) === 0) return false + if (net.isIP(url.hostname) !== 0) return false return true } From 7be84452c3bd6021a77a81b7b5cff9cf5656afb8 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 17:14:31 -0600 Subject: [PATCH 32/34] fix aud check on pds mod service auth --- packages/pds/src/auth-verifier.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 6625ea9629c..05a61ecdf5b 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -259,9 +259,20 @@ export class AuthVerifier { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } const payload = await this.verifyServiceJwt(reqCtx, { - aud: this.dids.entryway ?? this.dids.pds, + aud: null, iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`], }) + if ( + payload.aud !== this.dids.pds && + (!this.dids.entryway || payload.aud !== this.dids.entryway) + ) { + throw new AuthRequiredError( + 'jwt audience does not match service did', + 'BadJwtAudience', + ) + } + + payload.aud return { credentials: { type: 'mod_service', From 592518c63b0696aa70578c0316b510b1242ea739 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 17:16:55 -0600 Subject: [PATCH 33/34] tidy --- packages/pds/src/auth-verifier.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 05a61ecdf5b..e12e0e8acfe 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -271,8 +271,6 @@ export class AuthVerifier { 'BadJwtAudience', ) } - - payload.aud return { credentials: { type: 'mod_service', From b4211ea1d4402cd1fe1bb37800195e101158afc5 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 6 Mar 2024 18:21:44 -0600 Subject: [PATCH 34/34] fix pipethrough of headers --- packages/pds/src/pipethrough.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index faf4fe36aa3..0d9c00737b5 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -95,6 +95,12 @@ export const parseProxyHeader = async ( return { did, serviceUrl } } +const HEADERS_TO_FORWARD = [ + 'accept-language', + 'content-type', + 'atproto-labelers', +] + export const createUrlAndHeaders = async ( ctx: AppContext, req: express.Request, @@ -115,10 +121,13 @@ export const createUrlAndHeaders = async ( const headers = requester ? (await ctx.serviceAuthHeaders(requester, aud)).headers : {} - // forward accept-language header to upstream services - headers['accept-language'] = req.headers['accept-language'] - headers['content-type'] = req.headers['content-type'] - headers['atproto-labelers'] = req.headers['atproto-labelers'] + // forward select headers to upstream services + for (const header of HEADERS_TO_FORWARD) { + const val = req.headers[header] + if (val) { + headers[header] = val + } + } return { url, headers } }