diff --git a/.changeset/release-whitelist-exact-match.md b/.changeset/release-whitelist-exact-match.md new file mode 100644 index 00000000..d806b77d --- /dev/null +++ b/.changeset/release-whitelist-exact-match.md @@ -0,0 +1,5 @@ +--- +"nostream": major +--- + +Use exact pubkey matching for fee-schedule whitelists and event pubkey whitelist/blacklist checks. diff --git a/src/app/static-mirroring-worker.ts b/src/app/static-mirroring-worker.ts index 64582ff2..852e97e5 100644 --- a/src/app/static-mirroring-worker.ts +++ b/src/app/static-mirroring-worker.ts @@ -239,7 +239,7 @@ export class StaticMirroringWorker implements IRunnable { if ( typeof limits.pubkey?.whitelist !== 'undefined' && limits.pubkey.whitelist.length > 0 && - !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) + !limits.pubkey.whitelist.includes(event.pubkey) ) { debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) return false @@ -248,7 +248,7 @@ export class StaticMirroringWorker implements IRunnable { if ( typeof limits.pubkey?.blacklist !== 'undefined' && limits.pubkey.blacklist.length > 0 && - limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) + limits.pubkey.blacklist.includes(event.pubkey) ) { debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) return false @@ -288,7 +288,7 @@ export class StaticMirroringWorker implements IRunnable { const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled && - !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) && + !feeSchedule.whitelists?.pubkeys?.includes(event.pubkey) && !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index 1d908903..bdc9e6da 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -88,8 +88,8 @@ export class PostInvoiceController implements IController { } const isApplicableFee = (feeSchedule: FeeSchedule) => - feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => pubkey.startsWith(prefix)) - const admissionFee = currentSettings.payments?.feeSchedules.admission.filter(isApplicableFee) + feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.includes(pubkey) + const admissionFee = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) ?? [] if (!Array.isArray(admissionFee) || !admissionFee.length) { response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('No admission fee required') @@ -106,9 +106,7 @@ export class PostInvoiceController implements IController { } let invoice: Invoice - const amount = admissionFee.reduce((sum, fee) => { - return fee.enabled && !fee.whitelists?.pubkeys?.includes(pubkey) ? BigInt(fee.amount) + sum : sum - }, 0n) + const amount = admissionFee.reduce((sum, fee) => BigInt(fee.amount) + sum, 0n) try { const description = `${relayName} Admission Fee for ${toBech32('npub')(pubkey)}` diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index eb8fa546..62ee464d 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -195,7 +195,7 @@ export class EventMessageHandler implements IMessageHandler { if ( typeof limits.pubkey?.whitelist !== 'undefined' && limits.pubkey.whitelist.length > 0 && - !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) + !limits.pubkey.whitelist.includes(event.pubkey) ) { return 'blocked: pubkey not allowed' } @@ -203,7 +203,7 @@ export class EventMessageHandler implements IMessageHandler { if ( typeof limits.pubkey?.blacklist !== 'undefined' && limits.pubkey.blacklist.length > 0 && - limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) + limits.pubkey.blacklist.includes(event.pubkey) ) { return 'blocked: pubkey not allowed' } @@ -332,7 +332,7 @@ export class EventMessageHandler implements IMessageHandler { const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled && - !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) && + !feeSchedule.whitelists?.pubkeys?.includes(event.pubkey) && !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) diff --git a/src/services/payments-service.ts b/src/services/payments-service.ts index 237e8907..3b3da50a 100644 --- a/src/services/payments-service.ts +++ b/src/services/payments-service.ts @@ -164,7 +164,7 @@ export class PaymentsService implements IPaymentsService { } const isApplicableFee = (feeSchedule: FeeSchedule) => - feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => invoice.pubkey.startsWith(prefix)) + feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.includes(invoice.pubkey) const admissionFeeSchedules = currentSettings.payments?.feeSchedules?.admission ?? [] const admissionFeeAmount = admissionFeeSchedules.reduce((sum, feeSchedule) => { return sum + (isApplicableFee(feeSchedule) ? BigInt(feeSchedule.amount) : 0n) diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index fb4610c3..8c3f4685 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -478,7 +478,7 @@ describe('EventMessageHandler', () => { expect((handler as any).canAcceptEvent(event)).to.be.undefined }) - it('returns undefined if pubkey is not blacklisted by prefix', () => { + it('returns undefined if pubkey is not an exact match in the blacklist', () => { eventLimits.pubkey.blacklist = ['aa55'] event.pubkey = 'aabbcc' expect((handler as any).canAcceptEvent(event)).to.be.undefined @@ -490,10 +490,10 @@ describe('EventMessageHandler', () => { expect((handler as any).canAcceptEvent(event)).to.equal('blocked: pubkey not allowed') }) - it('returns reason if pubkey is blacklisted by prefix', () => { + it('returns undefined if pubkey extends a blacklist entry but is not an exact match', () => { eventLimits.pubkey.blacklist = ['aa55'] event.pubkey = 'aa55ccddeeff' - expect((handler as any).canAcceptEvent(event)).to.equal('blocked: pubkey not allowed') + expect((handler as any).canAcceptEvent(event)).to.be.undefined }) }) @@ -509,10 +509,10 @@ describe('EventMessageHandler', () => { expect((handler as any).canAcceptEvent(event)).to.be.undefined }) - it('returns undefined if pubkey is whitelisted by prefix', () => { + it('returns reason if pubkey is not an exact match in the whitelist', () => { eventLimits.pubkey.whitelist = ['aa55'] event.pubkey = 'aa55ccddeeff' - expect((handler as any).canAcceptEvent(event)).to.be.undefined + expect((handler as any).canAcceptEvent(event)).to.equal('blocked: pubkey not allowed') }) it('returns reason if pubkey is not whitelisted', () => { @@ -521,7 +521,7 @@ describe('EventMessageHandler', () => { expect((handler as any).canAcceptEvent(event)).to.equal('blocked: pubkey not allowed') }) - it('returns reason if pubkey is not whitelisted by prefix', () => { + it('returns reason if pubkey is not whitelisted by exact match', () => { eventLimits.pubkey.whitelist = ['aa55'] event.pubkey = 'aabbccddeeff' expect((handler as any).canAcceptEvent(event)).to.equal('blocked: pubkey not allowed') diff --git a/test/unit/services/payments-service.spec.ts b/test/unit/services/payments-service.spec.ts index 6d4e7035..53ba0563 100644 --- a/test/unit/services/payments-service.spec.ts +++ b/test/unit/services/payments-service.spec.ts @@ -395,9 +395,9 @@ describe('PaymentsService', () => { expect(userRepository.admitUser).not.to.have.been.called }) - it('skips the fee for whitelisted pubkeys', async () => { + it('skips the fee for whitelisted pubkeys (exact match)', async () => { settings.returns(makeSettings([ - { enabled: true, amount: 1000n, whitelists: { pubkeys: ['whitelisted'] } }, + { enabled: true, amount: 1000n, whitelists: { pubkeys: ['whitelistedpubkey'] } }, ])) await service.confirmInvoice(makeCompletedInvoice({ @@ -406,10 +406,23 @@ describe('PaymentsService', () => { amountPaid: 5000n, })) - // pubkey starts with 'whitelisted' → isApplicableFee = false → admissionFeeAmount = 0 → not admitted + // pubkey does not exactly match whitelist entry -> fee applies -> user must pay to get admitted expect(userRepository.admitUser).not.to.have.been.called }) + it('applies the fee when pubkey is not an exact whitelist match (prefix alone is insufficient)', async () => { + settings.returns(makeSettings([ + { enabled: true, amount: 1000n, whitelists: { pubkeys: ['whitelisted'] } }, + ])) + await service.confirmInvoice(makeCompletedInvoice({ + pubkey: 'whitelistedpubkey', + unit: InvoiceUnit.MSATS, + amountPaid: 5000n, + })) + expect(userRepository.admitUser).to.have.been.calledOnce + }) + + it('rolls back the transaction and re-throws on error', async () => { settings.returns(makeSettings([])) invoiceRepository.confirmInvoice.rejects(new Error('db error'))