diff --git a/packages/bsky/src/api/subscription-listener.ts b/packages/bsky/src/api/subscription-listener.ts index b7f2ff419ff..1220659c403 100644 --- a/packages/bsky/src/api/subscription-listener.ts +++ b/packages/bsky/src/api/subscription-listener.ts @@ -1,16 +1,18 @@ import express, { RequestHandler } from 'express' import AppContext from '../context' -import { AxiosInstance } from 'axios' import { httpLogger as log } from '../logger' +import { RevenueCatClient } from '../revenueCat' type AppContextWithRevenueCatClient = AppContext & { - revenueCatClient: AxiosInstance + revenueCatClient: RevenueCatClient } const auth = (ctx: AppContextWithRevenueCatClient): RequestHandler => (req: express.Request, res: express.Response, next: express.NextFunction) => - req.header('Authorization') === ctx.cfg.revenueCatWebhookAuthorization + ctx.revenueCatClient.isWebhookAuthorizationValid( + req.header('Authorization'), + ) ? next() : res .status(403) @@ -25,14 +27,6 @@ type RevenueCatEventBody = { } } -type RevenueCatSubscriberResponse = { - subscriber: { - entitlements: { - [entitlementIdentifier: string]: unknown - } - } -} - const revenueCatWebhookHandler = (ctx: AppContextWithRevenueCatClient): RequestHandler => async (req, res) => { @@ -42,11 +36,8 @@ const revenueCatWebhookHandler = try { const { app_user_id: did } = body.event - const { data } = await revenueCatClient.get( - `/subscribers/${encodeURIComponent(did)}`, - ) + const subscriberRes = await revenueCatClient.getSubscriber(did) - const subscriberRes = data as RevenueCatSubscriberResponse const entitlementIdentifiers = Object.keys( subscriberRes.subscriber.entitlements ?? {}, ) diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index cabee10009e..5344e61d9f8 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -24,7 +24,7 @@ export interface ServerConfigValues { searchUrl?: string suggestionsUrl?: string suggestionsApiKey?: string - revenueCatV1Url?: string + revenueCatV1Url: string revenueCatV1ApiKey?: string revenueCatWebhookAuthorization?: string cdnUrl?: string diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 7ecaf758ca8..444c958f30e 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -17,7 +17,7 @@ import { parseLabelerHeader, } from './util' import { httpLogger as log } from './logger' -import { AxiosInstance } from 'axios' +import { RevenueCatClient } from './revenueCat' export class AppContext { constructor( @@ -26,7 +26,7 @@ export class AppContext { dataplane: DataPlaneClient searchAgent: AtpAgent | undefined suggestionsAgent: AtpAgent | undefined - revenueCatClient: AxiosInstance | undefined + revenueCatClient: RevenueCatClient | undefined hydrator: Hydrator views: Views signingKey: Keypair @@ -54,7 +54,7 @@ export class AppContext { return this.opts.suggestionsAgent } - get revenueCatClient(): AxiosInstance | undefined { + get revenueCatClient(): RevenueCatClient | undefined { return this.opts.revenueCatClient } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 3253d0288d7..b00d40411da 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -31,6 +31,7 @@ import { authWithApiKey as courierAuth, createCourierClient } from './courier' import { FeatureGates } from './feature-gates' import { VideoUriBuilder } from './views/util' import axios from 'axios' +import { RevenueCatClient } from './revenueCat' export * from './data-plane' export type { ServerConfigValues } from './config' @@ -102,14 +103,14 @@ export class BskyAppView { ) } - const revenueCatClient = config.revenueCatV1ApiKey - ? axios.create({ - baseURL: config.revenueCatV1Url, - headers: { - Authorization: `Bearer ${config.revenueCatV1ApiKey}`, - }, - }) - : undefined + let revenueCatClient: RevenueCatClient | undefined + if (config.revenueCatV1ApiKey) { + revenueCatClient = new RevenueCatClient({ + apiKey: config.revenueCatV1ApiKey, + url: config.revenueCatV1Url, + webhookAuthorization: config.revenueCatWebhookAuthorization, + }) + } const dataplane = createDataPlaneClient(config.dataplaneUrls, { httpVersion: config.dataplaneHttpVersion, diff --git a/packages/bsky/src/revenueCat.ts b/packages/bsky/src/revenueCat.ts new file mode 100644 index 00000000000..32f494ac0ef --- /dev/null +++ b/packages/bsky/src/revenueCat.ts @@ -0,0 +1,46 @@ +import axios, { AxiosInstance } from 'axios' + +type Config = { + apiKey: string + url: string + webhookAuthorization: string | undefined +} + +type GetSubscriberResponse = { + subscriber: { + entitlements: { + [entitlementIdentifier: string]: unknown + } + } +} + +export class RevenueCatClient { + private webhookAuthorization: string | undefined + private axios: AxiosInstance + + constructor({ apiKey, url, webhookAuthorization }: Config) { + this.axios = axios.create({ + baseURL: url, + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }) + + this.webhookAuthorization = webhookAuthorization + } + + async getSubscriber(did: string): Promise { + const { data } = await this.axios.get( + `/subscribers/${encodeURIComponent(did)}`, + ) + + return data as GetSubscriberResponse + } + + isWebhookAuthorizationValid(authorization: string | undefined): boolean { + // It is either valid if the authorization is: + // 1. not configured and undefined in the request. + // 2. configured and matches the request. + return authorization === this.webhookAuthorization + } +} diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index e877362f4de..cf82a0220d6 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -67,7 +67,8 @@ export class TestBsky { bigThreadUris: new Set(), ...cfg, adminPasswords: [ADMIN_PASSWORD], - revenueCatV1Url: process.env.BSKY_REVENUE_CAT_V1_URL, + revenueCatV1Url: + process.env.BSKY_REVENUE_CAT_V1_URL || 'https://api.revenuecat.com/v1', revenueCatV1ApiKey: process.env.BSKY_REVENUE_CAT_V1_API_KEY, revenueCatWebhookAuthorization: process.env.BSKY_REVENUE_CAT_WEBHOOK_AUTHORIZATION,