From 3ddbff24dffe187cb97a85c5bde8bc6b058aa4da Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Sun, 19 Apr 2026 10:26:23 +0530 Subject: [PATCH 1/3] refactor: use exact pubkey matching for whitelist/blacklist --- src/app/static-mirroring-worker.ts | 6 +++--- .../invoices/post-invoice-controller.ts | 6 ++---- src/handlers/event-message-handler.ts | 6 +++--- src/services/payments-service.ts | 2 +- .../unit/handlers/event-message-handler.spec.ts | 12 ++++++------ test/unit/services/payments-service.spec.ts | 17 +++++++++++++++-- 6 files changed, 30 insertions(+), 19 deletions(-) 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..8f07ee33 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -88,7 +88,7 @@ export class PostInvoiceController implements IController { } const isApplicableFee = (feeSchedule: FeeSchedule) => - feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => pubkey.startsWith(prefix)) + feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.includes(pubkey) const admissionFee = currentSettings.payments?.feeSchedules.admission.filter(isApplicableFee) if (!Array.isArray(admissionFee) || !admissionFee.length) { @@ -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 2c5fd5e0..ded1cd6a 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 227d19bc..1d39d58a 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 blacklisted if list entry is not exact match', () => { 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 extends a whitelist entry but is not an exact match', () => { 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..74078368 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({ @@ -410,6 +410,19 @@ describe('PaymentsService', () => { 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')) From d9150b324d5213ffe063d0a3e162ac00afadeef5 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Sun, 19 Apr 2026 12:49:44 +0530 Subject: [PATCH 2/3] chore: add changeset for whitelist matching refactor --- .changeset/config.json | 4 ++-- .changeset/release-whitelist-exact-match.md | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/release-whitelist-exact-match.md diff --git a/.changeset/config.json b/.changeset/config.json index d88011f6..dcdf28d7 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": "@changesets/cli/changelog", + "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "cameri/nostream" }], "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/release-whitelist-exact-match.md b/.changeset/release-whitelist-exact-match.md new file mode 100644 index 00000000..dcce90fd --- /dev/null +++ b/.changeset/release-whitelist-exact-match.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Use exact pubkey matching for fee-schedule whitelists and event pubkey whitelist/blacklist checks. From 70e37b818ac324cce4b05f4382f9370865b534c9 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Sun, 19 Apr 2026 21:21:57 +0530 Subject: [PATCH 3/3] fix: address review comments --- .changeset/release-whitelist-exact-match.md | 2 +- src/controllers/invoices/post-invoice-controller.ts | 2 +- test/unit/handlers/event-message-handler.spec.ts | 4 ++-- test/unit/services/payments-service.spec.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/release-whitelist-exact-match.md b/.changeset/release-whitelist-exact-match.md index dcce90fd..d806b77d 100644 --- a/.changeset/release-whitelist-exact-match.md +++ b/.changeset/release-whitelist-exact-match.md @@ -1,5 +1,5 @@ --- -"nostream": patch +"nostream": major --- Use exact pubkey matching for fee-schedule whitelists and event pubkey whitelist/blacklist checks. diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index 8f07ee33..bdc9e6da 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -89,7 +89,7 @@ export class PostInvoiceController implements IController { const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.includes(pubkey) - const admissionFee = currentSettings.payments?.feeSchedules.admission.filter(isApplicableFee) + 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') diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index 1d39d58a..59777866 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 if list entry is not exact match', () => { + 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 @@ -509,7 +509,7 @@ describe('EventMessageHandler', () => { expect((handler as any).canAcceptEvent(event)).to.be.undefined }) - it('returns reason if pubkey extends a whitelist entry but is not an exact match', () => { + 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.equal('blocked: pubkey not allowed') diff --git a/test/unit/services/payments-service.spec.ts b/test/unit/services/payments-service.spec.ts index 74078368..53ba0563 100644 --- a/test/unit/services/payments-service.spec.ts +++ b/test/unit/services/payments-service.spec.ts @@ -395,7 +395,7 @@ describe('PaymentsService', () => { expect(userRepository.admitUser).not.to.have.been.called }) - it('skips the fee for whitelisted pubkeys(exact match)', async () => { + it('skips the fee for whitelisted pubkeys (exact match)', async () => { settings.returns(makeSettings([ { enabled: true, amount: 1000n, whitelists: { pubkeys: ['whitelistedpubkey'] } }, ])) @@ -406,7 +406,7 @@ 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 })