diff --git a/packages/bsync/package.json b/packages/bsync/package.json index a2e9f631b0d..f2fd34c73c6 100644 --- a/packages/bsync/package.json +++ b/packages/bsync/package.json @@ -29,7 +29,11 @@ "@atproto/syntax": "workspace:^", "@bufbuild/protobuf": "^1.5.0", "@connectrpc/connect": "^1.1.4", + "@connectrpc/connect-express": "^1.1.4", "@connectrpc/connect-node": "^1.1.4", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.21.1", "http-terminator": "^3.2.0", "kysely": "^0.22.0", "pg": "^8.10.0", @@ -40,6 +44,9 @@ "@bufbuild/buf": "^1.28.1", "@bufbuild/protoc-gen-es": "^1.5.0", "@connectrpc/protoc-gen-connect-es": "^1.1.4", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", "@types/pg": "^8.6.6", "get-port": "^5.1.1", "jest": "^28.1.2", diff --git a/packages/bsync/src/api/health.ts b/packages/bsync/src/api/health.ts new file mode 100644 index 00000000000..814579ac2d7 --- /dev/null +++ b/packages/bsync/src/api/health.ts @@ -0,0 +1,12 @@ +import express from 'express' +import AppContext from '../context' + +export const createRouter = (ctx: AppContext): express.Router => { + const router = express.Router() + + router.get('/_health', async function (_req, res) { + res.send({ version: ctx.cfg.service.version }) + }) + + return router +} diff --git a/packages/bsync/src/api/revenueCat.ts b/packages/bsync/src/api/revenueCat.ts new file mode 100644 index 00000000000..214fbeae79f --- /dev/null +++ b/packages/bsync/src/api/revenueCat.ts @@ -0,0 +1,76 @@ +import express, { RequestHandler } from 'express' +import { AppContext } from '..' +import { RevenueCatClient } from '../purchases' +import { addPurchaseOperation, RcEventBody } from '../purchases' +import { isValidDid } from '../routes/util' +import { httpLogger as log } from '..' + +type AppContextWithRevenueCatClient = AppContext & { + revenueCatClient: RevenueCatClient +} + +const auth = + (ctx: AppContextWithRevenueCatClient): RequestHandler => + (req: express.Request, res: express.Response, next: express.NextFunction) => + ctx.revenueCatClient.isWebhookAuthorizationValid( + req.header('Authorization'), + ) + ? next() + : res.status(403).send({ + success: false, + error: 'Forbidden: invalid authentication for RevenueCat webhook', + }) + +const webhookHandler = + (ctx: AppContextWithRevenueCatClient): RequestHandler => + async (req, res) => { + const { revenueCatClient } = ctx + + const body: RcEventBody = req.body + + try { + const { app_user_id: actorDid } = body.event + + if (!isValidDid(actorDid)) { + return res.status(400).send({ + success: false, + error: 'Bad request: invalid DID in app_user_id', + }) + } + + const entitlements = + await revenueCatClient.getEntitlementIdentifiers(actorDid) + + const id = await addPurchaseOperation(ctx.db, actorDid, entitlements) + + res.send({ success: true, operationId: id }) + } catch (error) { + log.error(error) + + res.status(500).send({ + success: false, + error: + 'Internal server error: an error happened while processing the request', + }) + } + } + +const assertAppContextWithRevenueCatClient: ( + ctx: AppContext, +) => asserts ctx is AppContextWithRevenueCatClient = (ctx: AppContext) => { + if (!ctx.revenueCatClient) { + throw new Error( + 'RevenueCat webhook was tried to be set up without configuring a RevenueCat client.', + ) + } +} + +export const createRouter = (ctx: AppContext): express.Router => { + assertAppContextWithRevenueCatClient(ctx) + + const router = express.Router() + router.use(auth(ctx)) + router.use(express.json()) + router.post('/', webhookHandler(ctx)) + return router +} diff --git a/packages/bsync/src/context.ts b/packages/bsync/src/context.ts index 34c66e94f4e..73a576e1724 100644 --- a/packages/bsync/src/context.ts +++ b/packages/bsync/src/context.ts @@ -4,7 +4,7 @@ import Database from './db' import { createMuteOpChannel } from './db/schema/mute_op' import { createNotifOpChannel } from './db/schema/notif_op' import { EventEmitter } from 'stream' -import { RevenueCatClient } from './subscriptions' +import { RevenueCatClient } from './purchases' export type AppContextOptions = { db: Database diff --git a/packages/bsync/src/db/migrations/20241205T030533572Z-subs.ts b/packages/bsync/src/db/migrations/20241205T030533572Z-purchases.ts similarity index 80% rename from packages/bsync/src/db/migrations/20241205T030533572Z-subs.ts rename to packages/bsync/src/db/migrations/20241205T030533572Z-purchases.ts index 12b1a7bcf17..2c459750556 100644 --- a/packages/bsync/src/db/migrations/20241205T030533572Z-subs.ts +++ b/packages/bsync/src/db/migrations/20241205T030533572Z-purchases.ts @@ -2,7 +2,7 @@ import { Kysely, sql } from 'kysely' export async function up(db: Kysely): Promise { await db.schema - .createTable('subs_op') + .createTable('purchase_op') .addColumn('id', 'bigserial', (col) => col.primaryKey()) .addColumn('actorDid', 'varchar', (col) => col.notNull()) .addColumn('entitlements', 'jsonb', (col) => col.notNull()) @@ -11,7 +11,7 @@ export async function up(db: Kysely): Promise { ) .execute() await db.schema - .createTable('subs_item') + .createTable('purchase_item') .addColumn('actorDid', 'varchar', (col) => col.primaryKey()) .addColumn('entitlements', 'jsonb', (col) => col.notNull()) .addColumn('fromId', 'bigint', (col) => col.notNull()) @@ -19,6 +19,6 @@ export async function up(db: Kysely): Promise { } export async function down(db: Kysely): Promise { - await db.schema.dropTable('subs_item').execute() - await db.schema.dropTable('subs_op').execute() + await db.schema.dropTable('purchase_item').execute() + await db.schema.dropTable('purchase_op').execute() } diff --git a/packages/bsync/src/db/migrations/index.ts b/packages/bsync/src/db/migrations/index.ts index bb0b7923a9d..167c9fdc534 100644 --- a/packages/bsync/src/db/migrations/index.ts +++ b/packages/bsync/src/db/migrations/index.ts @@ -4,4 +4,4 @@ export * as _20240108T220751294Z from './20240108T220751294Z-init' export * as _20240717T224303472Z from './20240717T224303472Z-notif-ops' -export * as _20241205T030533572Z from './20241205T030533572Z-subs' +export * as _20241205T030533572Z from './20241205T030533572Z-purchases' diff --git a/packages/bsync/src/db/schema/index.ts b/packages/bsync/src/db/schema/index.ts index dea00f7665e..6a6267f7531 100644 --- a/packages/bsync/src/db/schema/index.ts +++ b/packages/bsync/src/db/schema/index.ts @@ -3,15 +3,15 @@ import * as muteOp from './mute_op' import * as muteItem from './mute_item' import * as notifOp from './notif_op' import * as notifItem from './notif_item' -import * as subsOp from './subs_op' -import * as subsItem from './subs_item' +import * as purchaseOp from './purchase_op' +import * as purchaseItem from './purchase_item' export type DatabaseSchemaType = muteItem.PartialDB & muteOp.PartialDB & notifItem.PartialDB & notifOp.PartialDB & - subsItem.PartialDB & - subsOp.PartialDB + purchaseItem.PartialDB & + purchaseOp.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/bsync/src/db/schema/subs_item.ts b/packages/bsync/src/db/schema/purchase_item.ts similarity index 51% rename from packages/bsync/src/db/schema/subs_item.ts rename to packages/bsync/src/db/schema/purchase_item.ts index 523275cec23..f90f840399a 100644 --- a/packages/bsync/src/db/schema/subs_item.ts +++ b/packages/bsync/src/db/schema/purchase_item.ts @@ -1,14 +1,14 @@ import { ColumnType, Selectable } from 'kysely' -export interface SubsItem { +export interface PurchaseItem { actorDid: string // https://github.com/kysely-org/kysely/issues/137 entitlements: ColumnType fromId: number } -export type SubsItemEntry = Selectable +export type PurchaseItemEntry = Selectable -export const tableName = 'subs_item' +export const tableName = 'purchase_item' -export type PartialDB = { [tableName]: SubsItem } +export type PartialDB = { [tableName]: PurchaseItem } diff --git a/packages/bsync/src/db/schema/subs_op.ts b/packages/bsync/src/db/schema/purchase_op.ts similarity index 50% rename from packages/bsync/src/db/schema/subs_op.ts rename to packages/bsync/src/db/schema/purchase_op.ts index e7feb00a1be..b451bc0137f 100644 --- a/packages/bsync/src/db/schema/subs_op.ts +++ b/packages/bsync/src/db/schema/purchase_op.ts @@ -1,6 +1,6 @@ import { ColumnType, GeneratedAlways, Selectable } from 'kysely' -export interface SubsOp { +export interface PurchaseOp { id: GeneratedAlways actorDid: string // https://github.com/kysely-org/kysely/issues/137 @@ -8,10 +8,10 @@ export interface SubsOp { createdAt: GeneratedAlways } -export type SubsOpEntry = Selectable +export type PurchaseOpEntry = Selectable -export const tableName = 'subs_op' +export const tableName = 'purchase_op' -export type PartialDB = { [tableName]: SubsOp } +export type PartialDB = { [tableName]: PurchaseOp } -export const createSubsOpChannel = 'subs_op_create' // used with listen/notify +export const createPurchaseOpChannel = 'purchase_op_create' // used with listen/notify diff --git a/packages/bsync/src/index.ts b/packages/bsync/src/index.ts index 297326b19a1..10bade58ea0 100644 --- a/packages/bsync/src/index.ts +++ b/packages/bsync/src/index.ts @@ -1,17 +1,19 @@ +import express from 'express' +import cors from 'cors' +import compression from 'compression' import http from 'node:http' import events from 'node:events' import { createHttpTerminator, HttpTerminator } from 'http-terminator' -import { connectNodeAdapter } from '@connectrpc/connect-node' import { dbLogger, loggerMiddleware } from './logger' import AppContext, { AppContextOptions } from './context' import { ServerConfig } from './config' import routes from './routes' import { createMuteOpChannel } from './db/schema/mute_op' import { createNotifOpChannel } from './db/schema/notif_op' -import { - isRevenueCatWebhookUrl, - revenueCatWebhookHandler, -} from './subscriptions' +import * as health from './api/health' +import * as revenueCat from './api/revenueCat' +import { DAY, SECOND } from '@atproto/common' +import { expressConnectMiddleware } from '@connectrpc/connect-express' export * from './config' export * from './client' @@ -21,20 +23,20 @@ export { httpLogger } from './logger' export class BsyncService { public ctx: AppContext - public server: http.Server + public app: express.Application + public server?: http.Server private ac: AbortController - private terminator: HttpTerminator + private terminator?: HttpTerminator private dbStatsInterval?: NodeJS.Timeout constructor(opts: { ctx: AppContext - server: http.Server + app: express.Application ac: AbortController }) { this.ctx = opts.ctx - this.server = opts.server + this.app = opts.app this.ac = opts.ac - this.terminator = createHttpTerminator({ server: this.server }) } static async create( @@ -43,23 +45,25 @@ export class BsyncService { ): Promise { const ac = new AbortController() const ctx = await AppContext.fromConfig(cfg, ac.signal, overrides) - const handler = connectNodeAdapter({ - routes: routes(ctx), - shutdownSignal: ac.signal, - }) - const server = http.createServer((req, res) => { - loggerMiddleware(req, res) - if (isHealth(req.url)) { - res.statusCode = 200 - res.setHeader('content-type', 'application/json') - return res.end(JSON.stringify({ version: cfg.service.version })) - } - if (isRevenueCatWebhookUrl(req.url)) { - return revenueCatWebhookHandler(ctx, req, res) - } - handler(req, res) - }) - return new BsyncService({ ctx, server, ac }) + + const app = express() + app.use(cors({ maxAge: DAY / SECOND })) + app.use(loggerMiddleware) + app.use(compression()) + + app.use( + expressConnectMiddleware({ + routes: routes(ctx), + shutdownSignal: ac.signal, + }), + ) + + app.use(health.createRouter(ctx)) + if (ctx.revenueCatClient) { + app.use('/webhooks/revenuecat', revenueCat.createRouter(ctx)) + } + + return new BsyncService({ ctx, app, ac }) } async start(): Promise { @@ -77,15 +81,18 @@ export class BsyncService { ) }, 10000) await this.setupAppEvents() - this.server.listen(this.ctx.cfg.service.port) - this.server.keepAliveTimeout = 90000 + + const server = this.app.listen(this.ctx.cfg.service.port) + server.keepAliveTimeout = 90000 + this.server = server + this.terminator = createHttpTerminator({ server: this.server }) await events.once(this.server, 'listening') return this.server } async destroy(): Promise { this.ac.abort() - await this.terminator.terminate() + await this.terminator?.terminate() await this.ctx.db.close() clearInterval(this.dbStatsInterval) this.dbStatsInterval = undefined @@ -111,9 +118,3 @@ export class BsyncService { } export default BsyncService - -const isHealth = (urlStr: string | undefined) => { - if (!urlStr) return false - const url = new URL(urlStr, 'http://host') - return url.pathname === '/_health' -} diff --git a/packages/bsync/src/purchases/addPurchaseOperation.ts b/packages/bsync/src/purchases/addPurchaseOperation.ts new file mode 100644 index 00000000000..59eaa2552a6 --- /dev/null +++ b/packages/bsync/src/purchases/addPurchaseOperation.ts @@ -0,0 +1,58 @@ +import { sql } from 'kysely' +import { Database } from '..' +import { createPurchaseOpChannel } from '../db/schema/purchase_op' + +export const addPurchaseOperation = async ( + db: Database, + actorDid: string, + entitlements: string[], +) => { + return db.transaction(async (txn) => { + // create purchase op + const id = await createPurchaseOp(txn, actorDid, entitlements) + // update purchase state + await updatePurchaseItem(txn, id, actorDid, entitlements) + return id + }) +} + +const createPurchaseOp = async ( + db: Database, + actorDid: string, + entitlements: string[], +) => { + const { ref } = db.db.dynamic + const { id } = await db.db + .insertInto('purchase_op') + .values({ + actorDid, + entitlements: JSON.stringify(entitlements), + }) + .returning('id') + .executeTakeFirstOrThrow() + await sql`notify ${ref(createPurchaseOpChannel)}`.execute(db.db) // emitted transactionally + return id +} + +const updatePurchaseItem = async ( + db: Database, + fromId: number, + actorDid: string, + entitlements: string[], +) => { + const { ref } = db.db.dynamic + await db.db + .insertInto('purchase_item') + .values({ + actorDid, + entitlements: JSON.stringify(entitlements), + fromId, + }) + .onConflict((oc) => + oc.column('actorDid').doUpdateSet({ + entitlements: sql`${ref('excluded.entitlements')}`, + fromId: sql`${ref('excluded.fromId')}`, + }), + ) + .execute() +} diff --git a/packages/bsync/src/purchases/index.ts b/packages/bsync/src/purchases/index.ts new file mode 100644 index 00000000000..4a4b515dc03 --- /dev/null +++ b/packages/bsync/src/purchases/index.ts @@ -0,0 +1,3 @@ +export * from './addPurchaseOperation' +export * from './revenueCatClient' +export * from './revenueCatTypes' diff --git a/packages/bsync/src/subscriptions/revenueCatClient.ts b/packages/bsync/src/purchases/revenueCatClient.ts similarity index 77% rename from packages/bsync/src/subscriptions/revenueCatClient.ts rename to packages/bsync/src/purchases/revenueCatClient.ts index e71b61e8ed4..f223a43baaa 100644 --- a/packages/bsync/src/subscriptions/revenueCatClient.ts +++ b/packages/bsync/src/purchases/revenueCatClient.ts @@ -1,24 +1,11 @@ +import { RcGetSubscriberResponse } from './revenueCatTypes' + type Config = { v1ApiKey: string v1ApiUrl: string webhookAuthorization: string } -// Reference: https://www.revenuecat.com/docs/api-v1#tag/customers -export type GetSubscriberResponse = { - subscriber: Subscriber -} - -export type Subscriber = { - entitlements: { - [entitlementIdentifier: string]: Entitlement - } -} - -export type Entitlement = { - expires_date: string -} - export class RevenueCatClient { private v1ApiKey: string private v1ApiUrl: string @@ -49,8 +36,8 @@ export class RevenueCatClient { return res.json() as T } - private getSubscriber(did: string): Promise { - return this.fetch( + private getSubscriber(did: string): Promise { + return this.fetch( `/subscribers/${encodeURIComponent(did)}`, ) } diff --git a/packages/bsync/src/purchases/revenueCatTypes.ts b/packages/bsync/src/purchases/revenueCatTypes.ts new file mode 100644 index 00000000000..9d216287bbd --- /dev/null +++ b/packages/bsync/src/purchases/revenueCatTypes.ts @@ -0,0 +1,23 @@ +// Reference: https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields#events-format +export type RcEventBody = { + api_version: '1.0' + event: { + app_user_id: string + type: string + } +} + +// Reference: https://www.revenuecat.com/docs/api-v1#tag/customers +export type RcGetSubscriberResponse = { + subscriber: RcSubscriber +} + +export type RcSubscriber = { + entitlements: { + [entitlementIdentifier: string]: RcEntitlement + } +} + +export type RcEntitlement = { + expires_date: string +} diff --git a/packages/bsync/src/subscriptions/index.ts b/packages/bsync/src/subscriptions/index.ts deleted file mode 100644 index a1a87a784ee..00000000000 --- a/packages/bsync/src/subscriptions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './revenueCatClient' -export * from './revenueCatWebhookHandler' diff --git a/packages/bsync/src/subscriptions/revenueCatWebhookHandler.ts b/packages/bsync/src/subscriptions/revenueCatWebhookHandler.ts deleted file mode 100644 index 4e7fc7fa49e..00000000000 --- a/packages/bsync/src/subscriptions/revenueCatWebhookHandler.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { IncomingMessage, ServerResponse } from 'node:http' -import AppContext from '../context' -import { isValidDid } from '../routes/util' -import { Database } from '..' -import { sql } from 'kysely' -import { createSubsOpChannel } from '../db/schema/subs_op' - -export const isRevenueCatWebhookUrl = (urlStr: string | undefined) => { - if (!urlStr) return false - const url = new URL(urlStr, 'http://host') - return url.pathname === '/webhooks/revenuecat' -} - -const parseBody = async (req: IncomingMessage) => - new Promise((resolve, reject) => { - let body = '' - req.on('data', (chunk) => (body += chunk)) - req.on('end', () => { - try { - resolve(JSON.parse(body)) - } catch (err) { - reject(err) - } - }) - req.on('error', reject) - }) - -// Reference: https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields#events-format -type RevenueCatEventBody = { - api_version: '1.0' - event: { - app_user_id: string - type: string - } -} - -export const revenueCatWebhookHandler = async ( - ctx: AppContext, - req: IncomingMessage, - res: ServerResponse, -) => { - const { db, revenueCatClient } = ctx - - res.setHeader('content-type', 'application/json') - - if (!revenueCatClient) { - res.statusCode = 501 - return res.end( - JSON.stringify({ - error: - 'Not Implemented: bsync is being served without RevenueCat support', - }), - ) - } - - if ( - !revenueCatClient.isWebhookAuthorizationValid(req.headers['authorization']) - ) { - res.statusCode = 403 - return res.end( - JSON.stringify({ - error: 'Forbidden: invalid authentication for RevenueCat webhook', - }), - ) - } - - if (req.method !== 'POST') { - res.statusCode = 501 - return res.end( - JSON.stringify({ - error: - 'Not Implemented: only POST method is supported for RevenueCat webhook', - }), - ) - } - - if (req.headers['content-type'] !== 'application/json') { - res.statusCode = 400 - return res.end( - JSON.stringify({ - error: - 'Bad request: body must be JSON with Content-Type: application/json', - }), - ) - } - - let body: RevenueCatEventBody - try { - body = (await parseBody(req)) as RevenueCatEventBody - } catch (error) { - res.statusCode = 400 - return res.end( - JSON.stringify({ - error: 'Bad request: malformed JSON body', - }), - ) - } - - try { - const { app_user_id: actorDid } = body.event - - if (!isValidDid(actorDid)) { - res.statusCode = 400 - return res.end( - JSON.stringify({ - error: 'Bad request: invalid DID in app_user_id', - }), - ) - } - - const entitlements = - await revenueCatClient.getEntitlementIdentifiers(actorDid) - - const id = await db.transaction(async (txn) => { - // create subs op - const id = await createSubsOp(txn, actorDid, entitlements) - // update subs state - await updateSubsItem(txn, id, actorDid, entitlements) - return id - }) - - res.statusCode = 200 - res.end(JSON.stringify({ success: true, operationId: id })) - } catch (error) { - res.statusCode = 500 - return res.end( - JSON.stringify({ - error: - 'Internal server error: an error happened while processing the request', - }), - ) - } -} - -const createSubsOp = async ( - db: Database, - actorDid: string, - entitlements: string[], -) => { - const { ref } = db.db.dynamic - const { id } = await db.db - .insertInto('subs_op') - .values({ - actorDid, - entitlements: JSON.stringify(entitlements), - }) - .returning('id') - .executeTakeFirstOrThrow() - await sql`notify ${ref(createSubsOpChannel)}`.execute(db.db) // emitted transactionally - return id -} - -const updateSubsItem = async ( - db: Database, - fromId: number, - actorDid: string, - entitlements: string[], -) => { - const { ref } = db.db.dynamic - await db.db - .insertInto('subs_item') - .values({ - actorDid, - entitlements: JSON.stringify(entitlements), - fromId, - }) - .onConflict((oc) => - oc.column('actorDid').doUpdateSet({ - entitlements: sql`${ref('excluded.entitlements')}`, - fromId: sql`${ref('excluded.fromId')}`, - }), - ) - .execute() -} diff --git a/packages/bsync/tests/subscriptions.test.ts b/packages/bsync/tests/purchases.test.ts similarity index 82% rename from packages/bsync/tests/subscriptions.test.ts rename to packages/bsync/tests/purchases.test.ts index 8ba8f963f31..50705be476f 100644 --- a/packages/bsync/tests/subscriptions.test.ts +++ b/packages/bsync/tests/purchases.test.ts @@ -2,24 +2,24 @@ import http from 'node:http' import { once } from 'node:events' import getPort from 'get-port' import { BsyncService, Database, envToCfg } from '../src' -import { Entitlement, GetSubscriberResponse } from '../src/subscriptions' +import { RcEntitlement, RcGetSubscriberResponse } from '../src/purchases' const revenueCatWebhookAuthorization = 'Bearer any-token' -describe('subscriptions', () => { +describe('purchases', () => { let bsync: BsyncService let bsyncUrl: string const actorDid = 'did:example:a' let revenueCatServer: http.Server - let revenueCatApiMock: jest.Mock + let revenueCatApiMock: jest.Mock const TEN_MINUTES = 600_000 - const entitlementValid: Entitlement = { + const entitlementValid: RcEntitlement = { expires_date: new Date(Date.now() + TEN_MINUTES).toISOString(), } - const entitlementExpired: Entitlement = { + const entitlementExpired: RcEntitlement = { expires_date: new Date(Date.now() - TEN_MINUTES).toISOString(), } @@ -36,7 +36,7 @@ describe('subscriptions', () => { envToCfg({ port: await getPort(), dbUrl: process.env.DB_POSTGRES_URL, - dbSchema: 'bsync_subscriptions', + dbSchema: 'bsync_purchases', apiKeys: ['key-1'], longPollTimeoutMs: 500, revenueCatV1ApiKey: 'any-key', @@ -58,7 +58,7 @@ describe('subscriptions', () => { }) beforeEach(async () => { - await clearSubs(bsync.ctx.db) + await clearPurchases(bsync.ctx.db) }) describe('webhook handler', () => { @@ -78,6 +78,17 @@ describe('subscriptions', () => { }) }) + it('returns 400 if DID is invalid', async () => { + const response = await callWebhook(bsyncUrl, { + event: { app_user_id: 'invalidDid' }, + }) + + expect(response.status).toBe(400) + expect(response.json()).resolves.toMatchObject({ + error: 'Bad request: invalid DID in app_user_id', + }) + }) + it('stores valid entitlements from the API response, excluding expired', async () => { revenueCatApiMock.mockReturnValueOnce({ subscriber: { @@ -90,7 +101,7 @@ describe('subscriptions', () => { }) const op0 = await bsync.ctx.db.db - .selectFrom('subs_op') + .selectFrom('purchase_op') .selectAll() .where('actorDid', '=', actorDid) .orderBy('id', 'desc') @@ -105,7 +116,7 @@ describe('subscriptions', () => { await expect( bsync.ctx.db.db - .selectFrom('subs_item') + .selectFrom('purchase_item') .selectAll() .where('actorDid', '=', actorDid) .executeTakeFirstOrThrow(), @@ -126,7 +137,7 @@ describe('subscriptions', () => { }) const op1 = await bsync.ctx.db.db - .selectFrom('subs_op') + .selectFrom('purchase_op') .selectAll() .where('actorDid', '=', actorDid) .orderBy('id', 'desc') @@ -141,7 +152,7 @@ describe('subscriptions', () => { await expect( bsync.ctx.db.db - .selectFrom('subs_item') + .selectFrom('purchase_item') .selectAll() .where('actorDid', '=', actorDid) .executeTakeFirstOrThrow(), @@ -162,7 +173,7 @@ describe('subscriptions', () => { }) const op = await bsync.ctx.db.db - .selectFrom('subs_op') + .selectFrom('purchase_op') .selectAll() .where('actorDid', '=', actorDid) .orderBy('id', 'desc') @@ -177,7 +188,7 @@ describe('subscriptions', () => { await expect( bsync.ctx.db.db - .selectFrom('subs_item') + .selectFrom('purchase_item') .selectAll() .where('actorDid', '=', actorDid) .executeTakeFirstOrThrow(), @@ -190,16 +201,16 @@ describe('subscriptions', () => { }) }) -const clearSubs = async (db: Database) => { - await db.db.deleteFrom('subs_item').execute() - await db.db.deleteFrom('subs_op').execute() +const clearPurchases = async (db: Database) => { + await db.db.deleteFrom('purchase_item').execute() + await db.db.deleteFrom('purchase_op').execute() } const callWebhook = async ( baseUrl: string, body: Record, ): Promise => { - const response = await fetch(`${baseUrl}/webhooks/revenuecat`, { + return fetch(`${baseUrl}/webhooks/revenuecat`, { method: 'POST', body: JSON.stringify(body), headers: { @@ -207,19 +218,11 @@ const callWebhook = async ( 'Content-Type': 'application/json', }, }) - - if (!response.ok) { - throw new Error( - `Unexpected status on calling the webhook: '${response.status}'`, - ) - } - - return response } const createMockRevenueCatService = async ( port: number, - apiMock: jest.Mock, + apiMock: jest.Mock, ): Promise => { const server = http.createServer((req, res) => { if (!req.url) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 424882fc84c..cfa1930d26f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -314,9 +314,21 @@ importers: '@connectrpc/connect': specifier: ^1.1.4 version: 1.3.0(@bufbuild/protobuf@1.6.0) + '@connectrpc/connect-express': + specifier: ^1.1.4 + version: 1.3.0(@bufbuild/protobuf@1.6.0)(@connectrpc/connect-node@1.3.0)(@connectrpc/connect@1.3.0) '@connectrpc/connect-node': specifier: ^1.1.4 version: 1.3.0(@bufbuild/protobuf@1.6.0)(@connectrpc/connect@1.3.0) + compression: + specifier: ^1.7.4 + version: 1.7.4 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.21.1 + version: 4.21.1 http-terminator: specifier: ^3.2.0 version: 3.2.0 @@ -342,6 +354,15 @@ importers: '@connectrpc/protoc-gen-connect-es': specifier: ^1.1.4 version: 1.3.0(@bufbuild/protoc-gen-es@1.6.0)(@connectrpc/connect@1.3.0) + '@types/compression': + specifier: ^1.7.5 + version: 1.7.5 + '@types/cors': + specifier: ^2.8.12 + version: 2.8.12 + '@types/express': + specifier: ^4.17.13 + version: 4.17.21 '@types/pg': specifier: ^8.6.6 version: 8.6.6 @@ -4058,7 +4079,7 @@ packages: '@babel/traverse': 7.22.10 '@babel/types': 7.22.10 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4337,7 +4358,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.10 '@babel/types': 7.22.10 - debug: 4.3.4 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6231,6 +6252,12 @@ packages: '@types/connect': 3.4.35 '@types/node': 18.19.56 + /@types/compression@1.7.5: + resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==} + dependencies: + '@types/express': 4.17.21 + dev: true + /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: @@ -6662,7 +6689,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -7137,6 +7164,26 @@ packages: transitivePeerDependencies: - supports-color + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true @@ -7283,6 +7330,17 @@ packages: function-bind: 1.1.1 get-intrinsic: 1.2.1 + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -7600,6 +7658,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -7947,6 +8010,15 @@ packages: clone: 1.0.4 dev: true + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + /define-properties@1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} @@ -8132,6 +8204,11 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} requiresBuild: true @@ -8216,6 +8293,18 @@ packages: which-typed-array: 1.1.11 dev: true + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} @@ -8725,6 +8814,45 @@ packages: transitivePeerDependencies: - supports-color + /express@4.21.1: + resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true @@ -8874,6 +9002,21 @@ packages: transitivePeerDependencies: - supports-color + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -9026,6 +9169,10 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + /function.prototype.name@1.1.6: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} @@ -9077,6 +9224,17 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -9218,7 +9376,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: true /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -9280,6 +9437,12 @@ packages: dependencies: get-intrinsic: 1.2.1 + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -9311,6 +9474,13 @@ packages: inherits: 2.0.4 minimalistic-assert: 1.0.1 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -9375,7 +9545,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -9404,7 +9574,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -9819,7 +9989,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4 + debug: 4.3.7 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -10676,6 +10846,10 @@ packages: /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -11069,6 +11243,11 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + /object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + dev: false + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -11306,6 +11485,10 @@ packages: lru-cache: 10.2.0 minipass: 5.0.0 + /path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + dev: false + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -12151,6 +12334,13 @@ packages: dependencies: side-channel: 1.0.4 + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: true @@ -12192,6 +12382,16 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -12597,6 +12797,27 @@ packages: transitivePeerDependencies: - supports-color + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: @@ -12614,10 +12835,34 @@ packages: transitivePeerDependencies: - supports-color + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -12699,6 +12944,16 @@ packages: get-intrinsic: 1.2.1 object-inspect: 1.12.3 + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -12763,7 +13018,7 @@ packages: engines: {node: '>= 10'} dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.7 socks: 2.7.1 transitivePeerDependencies: - supports-color