From f2a01b96b9cd949843a209f5814ed8fe4f571c6f Mon Sep 17 00:00:00 2001 From: Mohit-Davar Date: Fri, 10 Apr 2026 23:52:59 +0530 Subject: [PATCH 1/2] feat(cache): implement caching for user admission check Closes #447 --- package-lock.json | 23 +- package.json | 2 +- src/@types/adapters.ts | 2 +- src/adapters/redis-adapter.ts | 9 +- src/constants/caching.ts | 5 + src/factories/message-handler-factory.ts | 13 +- src/handlers/event-message-handler.ts | 37 +- src/utils/nip44.ts | 48 +- .../factories/message-handler-factory.spec.ts | 14 + .../handlers/event-message-handler.spec.ts | 464 +++++++++++++++++- 10 files changed, 573 insertions(+), 44 deletions(-) create mode 100644 src/constants/caching.ts diff --git a/package-lock.json b/package-lock.json index 91862ee4..708e41a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "@types/express": "4.17.21", "@types/js-yaml": "4.0.5", "@types/mocha": "^9.1.1", - "@types/node": "^24.0.0", + "@types/node": "^24.12.2", "@types/pg": "^8.6.5", "@types/ramda": "^0.28.13", "@types/sinon": "^10.0.11", @@ -142,7 +142,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1157,7 +1156,6 @@ "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" } @@ -1580,7 +1578,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -1857,7 +1854,6 @@ "integrity": "sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bole": "^5.0.0", "ndjson": "^2.0.0" @@ -2057,7 +2053,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.4.2.tgz", "integrity": "sha512-oUdEjE0I7JS5AyaAjkD3aOXn9NhO7XKyPyXEyrgFDu++VrVBHUPnV6dgEya9TcMuj5nIJRuCzCm8ZP+c9zCHPw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.1", "generic-pool": "3.9.0", @@ -2970,7 +2965,6 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3787,7 +3781,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4017,7 +4010,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -4763,7 +4755,6 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -8420,7 +8411,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -11111,7 +11101,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12067,7 +12056,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.9.0.tgz", "integrity": "sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg==", "license": "MIT", - "peer": true, "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", @@ -13089,7 +13077,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/regexp-match-indices": { "version": "1.0.2", @@ -13497,7 +13486,6 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -13603,7 +13591,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -15802,7 +15789,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15959,7 +15945,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -16242,7 +16227,6 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17069,7 +17053,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 71c81b97..1d92e762 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@types/express": "4.17.21", "@types/js-yaml": "4.0.5", "@types/mocha": "^9.1.1", - "@types/node": "^24.0.0", + "@types/node": "^24.12.2", "@types/pg": "^8.6.5", "@types/ramda": "^0.28.13", "@types/sinon": "^10.0.11", diff --git a/src/@types/adapters.ts b/src/@types/adapters.ts index 0e491c3a..b8c06eab 100644 --- a/src/@types/adapters.ts +++ b/src/@types/adapters.ts @@ -21,7 +21,7 @@ export type IWebSocketAdapter = EventEmitter & { export interface ICacheAdapter { getKey(key: string): Promise hasKey(key: string): Promise - setKey(key: string, value: string): Promise + setKey(key: string, value: string, expirySeconds?: number): Promise addToSortedSet(key: string, set: Record | Record[]): Promise removeRangeByScoreFromSortedSet(key: string, min: number, max: number): Promise getRangeFromSortedSet(key: string, start: number, stop: number): Promise diff --git a/src/adapters/redis-adapter.ts b/src/adapters/redis-adapter.ts index fcb5a39e..e2df49ba 100644 --- a/src/adapters/redis-adapter.ts +++ b/src/adapters/redis-adapter.ts @@ -42,7 +42,7 @@ export class RedisAdapter implements ICacheAdapter { } private onClientError(error: Error) { - console.error('Unable to connect to Redis.', error) + debug('Unable to connect to Redis.', error) // throw error } @@ -58,9 +58,12 @@ export class RedisAdapter implements ICacheAdapter { return this.client.get(key) } - public async setKey(key: string, value: string): Promise { + public async setKey(key: string, value: string, expirySeconds?: number): Promise { await this.connection - debug('get %s key', key) + debug('set %s key', key) + if (typeof expirySeconds === 'number') { + return 'OK' === await this.client.set(key, value, { EX: expirySeconds }) + } return 'OK' === await this.client.set(key, value) } diff --git a/src/constants/caching.ts b/src/constants/caching.ts new file mode 100644 index 00000000..7bd6e556 --- /dev/null +++ b/src/constants/caching.ts @@ -0,0 +1,5 @@ +export enum CacheAdmissionState { + ADMITTED = 'admitted', + BLOCKED_NOT_ADMITTED = 'blocked_not_admitted', + BLOCKED_INSUFFICIENT_BALANCE = 'blocked_insufficient_balance', +} diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index e26d0511..b059875b 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -1,13 +1,23 @@ +import { ICacheAdapter, IWebSocketAdapter } from '../@types/adapters' import { IEventRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories' import { IncomingMessage, MessageType } from '../@types/messages' import { createSettings } from './settings-factory' import { EventMessageHandler } from '../handlers/event-message-handler' import { eventStrategyFactory } from './event-strategy-factory' -import { IWebSocketAdapter } from '../@types/adapters' +import { getCacheClient } from '../cache/client' +import { RedisAdapter } from '../adapters/redis-adapter' import { slidingWindowRateLimiterFactory } from './rate-limiter-factory' import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler' import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler' +let cacheAdapter: ICacheAdapter | undefined = undefined +const getCache = (): ICacheAdapter => { + if (!cacheAdapter) { + cacheAdapter = new RedisAdapter(getCacheClient()) + } + return cacheAdapter +} + export const messageHandlerFactory = ( eventRepository: IEventRepository, userRepository: IUserRepository, @@ -24,6 +34,7 @@ export const messageHandlerFactory = ( createSettings, slidingWindowRateLimiterFactory, nip05VerificationRepository, + getCache(), ) } case MessageType.REQ: diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 73e7970d..7582d984 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -26,9 +26,11 @@ import { } from '../utils/event' import { IEventRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories' import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' +import { CacheAdmissionState } from '../constants/caching' import { createCommandResult } from '../utils/messages' import { createLogger } from '../factories/logger-factory' import { Factory } from '../@types/base' +import { ICacheAdapter } from '../@types/adapters' import { IncomingEventMessage } from '../@types/messages' import { IRateLimiter } from '../@types/utils' import { IWebSocketAdapter } from '../@types/adapters' @@ -46,6 +48,7 @@ export class EventMessageHandler implements IMessageHandler { private readonly settings: () => Settings, private readonly slidingWindowRateLimiter: Factory, private readonly nip05VerificationRepository: INip05VerificationRepository, + private readonly cache: ICacheAdapter, ) {} public async handleMessage(message: IncomingEventMessage): Promise { @@ -112,8 +115,7 @@ export class EventMessageHandler implements IMessageHandler { try { await strategy.execute(event) this.processNip05Metadata(event) - } catch (error) { - console.error('error handling message', message, error) + } catch (_error) { this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event')) } } @@ -341,17 +343,44 @@ export class EventMessageHandler implements IMessageHandler { return } - // const hasKey = await this.cache.hasKey(`${event.pubkey}:is-admitted`) - // TODO: use cache + const cacheKey = `${event.pubkey}:is-admitted` + + try { + const cachedValue = await this.cache.getKey(cacheKey) + if (cachedValue === CacheAdmissionState.ADMITTED) { + debug('cache hit for %s admission: admitted', event.pubkey) + return + } + if (cachedValue === CacheAdmissionState.BLOCKED_NOT_ADMITTED) { + debug('cache hit for %s admission: blocked', event.pubkey) + return 'blocked: pubkey not admitted' + } + if (cachedValue === CacheAdmissionState.BLOCKED_INSUFFICIENT_BALANCE) { + debug('cache hit for %s admission: insufficient balance', event.pubkey) + return 'blocked: insufficient balance' + } + } catch (error) { + debug('cache error for %s: %o', event.pubkey, error) + } + const user = await this.userRepository.findByPubkey(event.pubkey) if (!user || !user.isAdmitted) { + this.cacheSet(cacheKey, CacheAdmissionState.BLOCKED_NOT_ADMITTED, 60) return 'blocked: pubkey not admitted' } const minBalance = currentSettings.limits?.event?.pubkey?.minBalance ?? 0n if (minBalance > 0n && user.balance < minBalance) { + this.cacheSet(cacheKey, CacheAdmissionState.BLOCKED_INSUFFICIENT_BALANCE, 60) return 'blocked: insufficient balance' } + + this.cacheSet(cacheKey, CacheAdmissionState.ADMITTED, 300) + } + + private cacheSet(key: string, value: string, ttl: number): void { + this.cache.setKey(key, value, ttl) + .catch((error) => debug('unable to cache %s: %o', key, error)) } protected addExpirationMetadata(event: Event): Event | ExpiringEvent { diff --git a/src/utils/nip44.ts b/src/utils/nip44.ts index dd7edfde..3d7b6b11 100644 --- a/src/utils/nip44.ts +++ b/src/utils/nip44.ts @@ -45,8 +45,12 @@ function getMessageKeys( conversationKey: Buffer, nonce: Buffer, ): { chachaKey: Buffer; chachaNonce: Buffer; hmacKey: Buffer } { - if (conversationKey.length !== 32) throw new Error('invalid conversation_key length') - if (nonce.length !== 32) throw new Error('invalid nonce length') + if (conversationKey.length !== 32) { + throw new Error('invalid conversation_key length') + } + if (nonce.length !== 32) { + throw new Error('invalid nonce length') + } const keys = hkdfExpand(conversationKey, nonce, 76) return { @@ -57,7 +61,9 @@ function getMessageKeys( } function calcPaddedLen(unpaddedLen: number): number { - if (unpaddedLen <= 32) return 32 + if (unpaddedLen <= 32) { + return 32 + } const nextPower = 1 << (Math.floor(Math.log2(unpaddedLen - 1)) + 1) const chunk = nextPower <= 256 ? 32 : nextPower / 8 return chunk * (Math.floor((unpaddedLen - 1) / chunk) + 1) @@ -116,14 +122,22 @@ export function nip44Encrypt( * Validates version byte, payload sizes, and MAC before decrypting. */ export function nip44Decrypt(payload: string, conversationKey: Buffer): string { - if (!payload || payload[0] === '#') throw new Error('unknown version') - if (payload.length < 132 || payload.length > 87472) throw new Error('invalid payload size') + if (!payload || payload[0] === '#') { + throw new Error('unknown version') + } + if (payload.length < 132 || payload.length > 87472) { + throw new Error('invalid payload size') + } const data = Buffer.from(payload, 'base64') - if (data.length < 99 || data.length > 65603) throw new Error('invalid data size') + if (data.length < 99 || data.length > 65603) { + throw new Error('invalid data size') + } const version = data[0] - if (version !== 2) throw new Error(`unknown version ${version}`) + if (version !== 2) { + throw new Error(`unknown version ${version}`) + } const nonce = data.subarray(1, 33) const ciphertext = data.subarray(33, data.length - 32) @@ -132,7 +146,9 @@ export function nip44Decrypt(payload: string, conversationKey: Buffer): string { const { chachaKey, chachaNonce, hmacKey } = getMessageKeys(conversationKey, nonce) const expectedMac = createHmac('sha256', hmacKey).update(nonce).update(ciphertext).digest() - if (!timingSafeEqual(expectedMac, mac)) throw new Error('invalid MAC') + if (!timingSafeEqual(expectedMac, mac)) { + throw new Error('invalid MAC') + } const iv = Buffer.concat([Buffer.alloc(4), chachaNonce]) const decipher = createDecipheriv('chacha20', chachaKey, iv) @@ -148,8 +164,12 @@ export function nip44Decrypt(payload: string, conversationKey: Buffer): string { const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/ export function validateNip44Payload(payload: string): string | undefined { - if (!payload || payload[0] === '#') return 'unsupported encryption version' - if (payload.length < 132 || payload.length > 87472) return 'invalid payload size' + if (!payload || payload[0] === '#') { + return 'unsupported encryption version' + } + if (payload.length < 132 || payload.length > 87472) { + return 'invalid payload size' + } if (payload.length % 4 !== 0 || !BASE64_RE.test(payload)) { return 'payload is not valid base64' @@ -157,8 +177,12 @@ export function validateNip44Payload(payload: string): string | undefined { const data = Buffer.from(payload, 'base64') - if (data.length < 99 || data.length > 65603) return 'invalid decoded payload size' - if (data[0] !== 2) return `unsupported encryption version ${data[0]}` + if (data.length < 99 || data.length > 65603) { + return 'invalid decoded payload size' + } + if (data[0] !== 2) { + return `unsupported encryption version ${data[0]}` + } return undefined } diff --git a/test/unit/factories/message-handler-factory.spec.ts b/test/unit/factories/message-handler-factory.spec.ts index ea19038f..65fddd05 100644 --- a/test/unit/factories/message-handler-factory.spec.ts +++ b/test/unit/factories/message-handler-factory.spec.ts @@ -8,6 +8,8 @@ import { IWebSocketAdapter } from '../../../src/@types/adapters' import { messageHandlerFactory } from '../../../src/factories/message-handler-factory' import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler' import { UnsubscribeMessageHandler } from '../../../src/handlers/unsubscribe-message-handler' +import * as cacheModule from '../../../src/cache/client' +import sinon from 'sinon' describe('messageHandlerFactory', () => { let event: Event @@ -17,8 +19,16 @@ describe('messageHandlerFactory', () => { let message: IncomingMessage let adapter: IWebSocketAdapter let factory + let sandbox: sinon.SinonSandbox beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.stub(cacheModule, 'getCacheClient').returns({ + connect: async () => {}, + on: function() { return this }, + once: function() { return this }, + removeListener: function() { return this } + } as any) eventRepository = {} as any userRepository = {} as any nip05VerificationRepository = {} as any @@ -29,6 +39,10 @@ describe('messageHandlerFactory', () => { factory = messageHandlerFactory(eventRepository, userRepository, nip05VerificationRepository) }) + afterEach(() => { + sandbox.restore() + }) + it('returns EventMessageHandler when given an EVENT message', () => { message = [ MessageType.EVENT, diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index 33a17b2c..0b124639 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -11,6 +11,7 @@ chai.use(chaiAsPromised) import { EventLimits, Settings } from '../../../src/@types/settings' import { identifyEvent, signEvent } from '../../../src/utils/event' import { IncomingEventMessage, MessageType } from '../../../src/@types/messages' +import { CacheAdmissionState } from '../../../src/constants/caching' import { Event } from '../../../src/@types/event' import { EventKinds } from '../../../src/constants/base' import { EventMessageHandler } from '../../../src/handlers/event-message-handler' @@ -99,6 +100,7 @@ describe('EventMessageHandler', () => { }) as any, () => ({ hit: async () => false }), {} as any, + { hasKey: async () => false, setKey: async () => true } as any, ) }) @@ -274,6 +276,7 @@ describe('EventMessageHandler', () => { () => settings, () => ({ hit: async () => false }), {} as any, + { hasKey: async () => false, setKey: async () => true } as any, ) }) @@ -804,6 +807,7 @@ describe('EventMessageHandler', () => { () => settings, () => ({ hit: rateLimiterHitStub }), {} as any, + { hasKey: async () => false, setKey: async () => true } as any, ) }) @@ -1018,6 +1022,7 @@ describe('EventMessageHandler', () => { let webSocket: IWebSocketAdapter let getRelayPublicKeyStub: SinonStub let userRepositoryFindByPubkeyStub: SinonStub + let cacheStub: any beforeEach(() => { settings = { @@ -1066,6 +1071,11 @@ describe('EventMessageHandler', () => { findByPubkey: userRepositoryFindByPubkeyStub, isVanished: async () => false, } as any + cacheStub = { + hasKey: sandbox.stub().resolves(false), + getKey: sandbox.stub().resolves(null), + setKey: sandbox.stub().resolves(true), + } handler = new EventMessageHandler( webSocket, () => null, @@ -1074,6 +1084,7 @@ describe('EventMessageHandler', () => { () => settings, () => ({ hit: async () => false }), {} as any, + cacheStub, ) }) @@ -1175,6 +1186,52 @@ describe('EventMessageHandler', () => { return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined }) + + describe('caching', () => { + it('fulfills with undefined and uses cache hit for admitted user without hitting DB', async () => { + cacheStub.getKey.resolves(CacheAdmissionState.ADMITTED) + + await expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + expect(userRepositoryFindByPubkeyStub).not.to.have.been.called + }) + + it('fulfills with reason and uses cache hit for blocked user without hitting DB', async () => { + cacheStub.getKey.resolves(CacheAdmissionState.BLOCKED_NOT_ADMITTED) + + await expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') + expect(userRepositoryFindByPubkeyStub).not.to.have.been.called + }) + + it('fulfills with reason and uses cache hit for insufficient balance without hitting DB', async () => { + cacheStub.getKey.resolves(CacheAdmissionState.BLOCKED_INSUFFICIENT_BALANCE) + + await expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: insufficient balance') + expect(userRepositoryFindByPubkeyStub).not.to.have.been.called + }) + + it('caches blocked status with 60s ttl when user is not found', async () => { + userRepositoryFindByPubkeyStub.resolves(undefined) + + await (handler as any).isUserAdmitted(event) + expect(cacheStub.setKey).to.have.been.calledWith(`${event.pubkey}:is-admitted`, CacheAdmissionState.BLOCKED_NOT_ADMITTED, 60) + }) + + it('caches insufficient balance status with 60s ttl when user balance is too low', async () => { + settings.limits.event.pubkey.minBalance = 100n + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, balance: 50n }) + + await (handler as any).isUserAdmitted(event) + expect(cacheStub.setKey).to.have.been.calledWith(`${event.pubkey}:is-admitted`, CacheAdmissionState.BLOCKED_INSUFFICIENT_BALANCE, 60) + }) + + it('caches admitted status with 300s ttl when user is admitted and has balance', async () => { + settings.limits.event.pubkey.minBalance = 100n + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, balance: 150n }) + + await (handler as any).isUserAdmitted(event) + expect(cacheStub.setKey).to.have.been.calledWith(`${event.pubkey}:is-admitted`, CacheAdmissionState.ADMITTED, 300) + }) + }) }) describe('checkNip05Verification', () => { @@ -1220,7 +1277,8 @@ describe('EventMessageHandler', () => { () => settings, () => ({ hit: async () => false }), nip05VerificationRepository, - ) + { hasKey: async () => false, setKey: async () => true, getKey: async () => null } as any, +) }) it('returns undefined if nip05 settings are not set', async () => { @@ -1389,7 +1447,409 @@ describe('EventMessageHandler', () => { () => settings, () => ({ hit: async () => false }), nip05VerificationRepository, - ) + { hasKey: async () => false, setKey: async () => true, getKey: async () => null } as any, +) + }) + + it('does nothing when nip05 settings are undefined', async () => { + settings.nip05 = undefined + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).not.to.have.been.called + }) + + it('does nothing when nip05 mode is disabled', async () => { + settings.nip05.mode = 'disabled' + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).not.to.have.been.called + }) + + it('does nothing for non-kind-0 events', async () => { + event.kind = EventKinds.TEXT_NOTE + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).not.to.have.been.called + }) + + it('deletes verification when kind-0 has no nip05 in content', async () => { + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ name: 'alice' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(nip05VerificationRepository.deleteByPubkey).to.have.been.calledOnceWithExactly(event.pubkey) + expect(verifyStub).not.to.have.been.called + }) + + it('does nothing when nip05 identifier is unparseable', async () => { + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'invalid-no-at-sign' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).not.to.have.been.called + expect(nip05VerificationRepository.deleteByPubkey).not.to.have.been.called + }) + + it('does nothing when domain is not allowed', async () => { + settings.nip05.domainBlacklist = ['blocked.com'] + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@blocked.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).not.to.have.been.called + }) + + it('verifies and upserts on successful verification', async () => { + nip05VerificationRepository.findByPubkey.resolves(undefined) + verifyStub.resolves({ status: 'verified' }) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).to.have.been.calledOnceWithExactly('alice@example.com', event.pubkey) + expect(nip05VerificationRepository.upsert).to.have.been.calledOnce + + const upsertArg = nip05VerificationRepository.upsert.firstCall.args[0] + expect(upsertArg.pubkey).to.equal(event.pubkey) + expect(upsertArg.nip05).to.equal('alice@example.com') + expect(upsertArg.domain).to.equal('example.com') + expect(upsertArg.isVerified).to.be.true + expect(upsertArg.failureCount).to.equal(0) + expect(upsertArg.lastVerifiedAt).to.be.an.instanceOf(Date) + }) + + it('upserts with unverified state and nulls lastVerifiedAt on definitive mismatch', async () => { + nip05VerificationRepository.findByPubkey.resolves(undefined) + verifyStub.resolves({ status: 'mismatch' }) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).to.have.been.calledOnce + expect(nip05VerificationRepository.upsert).to.have.been.calledOnce + + const upsertArg = nip05VerificationRepository.upsert.firstCall.args[0] + expect(upsertArg.isVerified).to.be.false + expect(upsertArg.failureCount).to.equal(1) + expect(upsertArg.lastVerifiedAt).to.be.null + }) + + it('increments failureCount from existing row on definitive mismatch', async () => { + const priorVerifiedAt = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + pubkey: event.pubkey, + nip05: 'alice@example.com', + domain: 'example.com', + isVerified: true, + lastVerifiedAt: priorVerifiedAt, + lastCheckedAt: priorVerifiedAt, + failureCount: 2, + createdAt: priorVerifiedAt, + updatedAt: priorVerifiedAt, + }) + verifyStub.resolves({ status: 'mismatch' }) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + const upsertArg = nip05VerificationRepository.upsert.firstCall.args[0] + expect(upsertArg.failureCount).to.equal(3) + expect(upsertArg.isVerified).to.be.false + expect(upsertArg.lastVerifiedAt).to.be.null + }) + + it('preserves prior isVerified/lastVerifiedAt on transient error', async () => { + const priorVerifiedAt = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + pubkey: event.pubkey, + nip05: 'alice@example.com', + domain: 'example.com', + isVerified: true, + lastVerifiedAt: priorVerifiedAt, + lastCheckedAt: priorVerifiedAt, + failureCount: 1, + createdAt: priorVerifiedAt, + updatedAt: priorVerifiedAt, + }) + verifyStub.resolves({ status: 'error', reason: 'ETIMEDOUT' }) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + const upsertArg = nip05VerificationRepository.upsert.firstCall.args[0] + expect(upsertArg.isVerified).to.be.true + expect(upsertArg.lastVerifiedAt).to.equal(priorVerifiedAt) + expect(upsertArg.failureCount).to.equal(2) + expect(upsertArg.lastCheckedAt).to.be.an.instanceOf(Date) + }) + + it('handles verification errors gracefully (thrown by verifier)', async () => { + nip05VerificationRepository.findByPubkey.resolves(undefined) + verifyStub.rejects(new Error('network error')) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(nip05VerificationRepository.upsert).not.to.have.been.called + }) + + it('works correctly in passive mode', async () => { + settings.nip05.mode = 'passive' + nip05VerificationRepository.findByPubkey.resolves(undefined) + verifyStub.resolves({ status: 'verified' }) + event.kind = EventKinds.SET_METADATA + event.content = JSON.stringify({ nip05: 'alice@example.com' }) + + ;(handler as any).processNip05Metadata(event) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(verifyStub).to.have.been.calledOnce + expect(nip05VerificationRepository.upsert).to.have.been.calledOnce + }) + }) + + describe('checkNip05Verification', () => { + let settings: Settings + let nip05VerificationRepository: any + let getRelayPublicKeyStub: Sinon.SinonStub + + beforeEach(() => { + settings = { + info: { + relay_url: 'relay_url', + }, + nip05: { + mode: 'enabled', + verifyExpiration: 86400000, + verifyUpdateFrequency: 3600000, + maxConsecutiveFailures: 10, + domainWhitelist: [], + domainBlacklist: [], + }, + } as any + event = { + content: 'hello', + created_at: 1665546189, + id: 'f'.repeat(64), + kind: 1, + pubkey: 'f'.repeat(64), + sig: 'f'.repeat(128), + tags: [], + } + nip05VerificationRepository = { + findByPubkey: sandbox.stub(), + upsert: sandbox.stub(), + deleteByPubkey: sandbox.stub(), + findPendingVerifications: sandbox.stub(), + } + getRelayPublicKeyStub = sandbox.stub(EventMessageHandler.prototype, 'getRelayPublicKey' as any) + handler = new EventMessageHandler( + {} as any, + () => null, + { hasActiveRequestToVanish: async () => false } as any, + userRepository, + () => settings, + () => ({ hit: async () => false }), + nip05VerificationRepository, + { hasKey: async () => false, setKey: async () => true, getKey: async () => null } as any, +) + }) + + it('returns undefined if nip05 settings are not set', async () => { + settings.nip05 = undefined + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns undefined if nip05 mode is disabled', async () => { + settings.nip05.mode = 'disabled' + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns undefined if nip05 mode is passive', async () => { + settings.nip05.mode = 'passive' + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns undefined for kind 0 events (SET_METADATA)', async () => { + event.kind = 0 + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns undefined if event pubkey equals relay public key', async () => { + getRelayPublicKeyStub.returns(event.pubkey) + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns reason if no verification found for pubkey', async () => { + nip05VerificationRepository.findByPubkey.resolves(undefined) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 verification required') + }) + + it('returns reason if verification exists but has no lastVerifiedAt', async () => { + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: false, + lastVerifiedAt: null, + domain: 'example.com', + }) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 verification required') + }) + + it('treats isVerified=true with null lastVerifiedAt as unverified (historical/bad data)', async () => { + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: null, + domain: 'example.com', + }) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 verification required') + }) + + it('returns reason if verification is expired', async () => { + const expired = new Date(Date.now() - 86400001) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: expired, + domain: 'example.com', + }) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 verification expired') + }) + + it('returns undefined if verification is valid and not expired', async () => { + const recent = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: recent, + domain: 'example.com', + }) + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('allows author when lastVerifiedAt is recent even if isVerified is false (transient re-check failure)', async () => { + const recent = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: false, + lastVerifiedAt: recent, + domain: 'example.com', + }) + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + + it('returns reason if domain is blacklisted', async () => { + settings.nip05.domainBlacklist = ['spam.com'] + const recent = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: recent, + domain: 'spam.com', + }) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 domain not allowed') + }) + + it('returns reason if domain is not in whitelist', async () => { + settings.nip05.domainWhitelist = ['allowed.com'] + const recent = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: recent, + domain: 'other.com', + }) + + return expect((handler as any).checkNip05Verification(event)) + .to.eventually.equal('blocked: NIP-05 domain not allowed') + }) + + it('returns undefined if domain is in whitelist', async () => { + settings.nip05.domainWhitelist = ['allowed.com'] + const recent = new Date(Date.now() - 1000) + nip05VerificationRepository.findByPubkey.resolves({ + isVerified: true, + lastVerifiedAt: recent, + domain: 'allowed.com', + }) + + return expect((handler as any).checkNip05Verification(event)).to.eventually.be.undefined + }) + }) + + describe('processNip05Metadata', () => { + let settings: Settings + let nip05VerificationRepository: any + let verifyStub: Sinon.SinonStub + + beforeEach(() => { + settings = { + info: { + relay_url: 'relay_url', + }, + nip05: { + mode: 'enabled', + verifyExpiration: 86400000, + verifyUpdateFrequency: 3600000, + maxConsecutiveFailures: 10, + domainWhitelist: [], + domainBlacklist: [], + }, + } as any + nip05VerificationRepository = { + findByPubkey: sandbox.stub(), + upsert: sandbox.stub().resolves(1), + deleteByPubkey: sandbox.stub().resolves(1), + findPendingVerifications: sandbox.stub(), + } + verifyStub = sandbox.stub(nip05Utils, 'verifyNip05Identifier') + handler = new EventMessageHandler( + {} as any, + () => null, + { hasActiveRequestToVanish: async () => false } as any, + userRepository, + () => settings, + () => ({ hit: async () => false }), + nip05VerificationRepository, + { hasKey: async () => false, setKey: async () => true, getKey: async () => null } as any, +) }) it('does nothing when nip05 settings are undefined', async () => { From 90e995e5b706779e87f4cfc170fe929211ec1e70 Mon Sep 17 00:00:00 2001 From: Mohit Davar Date: Sat, 18 Apr 2026 17:21:03 +0000 Subject: [PATCH 2/2] style(format): apply project formatting --- src/@types/adapters.ts | 1 - src/@types/base.ts | 14 +- src/@types/cache.ts | 7 +- src/@types/database.ts | 2 +- src/@types/event.ts | 7 +- src/@types/invoice.ts | 4 +- src/@types/messages.ts | 19 +- src/@types/repositories.ts | 24 +- src/@types/services.ts | 10 +- src/@types/settings.ts | 2 +- src/@types/utils.ts | 4 +- src/adapters/redis-adapter.ts | 16 +- src/adapters/web-server-adapter.ts | 4 +- src/adapters/web-socket-adapter.ts | 34 +- src/adapters/web-socket-server-adapter.ts | 3 +- src/app/app.ts | 31 +- src/app/maintenance-worker.ts | 8 +- src/app/static-mirroring-worker.ts | 699 ++++++----- src/app/worker.ts | 6 +- src/cache/client.ts | 5 +- src/clean-db.ts | 18 +- src/constants/adapter.ts | 4 +- .../get-admission-check-controller.ts | 16 +- .../callbacks/lnbits-callback-controller.ts | 56 +- .../callbacks/nodeless-callback-controller.ts | 45 +- .../callbacks/opennode-callback-controller.ts | 28 +- .../callbacks/zebedee-callback-controller.ts | 38 +- .../invoices/get-invoice-controller.ts | 15 +- .../invoices/get-invoice-status-controller.ts | 22 +- .../invoices/post-invoice-controller.ts | 80 +- src/database/client.ts | 96 +- src/database/transaction.ts | 8 +- .../get-admission-check-controller-factory.ts | 8 +- .../lnbits-callback-controller-factory.ts | 5 +- .../nodeless-callback-controller-factory.ts | 5 +- .../opennode-callback-controller-factory.ts | 4 +- .../post-invoice-controller-factory.ts | 7 +- .../zebedee-callback-controller-factory.ts | 4 +- src/factories/event-strategy-factory.ts | 18 +- src/factories/logger-factory.ts | 2 +- src/factories/maintenance-service-factory.ts | 5 +- src/factories/message-handler-factory.ts | 31 +- .../lnbits-payments-processor-factory.ts | 1 - .../lnurl-payments-processor-factory.ts | 4 +- .../nodeless-payments-processor-factory.ts | 4 +- .../opennode-payments-processor-factory.ts | 2 +- .../zebedee-payments-processor-factory.ts | 6 +- src/factories/payments-service-factory.ts | 2 +- .../static-mirroring.worker-factory.ts | 7 +- src/factories/web-app-factory.ts | 12 +- src/factories/websocket-adapter-factory.ts | 13 +- src/handlers/event-message-handler.ts | 135 +- .../default-event-strategy.ts | 4 +- .../event-strategies/delete-event-strategy.ts | 20 +- .../ephemeral-event-strategy.ts | 9 +- .../gift-wrap-event-strategy.ts | 130 +- ...arameterized-replaceable-event-strategy.ts | 12 +- .../replaceable-event-strategy.ts | 12 +- .../event-strategies/vanish-event-strategy.ts | 12 +- .../get-privacy-request-handler.ts | 4 +- .../get-terms-request-handler.ts | 6 +- .../request-handlers/nodeinfo-handler.ts | 69 +- .../rate-limiter-middleware.ts | 11 +- .../request-handlers/root-request-handler.ts | 56 +- .../with-controller-request-handler.ts | 19 +- src/handlers/subscribe-message-handler.ts | 25 +- src/handlers/unsubscribe-message-handler.ts | 4 +- .../lnbits-payment-processor.ts | 29 +- .../lnurl-payments-processor.ts | 12 +- .../nodeless-payments-processor.ts | 8 +- .../opennode-payments-processor.ts | 8 +- .../zebedee-payments-processor.ts | 8 +- src/repositories/event-repository.ts | 165 +-- src/repositories/invoice-repository.ts | 59 +- .../nip05-verification-repository.ts | 29 +- src/repositories/user-repository.ts | 76 +- src/routes/admissions/index.ts | 5 +- src/routes/callbacks/index.ts | 14 +- src/routes/invoices/index.ts | 2 +- src/schemas/base-schema.ts | 9 +- src/schemas/event-schema.ts | 32 +- src/schemas/filter-schema.ts | 37 +- src/schemas/lnbits-callback-schema.ts | 16 +- src/schemas/message-schema.ts | 60 +- src/schemas/nodeless-callback-schema.ts | 28 +- src/schemas/opennode-callback-schema.ts | 42 +- src/schemas/zebedee-callback-schema.ts | 32 +- src/scripts/export-events.ts | 13 +- src/services/event-import-service.ts | 115 +- src/services/maintenance-service.ts | 4 +- src/services/payments-service.ts | 60 +- src/tor/client.ts | 20 +- src/utils/event.ts | 135 +- src/utils/html.ts | 6 +- src/utils/messages.ts | 27 +- src/utils/misc.ts | 7 +- src/utils/nip05.ts | 24 +- src/utils/nip44.ts | 368 +++--- src/utils/proof-of-work.ts | 2 +- src/utils/secret.ts | 4 +- src/utils/settings.ts | 25 +- src/utils/sliding-window-rate-limiter.ts | 14 +- src/utils/stream.ts | 62 +- src/utils/transform.ts | 78 +- test/integration/features/helpers.ts | 27 +- .../features/nip-01/nip-01.feature.ts | 288 ++--- .../features/nip-09/nip-09.feature.ts | 31 +- .../features/nip-16/nip-16.feature.ts | 145 ++- .../features/nip-28/nip-28.feature.ts | 62 +- .../features/nip-33/nip-33.feature.ts | 262 ++-- test/integration/features/shared.ts | 59 +- test/unit/adapters/redis-adapter.spec.ts | 2 +- test/unit/adapters/web-server-adapter.spec.ts | 16 +- test/unit/adapters/web-socket-adapter.spec.ts | 62 +- .../web-socket-server-adapter.spec.ts | 63 +- test/unit/app/maintenance-worker.spec.ts | 32 +- test/unit/clean-db.spec.ts | 39 +- .../lnbits-callback-controller.spec.ts | 45 +- .../nodeless-callback-controller.spec.ts | 35 +- .../opennode-callback-controller.spec.ts | 24 +- .../zebedee-callback-controller.spec.ts | 24 +- .../invoices/post-invoice-controller.spec.ts | 31 +- test/unit/data/events.ts | 1118 ++++------------- .../factories/message-handler-factory.spec.ts | 32 +- test/unit/factories/settings-factory.spec.ts | 4 +- .../websocket-adapter-factory.spec.ts | 8 +- test/unit/factories/worker-factory.spec.ts | 5 +- .../handlers/event-message-handler.spec.ts | 346 +++-- .../default-event-strategy.spec.ts | 25 +- .../delete-event-strategy.spec.ts | 36 +- .../ephemeral-event-strategy.spec.ts | 20 +- .../gift-wrap-event-strategy.spec.ts | 85 +- ...terized-replaceable-event-strategy.spec.ts | 41 +- .../replaceable-event-strategy.spec.ts | 35 +- .../vanish-event-strategy.spec.ts | 29 +- .../root-request-handler.spec.ts | 6 +- .../subscribe-message-handler.spec.ts | 71 +- .../repositories/event-repository.spec.ts | 205 +-- test/unit/schemas/event-schema.spec.ts | 63 +- test/unit/schemas/filter-schema.spec.ts | 54 +- test/unit/schemas/message-schema.spec.ts | 11 +- .../services/event-import-service.spec.ts | 12 +- .../unit/services/maintenance-service.spec.ts | 9 +- test/unit/tor/onion.spec.ts | 365 +++--- test/unit/utils/event.spec.ts | 144 +-- test/unit/utils/html.spec.ts | 2 +- test/unit/utils/http.spec.ts | 21 +- test/unit/utils/messages.spec.ts | 20 +- test/unit/utils/nip05.spec.ts | 8 +- test/unit/utils/nip44.spec.ts | 7 +- test/unit/utils/proof-of-work.spec.ts | 22 +- test/unit/utils/settings.spec.ts | 55 +- .../utils/sliding-window-rate-limiter.spec.ts | 12 +- test/unit/utils/stream.spec.ts | 2 +- test/unit/utils/template-cache.spec.ts | 6 +- test/unit/utils/transform.spec.ts | 3 +- 156 files changed, 3248 insertions(+), 4510 deletions(-) diff --git a/src/@types/adapters.ts b/src/@types/adapters.ts index b8c06eab..c346adb0 100644 --- a/src/@types/adapters.ts +++ b/src/@types/adapters.ts @@ -11,7 +11,6 @@ export interface IWebServerAdapter extends EventEmitter { close(callback?: () => void): void } - export type IWebSocketAdapter = EventEmitter & { getClientId(): string getClientAddress(): string diff --git a/src/@types/base.ts b/src/@types/base.ts index 78808cf5..c10e7891 100644 --- a/src/@types/base.ts +++ b/src/@types/base.ts @@ -16,21 +16,15 @@ type ExtraTagValues = { } export interface TagBase extends ExtraTagValues { - 0: TagName; + 0: TagName 1: string } -type Enumerate< - N extends number, - Acc extends number[] = [], -> = Acc['length'] extends N +type Enumerate = Acc['length'] extends N ? Acc[number] : Enumerate -export type Range = Exclude< - Enumerate, - Enumerate -> +export type Range = Exclude, Enumerate> export type Factory = (input: TInput) => TOutput @@ -45,4 +39,4 @@ export interface ContextMetadata { export interface IRunnable { run(): void close(callback?: (...args: any[]) => void): void -} \ No newline at end of file +} diff --git a/src/@types/cache.ts b/src/@types/cache.ts index 2c224546..93276c6d 100644 --- a/src/@types/cache.ts +++ b/src/@types/cache.ts @@ -1,8 +1,3 @@ -import { - RedisClientType, - RedisFunctions, - RedisModules, - RedisScripts, -} from 'redis' +import { RedisClientType, RedisFunctions, RedisModules, RedisScripts } from 'redis' export type CacheClient = RedisClientType diff --git a/src/@types/database.ts b/src/@types/database.ts index 234d55af..ff39d0b8 100644 --- a/src/@types/database.ts +++ b/src/@types/database.ts @@ -2,7 +2,7 @@ import { DatabaseTransaction } from './base' export interface ITransaction { begin(): Promise - get transaction (): DatabaseTransaction + get transaction(): DatabaseTransaction commit(): Promise rollback(): Promise } diff --git a/src/@types/event.ts b/src/@types/event.ts index cc9241d4..dee845c7 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -1,5 +1,10 @@ import { ContextMetadata, EventId, Pubkey, Tag } from './base' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' +import { + ContextMetadataKey, + EventDeduplicationMetadataKey, + EventExpirationTimeMetadataKey, + EventKinds, +} from '../constants/base' export interface BaseEvent { id: EventId diff --git a/src/@types/invoice.ts b/src/@types/invoice.ts index 1644bcf2..3b94ab99 100644 --- a/src/@types/invoice.ts +++ b/src/@types/invoice.ts @@ -3,7 +3,7 @@ import { Pubkey } from './base' export enum InvoiceUnit { MSATS = 'msats', SATS = 'sats', - BTC = 'btc' + BTC = 'btc', } export enum InvoiceStatus { @@ -39,7 +39,7 @@ export interface DBInvoice { amount_requested: bigint amount_paid: bigint unit: InvoiceUnit - status: InvoiceStatus, + status: InvoiceStatus description: string confirmed_at: Date expires_at: Date diff --git a/src/@types/messages.ts b/src/@types/messages.ts index 63f24b62..c87d04ce 100644 --- a/src/@types/messages.ts +++ b/src/@types/messages.ts @@ -9,23 +9,14 @@ export enum MessageType { CLOSE = 'CLOSE', NOTICE = 'NOTICE', EOSE = 'EOSE', - OK = 'OK' + OK = 'OK', } -export type IncomingMessage = ( - | SubscribeMessage - | IncomingEventMessage - | UnsubscribeMessage - ) & { - [ContextMetadataKey]?: ContextMetadata - } - +export type IncomingMessage = (SubscribeMessage | IncomingEventMessage | UnsubscribeMessage) & { + [ContextMetadataKey]?: ContextMetadata +} -export type OutgoingMessage = - | OutgoingEventMessage - | EndOfStoredEventsNotice - | NoticeMessage - | CommandResult +export type OutgoingMessage = OutgoingEventMessage | EndOfStoredEventsNotice | NoticeMessage | CommandResult export type SubscribeMessage = { [index in Range<2, 100>]: SubscriptionFilter diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index b3ef1dce..23dff30d 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -42,21 +42,9 @@ export interface IEventRepository { export interface IInvoiceRepository { findById(id: string, client?: DatabaseClient): Promise upsert(invoice: Partial, client?: DatabaseClient): Promise - updateStatus( - invoice: Pick, - client?: DatabaseClient, - ): Promise - confirmInvoice( - invoiceId: string, - amountReceived: bigint, - confirmedAt: Date, - client?: DatabaseClient, - ): Promise - findPendingInvoices( - offset?: number, - limit?: number, - client?: DatabaseClient, - ): Promise + updateStatus(invoice: Pick, client?: DatabaseClient): Promise + confirmInvoice(invoiceId: string, amountReceived: bigint, confirmedAt: Date, client?: DatabaseClient): Promise + findPendingInvoices(offset?: number, limit?: number, client?: DatabaseClient): Promise } export interface IUserRepository { @@ -71,10 +59,6 @@ export interface IUserRepository { export interface INip05VerificationRepository { findByPubkey(pubkey: Pubkey): Promise upsert(verification: Nip05Verification): Promise - findPendingVerifications( - updateFrequencyMs: number, - maxFailures: number, - limit: number, - ): Promise + findPendingVerifications(updateFrequencyMs: number, maxFailures: number, limit: number): Promise deleteByPubkey(pubkey: Pubkey): Promise } diff --git a/src/@types/services.ts b/src/@types/services.ts index e9fc6480..5d8ea229 100644 --- a/src/@types/services.ts +++ b/src/@types/services.ts @@ -7,16 +7,10 @@ export interface IMaintenanceService { export interface IPaymentsService { getInvoiceFromPaymentsProcessor(invoice: string | Invoice): Promise> - createInvoice( - pubkey: Pubkey, - amount: bigint, - description: string, - ): Promise + createInvoice(pubkey: Pubkey, amount: bigint, description: string): Promise updateInvoice(invoice: Partial): Promise updateInvoiceStatus(invoice: Pick): Promise - confirmInvoice( - invoice: Pick, - ): Promise + confirmInvoice(invoice: Pick): Promise sendInvoiceUpdateNotification(invoice: Invoice): Promise getPendingInvoices(): Promise } diff --git a/src/@types/settings.ts b/src/@types/settings.ts index 0e4ec4a6..6f2691b0 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -199,7 +199,7 @@ export interface NodelessPaymentsProcessor { } export interface PaymentsProcessors { - lnurl?: LnurlPaymentsProcessor, + lnurl?: LnurlPaymentsProcessor zebedee?: ZebedeePaymentsProcessor lnbits?: LNbitsPaymentsProcessor nodeless?: NodelessPaymentsProcessor diff --git a/src/@types/utils.ts b/src/@types/utils.ts index 1548deb6..5e867422 100644 --- a/src/@types/utils.ts +++ b/src/@types/utils.ts @@ -1,6 +1,6 @@ export interface IRateLimiterOptions { - period: number; - rate: number; + period: number + rate: number } export interface IRateLimiter { diff --git a/src/adapters/redis-adapter.ts b/src/adapters/redis-adapter.ts index e2df49ba..a3816588 100644 --- a/src/adapters/redis-adapter.ts +++ b/src/adapters/redis-adapter.ts @@ -62,9 +62,9 @@ export class RedisAdapter implements ICacheAdapter { await this.connection debug('set %s key', key) if (typeof expirySeconds === 'number') { - return 'OK' === await this.client.set(key, value, { EX: expirySeconds }) + return 'OK' === (await this.client.set(key, value, { EX: expirySeconds })) } - return 'OK' === await this.client.set(key, value) + return 'OK' === (await this.client.set(key, value)) } public async removeRangeByScoreFromSortedSet(key: string, min: number, max: number): Promise { @@ -85,17 +85,11 @@ export class RedisAdapter implements ICacheAdapter { await this.client.expire(key, expiry) } - public async addToSortedSet( - key: string, - set: Record - ): Promise { + public async addToSortedSet(key: string, set: Record): Promise { await this.connection debug('add %o to sorted set %s', set, key) - const members = Object - .entries(set) - .map(([value, score]) => ({ score: Number(score), value })) + const members = Object.entries(set).map(([value, score]) => ({ score: Number(score), value })) return this.client.zAdd(key, members) } - -} \ No newline at end of file +} diff --git a/src/adapters/web-server-adapter.ts b/src/adapters/web-server-adapter.ts index c56cb473..4e479965 100644 --- a/src/adapters/web-server-adapter.ts +++ b/src/adapters/web-server-adapter.ts @@ -7,9 +7,7 @@ import { IWebServerAdapter } from '../@types/adapters' const debug = createLogger('web-server-adapter') export class WebServerAdapter extends EventEmitter implements IWebServerAdapter { - public constructor( - protected readonly webServer: Server, - ) { + public constructor(protected readonly webServer: Server) { debug('created') super() this.webServer diff --git a/src/adapters/web-socket-adapter.ts b/src/adapters/web-socket-adapter.ts index 123e460a..e436c013 100644 --- a/src/adapters/web-socket-adapter.ts +++ b/src/adapters/web-socket-adapter.ts @@ -22,7 +22,6 @@ import { messageSchema } from '../schemas/message-schema' import { Settings } from '../@types/settings' import { SocketAddress } from 'net' - const debug = createLogger('web-socket-adapter') const debugHeartbeat = debug.extend('heartbeat') @@ -58,7 +57,9 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter this.client .on('error', (error) => { if (error.name === 'RangeError' && error.message === 'Max payload size exceeded') { - console.error(`web-socket-adapter: client ${this.clientId} (${this.getClientAddress()}) sent payload too large`) + console.error( + `web-socket-adapter: client ${this.clientId} (${this.getClientAddress()}) sent payload too large`, + ) } else if (error.name === 'RangeError' && error.message === 'Invalid WebSocket frame: RSV1 must be clear') { debug(`client ${this.clientId} (${this.getClientAddress()}) enabled compression`) } else { @@ -72,8 +73,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter .on('pong', this.onClientPong.bind(this)) .on('ping', this.onClientPing.bind(this)) - this - .on(WebSocketAdapterEvent.Heartbeat, this.onHeartbeat.bind(this)) + this.on(WebSocketAdapterEvent.Heartbeat, this.onHeartbeat.bind(this)) .on(WebSocketAdapterEvent.Subscribe, this.onSubscribed.bind(this)) .on(WebSocketAdapterEvent.Unsubscribe, this.onUnsubscribed.bind(this)) .on(WebSocketAdapterEvent.Event, this.onSendEvent.bind(this)) @@ -113,9 +113,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter public onSendEvent(event: Event): void { this.subscriptions.forEach((filters, subscriptionId) => { - if ( - filters.map(isEventMatchingFilter).some((isMatch) => isMatch(event)) - ) { + if (filters.map(isEventMatchingFilter).some((isMatch) => isMatch(event))) { debug('sending event to client %s: %o', this.clientId, event) this.sendMessage(createOutgoingEventMessage(subscriptionId, event)) } @@ -148,7 +146,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter private async onClientMessage(raw: Buffer) { this.alive = true let abortable = false - let messageHandler: IMessageHandler & IAbortable | undefined = undefined + let messageHandler: (IMessageHandler & IAbortable) | undefined = undefined try { if (await this.isRateLimited(this.clientAddress.address)) { this.sendMessage(createNoticeMessage('rate limited')) @@ -182,9 +180,10 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter console.error(`web-socket-adapter: abort from client ${this.clientId} (${this.getClientAddress()})`) } else if (error.name === 'SyntaxError' || error instanceof ZodError) { debug('invalid message client %s (%s): %s', this.clientId, this.getClientAddress(), error.message) - const notice = error instanceof ZodError - ? `invalid: ${error.issues[0]?.message ?? error.message}` - : `invalid: ${error.message}` + const notice = + error instanceof ZodError + ? `invalid: ${error.issues[0]?.message ?? error.message}` + : `invalid: ${error.message}` this.sendMessage(createNoticeMessage(notice)) } else { console.error('web-socket-adapter: unable to handle message:', error) @@ -206,10 +205,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter } private async isRateLimited(client: string): Promise { - const { - rateLimits, - ipWhitelist = [], - } = this.settings().limits?.message ?? {} + const { rateLimits, ipWhitelist = [] } = this.settings().limits?.message ?? {} if (!Array.isArray(rateLimits) || !rateLimits.length || ipWhitelist.includes(client)) { return false @@ -217,18 +213,12 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter const rateLimiter = this.slidingWindowRateLimiter() - const hit = (period: number, rate: number) => - rateLimiter.hit( - `${client}:message:${period}`, - 1, - { period, rate }, - ) + const hit = (period: number, rate: number) => rateLimiter.hit(`${client}:message:${period}`, 1, { period, rate }) let limited = false for (const { rate, period } of rateLimits) { const isRateLimited = await hit(period, rate) - if (isRateLimited) { debug('rate limited %s: %d messages / %d ms exceeded', client, rate, period) diff --git a/src/adapters/web-socket-server-adapter.ts b/src/adapters/web-socket-server-adapter.ts index 27b3e568..a1246f34 100644 --- a/src/adapters/web-socket-server-adapter.ts +++ b/src/adapters/web-socket-server-adapter.ts @@ -35,8 +35,7 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock this.webSocketsAdapters = new WeakMap() - this - .on(WebSocketServerAdapterEvent.Broadcast, this.onBroadcast.bind(this)) + this.on(WebSocketServerAdapterEvent.Broadcast, this.onBroadcast.bind(this)) this.webSocketServer .on(WebSocketServerAdapterEvent.Connection, this.onConnection.bind(this)) diff --git a/src/app/app.ts b/src/app/app.ts index 44c38b66..864010f5 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -26,12 +26,9 @@ export class App implements IRunnable { this.workers = new WeakMap() - this.cluster - .on('message', this.onClusterMessage.bind(this)) - .on('exit', this.onClusterExit.bind(this)) + this.cluster.on('message', this.onClusterMessage.bind(this)).on('exit', this.onClusterExit.bind(this)) - this.process - .on('SIGTERM', this.onExit.bind(this)) + this.process.on('SIGTERM', this.onExit.bind(this)) debug('started') } @@ -65,7 +62,12 @@ export class App implements IRunnable { logCentered(`Payments provider: ${path(['payments', 'processor'], settings)}`, width) } - if (paymentsEnabled && (typeof this.process.env.SECRET !== 'string' || this.process.env.SECRET === '' || this.process.env.SECRET === 'changeme')) { + if ( + paymentsEnabled && + (typeof this.process.env.SECRET !== 'string' || + this.process.env.SECRET === '' || + this.process.env.SECRET === 'changeme') + ) { console.error('Please configure the secret using the SECRET environment variable.') this.process.exit(1) } @@ -108,11 +110,14 @@ export class App implements IRunnable { debug('settings: %O', settings) const host = `${hostname()}:${port}` - addOnion(torHiddenServicePort, host).then(value=>{ - logCentered(`Tor hidden service: ${value}:${torHiddenServicePort}`, width) - }, () => { - logCentered('Tor hidden service: disabled', width) - }) + addOnion(torHiddenServicePort, host).then( + (value) => { + logCentered(`Tor hidden service: ${value}:${torHiddenServicePort}`, width) + }, + () => { + logCentered('Tor hidden service: disabled', width) + }, + ) } private onClusterMessage(source: Worker, message: Serializable) { @@ -127,7 +132,7 @@ export class App implements IRunnable { } } - private onClusterExit(deadWorker: Worker, code: number, signal: string) { + private onClusterExit(deadWorker: Worker, code: number, signal: string) { debug('worker %s died', deadWorker.process.pid) if (code === 0 || signal === 'SIGINT') { @@ -164,4 +169,4 @@ export class App implements IRunnable { callback() } } -} \ No newline at end of file +} diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index d078aa6e..db10cd14 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -122,7 +122,7 @@ export class MaintenanceWorker implements IRunnable { await this.processNip05Reverifications(currentSettings) - if (!path(['payments','enabled'], currentSettings)) { + if (!path(['payments', 'enabled'], currentSettings)) { await clearOldEventsPromise return } @@ -148,9 +148,9 @@ export class MaintenanceWorker implements IRunnable { await this.paymentsService.updateInvoiceStatus({ id, status }) if ( - invoice.status !== updatedInvoice.status - && updatedInvoice.status == InvoiceStatus.COMPLETED - && updatedInvoice.confirmedAt + invoice.status !== updatedInvoice.status && + updatedInvoice.status == InvoiceStatus.COMPLETED && + updatedInvoice.confirmedAt ) { debug('confirming invoice %s & notifying %s', invoice.id, invoice.pubkey) diff --git a/src/app/static-mirroring-worker.ts b/src/app/static-mirroring-worker.ts index aba9b09d..64582ff2 100644 --- a/src/app/static-mirroring-worker.ts +++ b/src/app/static-mirroring-worker.ts @@ -1,345 +1,354 @@ -import { anyPass, map, mergeDeepRight, path } from 'ramda' -import { RawData, WebSocket } from 'ws' -import cluster from 'cluster' -import { randomUUID } from 'crypto' - -import { createRelayedEventMessage, createSubscriptionMessage } from '../utils/messages' -import { EventLimits, FeeSchedule, Mirror, Settings } from '../@types/settings' -import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isDirectMessageEvent, isEventIdValid, isEventKindOrRangeMatch, isEventMatchingFilter, isEventSignatureValid, isExpiredEvent, isFileMessageEvent, isSealEvent } from '../utils/event' -import { IEventRepository, IUserRepository } from '../@types/repositories' -import { createLogger } from '../factories/logger-factory' -import { Event } from '../@types/event' -import { EventExpirationTimeMetadataKey } from '../constants/base' -import { IRunnable } from '../@types/base' -import { OutgoingEventMessage } from '../@types/messages' -import { RelayedEvent } from '../@types/event' -import { WebSocketServerAdapterEvent } from '../constants/adapter' - -const debug = createLogger('static-mirror-worker') - -export class StaticMirroringWorker implements IRunnable { - private client: WebSocket | undefined - private config: Mirror - - public constructor( - private readonly eventRepository: IEventRepository, - private readonly userRepository: IUserRepository, - private readonly process: NodeJS.Process, - private readonly settings: () => Settings, - ) { - this.process - .on('message', this.onMessage.bind(this)) - .on('SIGINT', this.onExit.bind(this)) - .on('SIGHUP', this.onExit.bind(this)) - .on('SIGTERM', this.onExit.bind(this)) - .on('uncaughtException', this.onError.bind(this)) - .on('unhandledRejection', this.onError.bind(this)) - } - - public run(): void { - const currentSettings = this.settings() - - console.log('mirroring', currentSettings.mirroring) - - this.config = path(['mirroring', 'static', process.env.MIRROR_INDEX], currentSettings) as Mirror - - let since = Math.floor(Date.now() / 1000) - 60*10 - - const createMirror = (config: Mirror) => { - const subscriptionId = `mirror-${randomUUID()}` - - debug('connecting to %s', config.address) - - return new WebSocket(config.address, { timeout: 5000 }) - .on('open', function () { - debug('connected to %s', config.address) - - if (Array.isArray(config.filters) && config.filters?.length) { - const filters = config.filters.map((filter) => ({ ...filter, since })) - - debug('subscribing with %s: %o', subscriptionId, filters) - - this.send(JSON.stringify(createSubscriptionMessage(subscriptionId, filters))) - } - }) - .on('message', async (raw: RawData) => { - try { - const message = JSON.parse(raw.toString('utf8')) as OutgoingEventMessage - - if (!Array.isArray(message)) { - return - } - - if (message[0] !== 'EVENT' || message[1] !== subscriptionId) { - debug('%s >> local: %o', config.address, message) - return - } - - let event = message[2] - - if (!anyPass(map(isEventMatchingFilter, config.filters))(event)) { - return - } - - if (!await isEventIdValid(event) || !await isEventSignatureValid(event)) { - return - } - - if (isExpiredEvent(event)) { - return - } - - const eventExpiration = getEventExpiration(event) - if (eventExpiration) { - event = { - ...event, - [EventExpirationTimeMetadataKey]: eventExpiration, - } as any - } - - if (!this.canAcceptEvent(event)) { - return - } - - if (!await this.isUserAdmitted(event)) { - return - } - - // NIP-17: inner events (kind 13, 14, 15) must never be stored directly - if (isSealEvent(event) || isDirectMessageEvent(event) || isFileMessageEvent(event)) { - return - } - - since = Math.floor(Date.now() / 1000) - 30 - - debug('%s >> local: %s', config.address, event.id) - - const inserted = await this.eventRepository.create(event) - - if (inserted && cluster.isWorker && typeof process.send === 'function') { - - process.send({ - eventName: WebSocketServerAdapterEvent.Broadcast, - event, - source: config.address, - }) - } - } catch (error) { - debug('unable to process message: %o', error) - } - }) - .on('close', (code, reason) => { - debug(`disconnected (${code}): ${reason.toString()}`) - - setTimeout(() => { - this.client.removeAllListeners() - this.client = createMirror(config) - }, 5000) - }) - .on('error', function (error) { - debug('connection error: %o', error) - }) - } - - this.client = createMirror(this.config) - } - - private getRelayPublicKey(): string { - const relayPrivkey = getRelayPrivateKey(this.settings().info.relay_url) - return getPublicKey(relayPrivkey) - } - - private canAcceptEvent(event: Event): boolean { - if (this.getRelayPublicKey() === event.pubkey) { - debug(`event ${event.id} not accepted: pubkey is relay pubkey`) - return false - } - - const now = Math.floor(Date.now() / 1000) - - const eventLimits = this.settings().limits?.event ?? {} - - const eventLimitOverrides = this.config.limits.event ?? {} - - const limits = mergeDeepRight(eventLimits, eventLimitOverrides) as EventLimits - - if (Array.isArray(limits.content)) { - for (const limit of limits.content) { - if ( - typeof limit.maxLength !== 'undefined' - && limit.maxLength > 0 - && event.content.length > limit.maxLength - && ( - !Array.isArray(limit.kinds) - || limit.kinds.some(isEventKindOrRangeMatch(event)) - ) - ) { - debug(`event ${event.id} not accepted: content is longer than ${limit.maxLength} bytes`) - return false - } - } - } else if ( - typeof limits.content?.maxLength !== 'undefined' - && limits.content?.maxLength > 0 - && event.content.length > limits.content.maxLength - && ( - !Array.isArray(limits.content.kinds) - || limits.content.kinds.some(isEventKindOrRangeMatch(event)) - ) - ) { - debug(`event ${event.id} not accepted: content is longer than ${limits.content.maxLength} bytes`) - return false - } - - if ( - typeof limits.createdAt?.maxPositiveDelta !== 'undefined' - && limits.createdAt.maxPositiveDelta > 0 - && event.created_at > now + limits.createdAt.maxPositiveDelta) { - debug(`event ${event.id} not accepted: created_at is more than ${limits.createdAt.maxPositiveDelta} seconds in the future`) - return false - } - - if ( - typeof limits.createdAt?.maxNegativeDelta !== 'undefined' - && limits.createdAt.maxNegativeDelta > 0 - && event.created_at < now - limits.createdAt.maxNegativeDelta) { - debug(`event ${event.id} not accepted: created_at is more than ${limits.createdAt.maxNegativeDelta} seconds in the past`) - return false - } - - if ( - typeof limits.eventId?.minLeadingZeroBits !== 'undefined' - && limits.eventId.minLeadingZeroBits > 0 - ) { - const pow = getEventProofOfWork(event.id) - if (pow < limits.eventId.minLeadingZeroBits) { - debug(`event ${event.id} not accepted: pow difficulty ${pow}<${limits.eventId.minLeadingZeroBits}`) - return false - } - } - - if ( - typeof limits.pubkey?.minLeadingZeroBits !== 'undefined' - && limits.pubkey.minLeadingZeroBits > 0 - ) { - const pow = getPubkeyProofOfWork(event.pubkey) - if (pow < limits.pubkey.minLeadingZeroBits) { - debug(`event ${event.id} not accepted: pow pubkey difficulty ${pow}<${limits.pubkey.minLeadingZeroBits}`) - return false - } - } - - if ( - typeof limits.pubkey?.whitelist !== 'undefined' - && limits.pubkey.whitelist.length > 0 - && !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) - ) { - debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) - return false - } - - if ( - typeof limits.pubkey?.blacklist !== 'undefined' - && limits.pubkey.blacklist.length > 0 - && limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) - ) { - debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) - return false - } - - if ( - typeof limits.kind?.whitelist !== 'undefined' - && limits.kind.whitelist.length > 0 - && !limits.kind.whitelist.some(isEventKindOrRangeMatch(event))) { - debug(`blocked: event kind ${event.kind} not allowed`) - return false - } - - if ( - typeof limits.kind?.blacklist !== 'undefined' - && limits.kind.blacklist.length > 0 - && limits.kind.blacklist.some(isEventKindOrRangeMatch(event))) { - debug(`blocked: event kind ${event.kind} not allowed`) - return false - } - - return true - } - - protected async isUserAdmitted(event: Event): Promise { - const currentSettings = this.settings() - - if (this.config.skipAdmissionCheck === true) { - return true - } - - if (currentSettings.payments?.enabled !== true) { - return true - } - - const isApplicableFee = (feeSchedule: FeeSchedule) => - feeSchedule.enabled - && !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) - && !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) - - const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) - - if (!Array.isArray(feeSchedules) || !feeSchedules.length) { - return true - } - - const user = await this.userRepository.findByPubkey(event.pubkey) - if (user?.isAdmitted !== true) { - debug(`user not admitted: ${event.pubkey}`) - return false - } - - const minBalance = currentSettings.limits?.event?.pubkey?.minBalance - if (minBalance && user.balance < minBalance) { - debug(`user not admitted: user balance ${user.balance} < ${minBalance}`) - return false - } - - return true - } - - private onMessage(message: { eventName: string, event: unknown, source: string }): void { - if ( - message.eventName !== WebSocketServerAdapterEvent.Broadcast - || message.source === this.config.address - || !this.client - || this.client.readyState !== WebSocket.OPEN - ) { - return - } - - const event = message.event as RelayedEvent - - const eventToRelay = createRelayedEventMessage(event, this.config.secret) - const outboundMessage = JSON.stringify(eventToRelay) - debug('%s >> %s: %s', message.source ?? 'local', this.config.address, outboundMessage) - this.client.send(outboundMessage) - } - - private onError(error: Error) { - debug('error: %o', error) - throw error - } - - private onExit() { - debug('exiting') - this.close(() => { - this.process.exit(0) - }) - } - - public close(callback?: () => void) { - debug('closing') - if (this.client) { - this.client.terminate() - } - if (typeof callback === 'function') { - callback() - } - } -} +import { anyPass, map, mergeDeepRight, path } from 'ramda' +import { RawData, WebSocket } from 'ws' +import cluster from 'cluster' +import { randomUUID } from 'crypto' + +import { createRelayedEventMessage, createSubscriptionMessage } from '../utils/messages' +import { EventLimits, FeeSchedule, Mirror, Settings } from '../@types/settings' +import { + getEventExpiration, + getEventProofOfWork, + getPubkeyProofOfWork, + getPublicKey, + getRelayPrivateKey, + isDirectMessageEvent, + isEventIdValid, + isEventKindOrRangeMatch, + isEventMatchingFilter, + isEventSignatureValid, + isExpiredEvent, + isFileMessageEvent, + isSealEvent, +} from '../utils/event' +import { IEventRepository, IUserRepository } from '../@types/repositories' +import { createLogger } from '../factories/logger-factory' +import { Event } from '../@types/event' +import { EventExpirationTimeMetadataKey } from '../constants/base' +import { IRunnable } from '../@types/base' +import { OutgoingEventMessage } from '../@types/messages' +import { RelayedEvent } from '../@types/event' +import { WebSocketServerAdapterEvent } from '../constants/adapter' + +const debug = createLogger('static-mirror-worker') + +export class StaticMirroringWorker implements IRunnable { + private client: WebSocket | undefined + private config: Mirror + + public constructor( + private readonly eventRepository: IEventRepository, + private readonly userRepository: IUserRepository, + private readonly process: NodeJS.Process, + private readonly settings: () => Settings, + ) { + this.process + .on('message', this.onMessage.bind(this)) + .on('SIGINT', this.onExit.bind(this)) + .on('SIGHUP', this.onExit.bind(this)) + .on('SIGTERM', this.onExit.bind(this)) + .on('uncaughtException', this.onError.bind(this)) + .on('unhandledRejection', this.onError.bind(this)) + } + + public run(): void { + const currentSettings = this.settings() + + console.log('mirroring', currentSettings.mirroring) + + this.config = path(['mirroring', 'static', process.env.MIRROR_INDEX], currentSettings) as Mirror + + let since = Math.floor(Date.now() / 1000) - 60 * 10 + + const createMirror = (config: Mirror) => { + const subscriptionId = `mirror-${randomUUID()}` + + debug('connecting to %s', config.address) + + return new WebSocket(config.address, { timeout: 5000 }) + .on('open', function () { + debug('connected to %s', config.address) + + if (Array.isArray(config.filters) && config.filters?.length) { + const filters = config.filters.map((filter) => ({ ...filter, since })) + + debug('subscribing with %s: %o', subscriptionId, filters) + + this.send(JSON.stringify(createSubscriptionMessage(subscriptionId, filters))) + } + }) + .on('message', async (raw: RawData) => { + try { + const message = JSON.parse(raw.toString('utf8')) as OutgoingEventMessage + + if (!Array.isArray(message)) { + return + } + + if (message[0] !== 'EVENT' || message[1] !== subscriptionId) { + debug('%s >> local: %o', config.address, message) + return + } + + let event = message[2] + + if (!anyPass(map(isEventMatchingFilter, config.filters))(event)) { + return + } + + if (!(await isEventIdValid(event)) || !(await isEventSignatureValid(event))) { + return + } + + if (isExpiredEvent(event)) { + return + } + + const eventExpiration = getEventExpiration(event) + if (eventExpiration) { + event = { + ...event, + [EventExpirationTimeMetadataKey]: eventExpiration, + } as any + } + + if (!this.canAcceptEvent(event)) { + return + } + + if (!(await this.isUserAdmitted(event))) { + return + } + + // NIP-17: inner events (kind 13, 14, 15) must never be stored directly + if (isSealEvent(event) || isDirectMessageEvent(event) || isFileMessageEvent(event)) { + return + } + + since = Math.floor(Date.now() / 1000) - 30 + + debug('%s >> local: %s', config.address, event.id) + + const inserted = await this.eventRepository.create(event) + + if (inserted && cluster.isWorker && typeof process.send === 'function') { + process.send({ + eventName: WebSocketServerAdapterEvent.Broadcast, + event, + source: config.address, + }) + } + } catch (error) { + debug('unable to process message: %o', error) + } + }) + .on('close', (code, reason) => { + debug(`disconnected (${code}): ${reason.toString()}`) + + setTimeout(() => { + this.client.removeAllListeners() + this.client = createMirror(config) + }, 5000) + }) + .on('error', function (error) { + debug('connection error: %o', error) + }) + } + + this.client = createMirror(this.config) + } + + private getRelayPublicKey(): string { + const relayPrivkey = getRelayPrivateKey(this.settings().info.relay_url) + return getPublicKey(relayPrivkey) + } + + private canAcceptEvent(event: Event): boolean { + if (this.getRelayPublicKey() === event.pubkey) { + debug(`event ${event.id} not accepted: pubkey is relay pubkey`) + return false + } + + const now = Math.floor(Date.now() / 1000) + + const eventLimits = this.settings().limits?.event ?? {} + + const eventLimitOverrides = this.config.limits.event ?? {} + + const limits = mergeDeepRight(eventLimits, eventLimitOverrides) as EventLimits + + if (Array.isArray(limits.content)) { + for (const limit of limits.content) { + if ( + typeof limit.maxLength !== 'undefined' && + limit.maxLength > 0 && + event.content.length > limit.maxLength && + (!Array.isArray(limit.kinds) || limit.kinds.some(isEventKindOrRangeMatch(event))) + ) { + debug(`event ${event.id} not accepted: content is longer than ${limit.maxLength} bytes`) + return false + } + } + } else if ( + typeof limits.content?.maxLength !== 'undefined' && + limits.content?.maxLength > 0 && + event.content.length > limits.content.maxLength && + (!Array.isArray(limits.content.kinds) || limits.content.kinds.some(isEventKindOrRangeMatch(event))) + ) { + debug(`event ${event.id} not accepted: content is longer than ${limits.content.maxLength} bytes`) + return false + } + + if ( + typeof limits.createdAt?.maxPositiveDelta !== 'undefined' && + limits.createdAt.maxPositiveDelta > 0 && + event.created_at > now + limits.createdAt.maxPositiveDelta + ) { + debug( + `event ${event.id} not accepted: created_at is more than ${limits.createdAt.maxPositiveDelta} seconds in the future`, + ) + return false + } + + if ( + typeof limits.createdAt?.maxNegativeDelta !== 'undefined' && + limits.createdAt.maxNegativeDelta > 0 && + event.created_at < now - limits.createdAt.maxNegativeDelta + ) { + debug( + `event ${event.id} not accepted: created_at is more than ${limits.createdAt.maxNegativeDelta} seconds in the past`, + ) + return false + } + + if (typeof limits.eventId?.minLeadingZeroBits !== 'undefined' && limits.eventId.minLeadingZeroBits > 0) { + const pow = getEventProofOfWork(event.id) + if (pow < limits.eventId.minLeadingZeroBits) { + debug(`event ${event.id} not accepted: pow difficulty ${pow}<${limits.eventId.minLeadingZeroBits}`) + return false + } + } + + if (typeof limits.pubkey?.minLeadingZeroBits !== 'undefined' && limits.pubkey.minLeadingZeroBits > 0) { + const pow = getPubkeyProofOfWork(event.pubkey) + if (pow < limits.pubkey.minLeadingZeroBits) { + debug(`event ${event.id} not accepted: pow pubkey difficulty ${pow}<${limits.pubkey.minLeadingZeroBits}`) + return false + } + } + + if ( + typeof limits.pubkey?.whitelist !== 'undefined' && + limits.pubkey.whitelist.length > 0 && + !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) + ) { + debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) + return false + } + + if ( + typeof limits.pubkey?.blacklist !== 'undefined' && + limits.pubkey.blacklist.length > 0 && + limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) + ) { + debug(`event ${event.id} not accepted: pubkey not allowed: ${event.pubkey}`) + return false + } + + if ( + typeof limits.kind?.whitelist !== 'undefined' && + limits.kind.whitelist.length > 0 && + !limits.kind.whitelist.some(isEventKindOrRangeMatch(event)) + ) { + debug(`blocked: event kind ${event.kind} not allowed`) + return false + } + + if ( + typeof limits.kind?.blacklist !== 'undefined' && + limits.kind.blacklist.length > 0 && + limits.kind.blacklist.some(isEventKindOrRangeMatch(event)) + ) { + debug(`blocked: event kind ${event.kind} not allowed`) + return false + } + + return true + } + + protected async isUserAdmitted(event: Event): Promise { + const currentSettings = this.settings() + + if (this.config.skipAdmissionCheck === true) { + return true + } + + if (currentSettings.payments?.enabled !== true) { + return true + } + + const isApplicableFee = (feeSchedule: FeeSchedule) => + feeSchedule.enabled && + !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) && + !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) + + const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) + + if (!Array.isArray(feeSchedules) || !feeSchedules.length) { + return true + } + + const user = await this.userRepository.findByPubkey(event.pubkey) + if (user?.isAdmitted !== true) { + debug(`user not admitted: ${event.pubkey}`) + return false + } + + const minBalance = currentSettings.limits?.event?.pubkey?.minBalance + if (minBalance && user.balance < minBalance) { + debug(`user not admitted: user balance ${user.balance} < ${minBalance}`) + return false + } + + return true + } + + private onMessage(message: { eventName: string; event: unknown; source: string }): void { + if ( + message.eventName !== WebSocketServerAdapterEvent.Broadcast || + message.source === this.config.address || + !this.client || + this.client.readyState !== WebSocket.OPEN + ) { + return + } + + const event = message.event as RelayedEvent + + const eventToRelay = createRelayedEventMessage(event, this.config.secret) + const outboundMessage = JSON.stringify(eventToRelay) + debug('%s >> %s: %s', message.source ?? 'local', this.config.address, outboundMessage) + this.client.send(outboundMessage) + } + + private onError(error: Error) { + debug('error: %o', error) + throw error + } + + private onExit() { + debug('exiting') + this.close(() => { + this.process.exit(0) + }) + } + + public close(callback?: () => void) { + debug('closing') + if (this.client) { + this.client.terminate() + } + if (typeof callback === 'function') { + callback() + } + } +} diff --git a/src/app/worker.ts b/src/app/worker.ts index 1693595a..93b29a5a 100644 --- a/src/app/worker.ts +++ b/src/app/worker.ts @@ -12,7 +12,7 @@ export class AppWorker implements IRunnable { public constructor( private readonly process: NodeJS.Process, - private readonly adapter: IWebSocketServerAdapter + private readonly adapter: IWebSocketServerAdapter, ) { this.process .on('message', this.onMessage.bind(this)) @@ -30,14 +30,14 @@ export class AppWorker implements IRunnable { this.adapter.listen(typeof port === 'number' ? port : Number(port)) } - private onMessage(message: { eventName: string, event: unknown }): void { + private onMessage(message: { eventName: string; event: unknown }): void { this.adapter.emit(message.eventName, message.event) } private onError(error: Error) { if (error.name === 'TypeError' && error.message === "Cannot read properties of undefined (reading '__knexUid')") { console.error( - 'Unable to acquire connection. Please increase DB_MAX_POOL_SIZE, DB_ACQUIRE_CONNECTION_TIMEOUT and tune postgresql.conf to make use of server\'s resources.' + "Unable to acquire connection. Please increase DB_MAX_POOL_SIZE, DB_ACQUIRE_CONNECTION_TIMEOUT and tune postgresql.conf to make use of server's resources.", ) return } diff --git a/src/cache/client.ts b/src/cache/client.ts index 3d3ce584..b73c1fc6 100644 --- a/src/cache/client.ts +++ b/src/cache/client.ts @@ -2,11 +2,12 @@ import { createClient, RedisClientOptions } from 'redis' import { CacheClient } from '../@types/cache' import { createLogger } from '../factories/logger-factory' - const debug = createLogger('cache-client') export const getCacheConfig = (): RedisClientOptions => ({ - url: process.env.REDIS_URI ? process.env.REDIS_URI : `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, + url: process.env.REDIS_URI + ? process.env.REDIS_URI + : `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, password: process.env.REDIS_PASSWORD, }) diff --git a/src/clean-db.ts b/src/clean-db.ts index c2b863df..ab096172 100644 --- a/src/clean-db.ts +++ b/src/clean-db.ts @@ -270,11 +270,13 @@ export const runCleanDb = async (args: string[] = process.argv.slice(2)): Promis } if (require.main === module) { - runCleanDb().then((exitCode) => { - process.exitCode = exitCode - }).catch((error) => { - const message = error instanceof Error ? error.message : String(error) - console.error(message) - process.exitCode = 1 - }) -} \ No newline at end of file + runCleanDb() + .then((exitCode) => { + process.exitCode = exitCode + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + console.error(message) + process.exitCode = 1 + }) +} diff --git a/src/constants/adapter.ts b/src/constants/adapter.ts index d463d781..74a280d0 100644 --- a/src/constants/adapter.ts +++ b/src/constants/adapter.ts @@ -4,10 +4,10 @@ export enum WebSocketAdapterEvent { Broadcast = 'broadcast', Subscribe = 'subscribe', Unsubscribe = 'unsubscribe', - Heartbeat = 'heartbeat' + Heartbeat = 'heartbeat', } export enum WebSocketServerAdapterEvent { Broadcast = 'broadcast', - Connection = 'connection' + Connection = 'connection', } diff --git a/src/controllers/admission/get-admission-check-controller.ts b/src/controllers/admission/get-admission-check-controller.ts index 5900643a..0fafb668 100644 --- a/src/controllers/admission/get-admission-check-controller.ts +++ b/src/controllers/admission/get-admission-check-controller.ts @@ -14,20 +14,17 @@ export class GetSubmissionCheckController implements IController { private readonly userRepository: IUserRepository, private readonly settings: () => Settings, private readonly rateLimiter: () => IRateLimiter, - ){} + ) {} public async handleRequest(request: Request, response: Response): Promise { const currentSettings = this.settings() const limited = await this.isRateLimited(request, currentSettings) if (limited) { - response - .status(429) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Too many requests') + response.status(429).setHeader('content-type', 'text/plain; charset=utf8').send('Too many requests') return } - + const pubkey = request.params.pubkey const user = await this.userRepository.findByPubkey(pubkey) @@ -38,10 +35,7 @@ export class GetSubmissionCheckController implements IController { userAdmitted = true } - response - .status(200) - .setHeader('content-type', 'application/json; charset=utf8') - .send({ userAdmitted }) + response.status(200).setHeader('content-type', 'application/json; charset=utf8').send({ userAdmitted }) return } @@ -67,4 +61,4 @@ export class GetSubmissionCheckController implements IController { } return limited } -} \ No newline at end of file +} diff --git a/src/controllers/callbacks/lnbits-callback-controller.ts b/src/controllers/callbacks/lnbits-callback-controller.ts index 7aafc9f7..49bde798 100644 --- a/src/controllers/callbacks/lnbits-callback-controller.ts +++ b/src/controllers/callbacks/lnbits-callback-controller.ts @@ -16,13 +16,10 @@ const debug = createLogger('lnbits-callback-controller') export class LNbitsCallbackController implements IController { public constructor( private readonly paymentsService: IPaymentsService, - private readonly invoiceRepository: IInvoiceRepository - ) { } + private readonly invoiceRepository: IInvoiceRepository, + ) {} - public async handleRequest( - request: Request, - response: Response, - ) { + public async handleRequest(request: Request, response: Response) { debug('request headers: %o', request.headers) debug('request body: %o', request.body) @@ -32,18 +29,14 @@ export class LNbitsCallbackController implements IController { if (paymentProcessor !== 'lnbits') { debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } const queryValidation = validateSchema(lnbitsCallbackQuerySchema)(request.query) if (queryValidation.error) { debug('unauthorized request from %s to /callbacks/lnbits: invalid query %o', remoteAddress, queryValidation.error) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } @@ -52,9 +45,7 @@ export class LNbitsCallbackController implements IController { const expiryString = split[0] const expiry = Number(expiryString) const hasValidSplit = split.length === 2 - const hasValidExpiry = - /^\d+$/.test(expiryString) && - Number.isSafeInteger(expiry) + const hasValidExpiry = /^\d+$/.test(expiryString) && Number.isSafeInteger(expiry) if ( !hasValidSplit || hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), expiryString).toString('hex') !== split[1] || @@ -62,18 +53,13 @@ export class LNbitsCallbackController implements IController { expiry <= Date.now() ) { debug('unauthorized request from %s to /callbacks/lnbits: hmac signature mismatch or expired', remoteAddress) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } const bodyValidation = validateSchema(lnbitsCallbackBodySchema)(request.body) if (bodyValidation.error) { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Malformed body') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body') return } @@ -82,10 +68,7 @@ export class LNbitsCallbackController implements IController { const storedInvoice = await this.invoiceRepository.findById(body.payment_hash) if (!storedInvoice) { - response - .status(404) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('No such invoice') + response.status(404).setHeader('content-type', 'text/plain; charset=utf8').send('No such invoice') return } @@ -97,22 +80,14 @@ export class LNbitsCallbackController implements IController { throw error } - if ( - invoice.status !== InvoiceStatus.COMPLETED - && !invoice.confirmedAt - ) { - response - .status(200) - .send() + if (invoice.status !== InvoiceStatus.COMPLETED && !invoice.confirmedAt) { + response.status(200).send() return } if (storedInvoice.status === InvoiceStatus.COMPLETED) { - response - .status(409) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invoice is already marked paid') + response.status(409).setHeader('content-type', 'text/plain; charset=utf8').send('Invoice is already marked paid') return } @@ -127,9 +102,6 @@ export class LNbitsCallbackController implements IController { throw error } - response - .status(200) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('OK') + response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK') } -} \ No newline at end of file +} diff --git a/src/controllers/callbacks/nodeless-callback-controller.ts b/src/controllers/callbacks/nodeless-callback-controller.ts index 4f6efc1a..1dc55f26 100644 --- a/src/controllers/callbacks/nodeless-callback-controller.ts +++ b/src/controllers/callbacks/nodeless-callback-controller.ts @@ -14,14 +14,9 @@ import { validateSchema } from '../../utils/validation' const debug = createLogger('nodeless-callback-controller') export class NodelessCallbackController implements IController { - public constructor( - private readonly paymentsService: IPaymentsService, - ) {} - - public async handleRequest( - request: Request, - response: Response, - ) { + public constructor(private readonly paymentsService: IPaymentsService) {} + + public async handleRequest(request: Request, response: Response) { debug('callback request headers: %o', request.headers) debug('callback request body: %O', request.body) @@ -43,17 +38,13 @@ export class NodelessCallbackController implements IController { if (expected !== actual) { console.error('nodeless callback request rejected: signature mismatch:', { expected, actual }) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } if (paymentProcessor !== 'nodeless') { debug('denied request from %s to /callbacks/nodeless which is not the current payment processor') - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } @@ -62,16 +53,8 @@ export class NodelessCallbackController implements IController { status: prop('status'), satsAmount: prop('amount'), metadata: prop('metadata'), - paidAt: ifElse( - propEq('status', 'paid'), - always(new Date().toISOString()), - always(null), - ), - createdAt: ifElse( - propSatisfies(is(String), 'createdAt'), - prop('createdAt'), - path(['metadata', 'createdAt']), - ), + paidAt: ifElse(propEq('status', 'paid'), always(new Date().toISOString()), always(null)), + createdAt: ifElse(propSatisfies(is(String), 'createdAt'), prop('createdAt'), path(['metadata', 'createdAt'])), })(request.body) debug('nodeless invoice: %O', nodelessInvoice) @@ -90,13 +73,8 @@ export class NodelessCallbackController implements IController { throw error } - if ( - updatedInvoice.status !== InvoiceStatus.COMPLETED - && !updatedInvoice.confirmedAt - ) { - response - .status(200) - .send() + if (updatedInvoice.status !== InvoiceStatus.COMPLETED && !updatedInvoice.confirmedAt) { + response.status(200).send() return } @@ -113,9 +91,6 @@ export class NodelessCallbackController implements IController { throw error } - response - .status(200) - .setHeader('content-type', 'application/json; charset=utf8') - .send('{"status":"ok"}') + response.status(200).setHeader('content-type', 'application/json; charset=utf8').send('{"status":"ok"}') } } diff --git a/src/controllers/callbacks/opennode-callback-controller.ts b/src/controllers/callbacks/opennode-callback-controller.ts index ed8d704f..ddda9cb8 100644 --- a/src/controllers/callbacks/opennode-callback-controller.ts +++ b/src/controllers/callbacks/opennode-callback-controller.ts @@ -11,24 +11,16 @@ import { validateSchema } from '../../utils/validation' const debug = createLogger('opennode-callback-controller') export class OpenNodeCallbackController implements IController { - public constructor( - private readonly paymentsService: IPaymentsService, - ) {} + public constructor(private readonly paymentsService: IPaymentsService) {} - public async handleRequest( - request: Request, - response: Response, - ) { + public async handleRequest(request: Request, response: Response) { debug('request headers: %o', request.headers) debug('request body: %O', request.body) const bodyValidation = validateSchema(opennodeCallbackBodySchema)(request.body) if (bodyValidation.error) { debug('opennode callback request rejected: invalid body %o', bodyValidation.error) - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Malformed body') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body') return } @@ -45,13 +37,8 @@ export class OpenNodeCallbackController implements IController { throw error } - if ( - updatedInvoice.status !== InvoiceStatus.COMPLETED - && !updatedInvoice.confirmedAt - ) { - response - .status(200) - .send() + if (updatedInvoice.status !== InvoiceStatus.COMPLETED && !updatedInvoice.confirmedAt) { + response.status(200).send() return } @@ -74,9 +61,6 @@ export class OpenNodeCallbackController implements IController { throw error } - response - .status(200) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('OK') + response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK') } } diff --git a/src/controllers/callbacks/zebedee-callback-controller.ts b/src/controllers/callbacks/zebedee-callback-controller.ts index fa372d23..32120eea 100644 --- a/src/controllers/callbacks/zebedee-callback-controller.ts +++ b/src/controllers/callbacks/zebedee-callback-controller.ts @@ -13,24 +13,16 @@ import { zebedeeCallbackBodySchema } from '../../schemas/zebedee-callback-schema const debug = createLogger('zebedee-callback-controller') export class ZebedeeCallbackController implements IController { - public constructor( - private readonly paymentsService: IPaymentsService, - ) {} - - public async handleRequest( - request: Request, - response: Response, - ) { + public constructor(private readonly paymentsService: IPaymentsService) {} + + public async handleRequest(request: Request, response: Response) { debug('request headers: %o', request.headers) debug('request body: %O', request.body) const bodyValidation = validateSchema(zebedeeCallbackBodySchema)(request.body) if (bodyValidation.error) { debug('zebedee callback request rejected: invalid body %o', bodyValidation.error) - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Malformed body') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body') return } @@ -42,17 +34,13 @@ export class ZebedeeCallbackController implements IController { if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) { debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } if (paymentProcessor !== 'zebedee') { debug('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress) - response - .status(403) - .send('Forbidden') + response.status(403).send('Forbidden') return } @@ -69,13 +57,8 @@ export class ZebedeeCallbackController implements IController { throw error } - if ( - updatedInvoice.status !== InvoiceStatus.COMPLETED - && !updatedInvoice.confirmedAt - ) { - response - .status(200) - .send() + if (updatedInvoice.status !== InvoiceStatus.COMPLETED && !updatedInvoice.confirmedAt) { + response.status(200).send() return } @@ -99,9 +82,6 @@ export class ZebedeeCallbackController implements IController { throw error } - response - .status(200) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('OK') + response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK') } } diff --git a/src/controllers/invoices/get-invoice-controller.ts b/src/controllers/invoices/get-invoice-controller.ts index 058cdc6f..621532f5 100644 --- a/src/controllers/invoices/get-invoice-controller.ts +++ b/src/controllers/invoices/get-invoice-controller.ts @@ -9,17 +9,14 @@ import { IController } from '../../@types/controllers' import { getTemplate } from '../../utils/template-cache' - - export class GetInvoiceController implements IController { - public async handleRequest( - _req: Request, - res: Response, - ): Promise { + public async handleRequest(_req: Request, res: Response): Promise { const settings = createSettings() - if (pathEq(['payments', 'enabled'], true, settings) - && pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings)) { + if ( + pathEq(['payments', 'enabled'], true, settings) && + pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings) + ) { const name = path(['info', 'name'])(settings) const feeSchedule = path(['payments', 'feeSchedules', 'admission', '0'], settings) const page = getTemplate('./resources/get-invoice.html') @@ -33,4 +30,4 @@ export class GetInvoiceController implements IController { res.status(404).send() } } -} \ No newline at end of file +} diff --git a/src/controllers/invoices/get-invoice-status-controller.ts b/src/controllers/invoices/get-invoice-status-controller.ts index 2018a7a9..10977940 100644 --- a/src/controllers/invoices/get-invoice-status-controller.ts +++ b/src/controllers/invoices/get-invoice-status-controller.ts @@ -6,21 +6,16 @@ import { IInvoiceRepository } from '../../@types/repositories' const debug = createLogger('get-invoice-status-controller') export class GetInvoiceStatusController implements IController { - public constructor( - private readonly invoiceRepository: IInvoiceRepository, - ) {} + public constructor(private readonly invoiceRepository: IInvoiceRepository) {} - public async handleRequest( - request: Request, - response: Response, - ): Promise { + public async handleRequest(request: Request, response: Response): Promise { const invoiceId = request.params.invoiceId if (typeof invoiceId !== 'string' || !invoiceId) { debug('invalid invoice id: %s', invoiceId) response .status(400) .setHeader('content-type', 'application/json; charset=utf8') - .send({ id: invoiceId, status: 'invalid invoice' }) + .send({ id: invoiceId, status: 'invalid invoice' }) return } @@ -37,13 +32,10 @@ export class GetInvoiceStatusController implements IController { return } - response - .status(200) - .setHeader('content-type', 'application/json; charset=utf8') - .send({ - id: invoice.id, - status: invoice.status, - }) + response.status(200).setHeader('content-type', 'application/json; charset=utf8').send({ + id: invoice.id, + status: invoice.status, + }) } catch (error) { console.error(`get-invoice-status-controller: unable to get invoice ${invoiceId}:`, error) diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index 96d7f6b4..1d908903 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -17,8 +17,6 @@ import { getPublicKey, getRelayPrivateKey } from '../../utils/event' import { getRemoteAddress } from '../../utils/http' import { getTemplate } from '../../utils/template-cache' - - const debug = createLogger('post-invoice-controller') export class PostInvoiceController implements IController { @@ -27,11 +25,9 @@ export class PostInvoiceController implements IController { private readonly paymentsService: IPaymentsService, private readonly settings: () => Settings, private readonly rateLimiter: () => IRateLimiter, - ){} + ) {} public async handleRequest(request: Request, response: Response): Promise { - - debug('params: %o', request.params) debug('body: %o', request.body) @@ -43,18 +39,12 @@ export class PostInvoiceController implements IController { const limited = await this.isRateLimited(request, currentSettings) if (limited) { - response - .status(429) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Too many requests') + response.status(429).setHeader('content-type', 'text/plain; charset=utf8').send('Too many requests') return } if (!request.body || typeof request.body !== 'object') { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invalid request') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Invalid request') return } @@ -62,20 +52,14 @@ export class PostInvoiceController implements IController { const tosAccepted = request.body?.tosAccepted === 'yes' if (!tosAccepted) { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('ToS agreement: not accepted') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('ToS agreement: not accepted') return } const isAdmissionInvoice = request.body?.feeSchedule === 'admission' if (!isAdmissionInvoice) { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invalid fee') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Invalid fee') return } @@ -84,10 +68,7 @@ export class PostInvoiceController implements IController { let pubkey: string if (typeof pubkeyRaw !== 'string') { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invalid pubkey: missing') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Invalid pubkey: missing') return } else if (/^[0-9a-f]{64}$/.test(pubkeyRaw)) { @@ -96,32 +77,22 @@ export class PostInvoiceController implements IController { try { pubkey = fromBech32(pubkeyRaw) } catch (_error) { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invalid pubkey: invalid npub') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Invalid pubkey: invalid npub') return } } else { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Invalid pubkey: unknown format') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Invalid pubkey: unknown format') return } - const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled - && !feeSchedule.whitelists?.pubkeys?.some((prefix) => pubkey.startsWith(prefix)) - const admissionFee = currentSettings.payments?.feeSchedules.admission - .filter(isApplicableFee) + const isApplicableFee = (feeSchedule: FeeSchedule) => + feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => pubkey.startsWith(prefix)) + 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') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('No admission fee required') return } @@ -129,35 +100,23 @@ export class PostInvoiceController implements IController { const minBalance = currentSettings.limits?.event?.pubkey?.minBalance const user = await this.userRepository.findByPubkey(pubkey) if (user && user.isAdmitted && (!minBalance || user.balance >= minBalance)) { - response - .status(400) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('User is already admitted.') + response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('User is already admitted.') return } let invoice: Invoice const amount = admissionFee.reduce((sum, fee) => { - return fee.enabled && !fee.whitelists?.pubkeys?.includes(pubkey) - ? BigInt(fee.amount) + sum - : sum + return fee.enabled && !fee.whitelists?.pubkeys?.includes(pubkey) ? BigInt(fee.amount) + sum : sum }, 0n) try { const description = `${relayName} Admission Fee for ${toBech32('npub')(pubkey)}` - invoice = await this.paymentsService.createInvoice( - pubkey, - amount, - description, - ) + invoice = await this.paymentsService.createInvoice(pubkey, amount, description) } catch (error) { console.error('Unable to create invoice. Reason:', error) - response - .status(500) - .setHeader('content-type', 'text/plain') - .send('Unable to create invoice') + response.status(500).setHeader('content-type', 'text/plain').send('Unable to create invoice') return } @@ -185,10 +144,7 @@ export class PostInvoiceController implements IController { // nonce is crypto-random base64 — safe in both attribute and script contexts .replaceAll('{{nonce}}', response.locals.nonce) - response - .status(200) - .setHeader('Content-Type', 'text/html; charset=utf8') - .send(body) + response.status(200).setHeader('Content-Type', 'text/html; charset=utf8').send(body) return } @@ -214,4 +170,4 @@ export class PostInvoiceController implements IController { } return limited } -} \ No newline at end of file +} diff --git a/src/database/client.ts b/src/database/client.ts index 555cf249..e5265ad3 100644 --- a/src/database/client.ts +++ b/src/database/client.ts @@ -3,7 +3,7 @@ import 'pg-query-stream' import knex, { Knex } from 'knex' import { createLogger } from '../factories/logger-factory' -((knex) => { +;((knex) => { const lastUpdate = {} knex.Client.prototype.releaseConnection = function (connection) { const released = this.pool.release(connection) @@ -14,7 +14,9 @@ import { createLogger } from '../factories/logger-factory' lastUpdate[tag] = lastUpdate[tag] ?? now if (now - lastUpdate[tag] >= 60000) { lastUpdate[tag] = now - console.log(`${tag} connection pool: ${this.pool.numUsed()} used / ${this.pool.numFree()} free / ${this.pool.numPendingAcquires()} pending`) + console.log( + `${tag} connection pool: ${this.pool.numUsed()} used / ${this.pool.numFree()} free / ${this.pool.numPendingAcquires()} pending`, + ) } } @@ -22,50 +24,54 @@ import { createLogger } from '../factories/logger-factory' } })(knex) -const getMasterConfig = (): Knex.Config => ({ - tag: 'master', - client: 'pg', - connection: process.env.DB_URI ? process.env.DB_URI : { - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - }, - pool: { - min: process.env.DB_MIN_POOL_SIZE ? Number(process.env.DB_MIN_POOL_SIZE) : 0, - max: process.env.DB_MAX_POOL_SIZE ? Number(process.env.DB_MAX_POOL_SIZE) : 3, - idleTimeoutMillis: 60000, - propagateCreateError: false, - acquireTimeoutMillis: process.env.DB_ACQUIRE_CONNECTION_TIMEOUT - ? Number(process.env.DB_ACQUIRE_CONNECTION_TIMEOUT) - : 60000, - }, - acquireConnectionTimeout: process.env.DB_ACQUIRE_CONNECTION_TIMEOUT - ? Number(process.env.DB_ACQUIRE_CONNECTION_TIMEOUT) - : 60000, -} as any) +const getMasterConfig = (): Knex.Config => + ({ + tag: 'master', + client: 'pg', + connection: process.env.DB_URI + ? process.env.DB_URI + : { + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }, + pool: { + min: process.env.DB_MIN_POOL_SIZE ? Number(process.env.DB_MIN_POOL_SIZE) : 0, + max: process.env.DB_MAX_POOL_SIZE ? Number(process.env.DB_MAX_POOL_SIZE) : 3, + idleTimeoutMillis: 60000, + propagateCreateError: false, + acquireTimeoutMillis: process.env.DB_ACQUIRE_CONNECTION_TIMEOUT + ? Number(process.env.DB_ACQUIRE_CONNECTION_TIMEOUT) + : 60000, + }, + acquireConnectionTimeout: process.env.DB_ACQUIRE_CONNECTION_TIMEOUT + ? Number(process.env.DB_ACQUIRE_CONNECTION_TIMEOUT) + : 60000, + }) as any -const getReadReplicaConfigByIndex = (index: number): Knex.Config => ({ - tag: 'read-replica', - client: 'pg', - connection: { - host: process.env[`RR${index}_DB_HOST`], - port: Number(process.env[`RR${index}_DB_PORT`]), - user: process.env[`RR${index}_DB_USER`], - password: process.env[`RR${index}_DB_PASSWORD`], - database: process.env[`RR${index}_DB_NAME`], - }, - pool: { - min: process.env[`RR${index}_DB_MIN_POOL_SIZE`] ? Number(process.env[`RR${index}_DB_MIN_POOL_SIZE`]) : 0, - max: process.env[`RR${index}_DB_MAX_POOL_SIZE`] ? Number(process.env[`RR${index}_DB_MAX_POOL_SIZE`]) : 3, - idleTimeoutMillis: 60000, - propagateCreateError: false, - acquireTimeoutMillis: process.env[`RR${index}_DB_ACQUIRE_CONNECTION_TIMEOUT`] - ? Number(process.env[`RR${index}_DB_ACQUIRE_CONNECTION_TIMEOUT`]) - : 60000, - }, -} as any) +const getReadReplicaConfigByIndex = (index: number): Knex.Config => + ({ + tag: 'read-replica', + client: 'pg', + connection: { + host: process.env[`RR${index}_DB_HOST`], + port: Number(process.env[`RR${index}_DB_PORT`]), + user: process.env[`RR${index}_DB_USER`], + password: process.env[`RR${index}_DB_PASSWORD`], + database: process.env[`RR${index}_DB_NAME`], + }, + pool: { + min: process.env[`RR${index}_DB_MIN_POOL_SIZE`] ? Number(process.env[`RR${index}_DB_MIN_POOL_SIZE`]) : 0, + max: process.env[`RR${index}_DB_MAX_POOL_SIZE`] ? Number(process.env[`RR${index}_DB_MAX_POOL_SIZE`]) : 3, + idleTimeoutMillis: 60000, + propagateCreateError: false, + acquireTimeoutMillis: process.env[`RR${index}_DB_ACQUIRE_CONNECTION_TIMEOUT`] + ? Number(process.env[`RR${index}_DB_ACQUIRE_CONNECTION_TIMEOUT`]) + : 60000, + }, + }) as any const getReadReplicaConfig = (): Knex.Config => { const readReplicaIndex = Number(process.env.WORKER_INDEX) % Number(process.env.READ_REPLICAS) diff --git a/src/database/transaction.ts b/src/database/transaction.ts index 7301d4dd..a7dce378 100644 --- a/src/database/transaction.ts +++ b/src/database/transaction.ts @@ -6,15 +6,13 @@ import { ITransaction } from '../@types/database' export class Transaction implements ITransaction { private trx: Knex.Transaction - public constructor( - private readonly dbClient: DatabaseClient, - ) {} + public constructor(private readonly dbClient: DatabaseClient) {} public async begin(): Promise { this.trx = await this.dbClient.transaction(null, { isolationLevel: 'serializable' }) } - public get transaction (): DatabaseTransaction { + public get transaction(): DatabaseTransaction { if (!this.trx) { throw new Error('Unable to get transaction: transaction not started.') } @@ -34,4 +32,4 @@ export class Transaction implements ITransaction { } return this.trx.rollback() } -} \ No newline at end of file +} diff --git a/src/factories/controllers/get-admission-check-controller-factory.ts b/src/factories/controllers/get-admission-check-controller-factory.ts index c1bdd265..cdad249c 100644 --- a/src/factories/controllers/get-admission-check-controller-factory.ts +++ b/src/factories/controllers/get-admission-check-controller-factory.ts @@ -10,10 +10,6 @@ export const createGetAdmissionCheckController = () => { const readReplicaDbClient = getReadReplicaDbClient() const eventRepository = new EventRepository(dbClient, readReplicaDbClient) const userRepository = new UserRepository(dbClient, eventRepository) - - return new GetSubmissionCheckController( - userRepository, - createSettings, - slidingWindowRateLimiterFactory - ) + + return new GetSubmissionCheckController(userRepository, createSettings, slidingWindowRateLimiterFactory) } diff --git a/src/factories/controllers/lnbits-callback-controller-factory.ts b/src/factories/controllers/lnbits-callback-controller-factory.ts index 312b6e86..b3674724 100644 --- a/src/factories/controllers/lnbits-callback-controller-factory.ts +++ b/src/factories/controllers/lnbits-callback-controller-factory.ts @@ -5,8 +5,5 @@ import { InvoiceRepository } from '../../repositories/invoice-repository' import { LNbitsCallbackController } from '../../controllers/callbacks/lnbits-callback-controller' export const createLNbitsCallbackController = (): IController => { - return new LNbitsCallbackController( - createPaymentsService(), - new InvoiceRepository(getMasterDbClient()) - ) + return new LNbitsCallbackController(createPaymentsService(), new InvoiceRepository(getMasterDbClient())) } diff --git a/src/factories/controllers/nodeless-callback-controller-factory.ts b/src/factories/controllers/nodeless-callback-controller-factory.ts index ab34d980..53efa4f0 100644 --- a/src/factories/controllers/nodeless-callback-controller-factory.ts +++ b/src/factories/controllers/nodeless-callback-controller-factory.ts @@ -2,6 +2,5 @@ import { createPaymentsService } from '../payments-service-factory' import { IController } from '../../@types/controllers' import { NodelessCallbackController } from '../../controllers/callbacks/nodeless-callback-controller' -export const createNodelessCallbackController = (): IController => new NodelessCallbackController( - createPaymentsService(), -) +export const createNodelessCallbackController = (): IController => + new NodelessCallbackController(createPaymentsService()) diff --git a/src/factories/controllers/opennode-callback-controller-factory.ts b/src/factories/controllers/opennode-callback-controller-factory.ts index e6829211..10a3ed6f 100644 --- a/src/factories/controllers/opennode-callback-controller-factory.ts +++ b/src/factories/controllers/opennode-callback-controller-factory.ts @@ -3,7 +3,5 @@ import { IController } from '../../@types/controllers' import { OpenNodeCallbackController } from '../../controllers/callbacks/opennode-callback-controller' export const createOpenNodeCallbackController = (): IController => { - return new OpenNodeCallbackController( - createPaymentsService(), - ) + return new OpenNodeCallbackController(createPaymentsService()) } diff --git a/src/factories/controllers/post-invoice-controller-factory.ts b/src/factories/controllers/post-invoice-controller-factory.ts index 1d5b6593..f92abd1e 100644 --- a/src/factories/controllers/post-invoice-controller-factory.ts +++ b/src/factories/controllers/post-invoice-controller-factory.ts @@ -14,10 +14,5 @@ export const createPostInvoiceController = (): IController => { const userRepository = new UserRepository(dbClient, eventRepository) const paymentsService = createPaymentsService() - return new PostInvoiceController( - userRepository, - paymentsService, - createSettings, - slidingWindowRateLimiterFactory, - ) + return new PostInvoiceController(userRepository, paymentsService, createSettings, slidingWindowRateLimiterFactory) } diff --git a/src/factories/controllers/zebedee-callback-controller-factory.ts b/src/factories/controllers/zebedee-callback-controller-factory.ts index dd6b19a3..500f0a92 100644 --- a/src/factories/controllers/zebedee-callback-controller-factory.ts +++ b/src/factories/controllers/zebedee-callback-controller-factory.ts @@ -3,7 +3,5 @@ import { IController } from '../../@types/controllers' import { ZebedeeCallbackController } from '../../controllers/callbacks/zebedee-callback-controller' export const createZebedeeCallbackController = (): IController => { - return new ZebedeeCallbackController( - createPaymentsService(), - ) + return new ZebedeeCallbackController(createPaymentsService()) } diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 6e9fc868..b18433b7 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,5 +1,12 @@ import { IEventRepository, IUserRepository } from '../@types/repositories' -import { isDeleteEvent, isEphemeralEvent, isGiftWrapEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } from '../utils/event' +import { + isDeleteEvent, + isEphemeralEvent, + isGiftWrapEvent, + isParameterizedReplaceableEvent, + isReplaceableEvent, + isRequestToVanishEvent, +} from '../utils/event' import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy' import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy' import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy' @@ -12,10 +19,11 @@ import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strateg import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy' import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-strategy' -export const eventStrategyFactory = ( - eventRepository: IEventRepository, - userRepository: IUserRepository, -): Factory>, [Event, IWebSocketAdapter]> => +export const eventStrategyFactory = + ( + eventRepository: IEventRepository, + userRepository: IUserRepository, + ): Factory>, [Event, IWebSocketAdapter]> => ([event, adapter]: [Event, IWebSocketAdapter]) => { if (isRequestToVanishEvent(event)) { return new VanishEventStrategy(adapter, eventRepository, userRepository) diff --git a/src/factories/logger-factory.ts b/src/factories/logger-factory.ts index 31296043..ba40d0cd 100644 --- a/src/factories/logger-factory.ts +++ b/src/factories/logger-factory.ts @@ -3,7 +3,7 @@ import debug from 'debug' export const createLogger = ( namespace: string, - options: { enabled?: boolean; stdout?: boolean } = { enabled: false, stdout: false } + options: { enabled?: boolean; stdout?: boolean } = { enabled: false, stdout: false }, ) => { const prefix = cluster.isWorker ? process.env.WORKER_TYPE : 'primary' const instance = debug(prefix) diff --git a/src/factories/maintenance-service-factory.ts b/src/factories/maintenance-service-factory.ts index 3edb9ea5..bb7af596 100644 --- a/src/factories/maintenance-service-factory.ts +++ b/src/factories/maintenance-service-factory.ts @@ -4,8 +4,5 @@ import { EventRepository } from '../repositories/event-repository' import { MaintenanceService } from '../services/maintenance-service' export const createMaintenanceService = () => { - return new MaintenanceService( - new EventRepository(getMasterDbClient(), getReadReplicaDbClient()), - createSettings - ) + return new MaintenanceService(new EventRepository(getMasterDbClient(), getReadReplicaDbClient()), createSettings) } diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index b059875b..e702bf8b 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -18,14 +18,15 @@ const getCache = (): ICacheAdapter => { return cacheAdapter } -export const messageHandlerFactory = ( - eventRepository: IEventRepository, - userRepository: IUserRepository, - nip05VerificationRepository: INip05VerificationRepository, -) => ([message, adapter]: [IncomingMessage, IWebSocketAdapter]) => { - switch (message[0]) { - case MessageType.EVENT: - { +export const messageHandlerFactory = + ( + eventRepository: IEventRepository, + userRepository: IUserRepository, + nip05VerificationRepository: INip05VerificationRepository, + ) => + ([message, adapter]: [IncomingMessage, IWebSocketAdapter]) => { + switch (message[0]) { + case MessageType.EVENT: { return new EventMessageHandler( adapter, eventStrategyFactory(eventRepository, userRepository), @@ -37,11 +38,11 @@ export const messageHandlerFactory = ( getCache(), ) } - case MessageType.REQ: - return new SubscribeMessageHandler(adapter, eventRepository, createSettings) - case MessageType.CLOSE: - return new UnsubscribeMessageHandler(adapter) - default: - throw new Error(`Unknown message type: ${String(message[0]).substring(0, 64)}`) + case MessageType.REQ: + return new SubscribeMessageHandler(adapter, eventRepository, createSettings) + case MessageType.CLOSE: + return new UnsubscribeMessageHandler(adapter) + default: + throw new Error(`Unknown message type: ${String(message[0]).substring(0, 64)}`) + } } -} diff --git a/src/factories/payments-processors/lnbits-payments-processor-factory.ts b/src/factories/payments-processors/lnbits-payments-processor-factory.ts index 582593bb..e10097ba 100644 --- a/src/factories/payments-processors/lnbits-payments-processor-factory.ts +++ b/src/factories/payments-processors/lnbits-payments-processor-factory.ts @@ -6,7 +6,6 @@ import { IPaymentsProcessor } from '../../@types/clients' import { LNbitsPaymentsProcessor } from '../../payments-processors/lnbits-payment-processor' import { Settings } from '../../@types/settings' - const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults => { if (!process.env.LNBITS_API_KEY) { throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.') diff --git a/src/factories/payments-processors/lnurl-payments-processor-factory.ts b/src/factories/payments-processors/lnurl-payments-processor-factory.ts index f1254a19..02975241 100644 --- a/src/factories/payments-processors/lnurl-payments-processor-factory.ts +++ b/src/factories/payments-processors/lnurl-payments-processor-factory.ts @@ -9,7 +9,9 @@ import { Settings } from '../../@types/settings' export const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { const invoiceURL = path(['paymentsProcessors', 'lnurl', 'invoiceURL'], settings) as string | undefined if (typeof invoiceURL === 'undefined') { - throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.') + throw new Error( + 'Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.', + ) } const client = axios.create() diff --git a/src/factories/payments-processors/nodeless-payments-processor-factory.ts b/src/factories/payments-processors/nodeless-payments-processor-factory.ts index ab0abd33..4e1f2a03 100644 --- a/src/factories/payments-processors/nodeless-payments-processor-factory.ts +++ b/src/factories/payments-processors/nodeless-payments-processor-factory.ts @@ -16,8 +16,8 @@ const getNodelessAxiosConfig = (settings: Settings): CreateAxiosDefaults => return { headers: { 'content-type': 'application/json', - 'authorization': `Bearer ${process.env.NODELESS_API_KEY}`, - 'accept': 'application/json', + authorization: `Bearer ${process.env.NODELESS_API_KEY}`, + accept: 'application/json', }, baseURL: path(['paymentsProcessors', 'nodeless', 'baseURL'], settings), maxRedirects: 1, diff --git a/src/factories/payments-processors/opennode-payments-processor-factory.ts b/src/factories/payments-processors/opennode-payments-processor-factory.ts index 9a51853b..8e934fa0 100644 --- a/src/factories/payments-processors/opennode-payments-processor-factory.ts +++ b/src/factories/payments-processors/opennode-payments-processor-factory.ts @@ -16,7 +16,7 @@ const getOpenNodeAxiosConfig = (settings: Settings): CreateAxiosDefaults => return { headers: { 'content-type': 'application/json', - 'authorization': process.env.OPENNODE_API_KEY, + authorization: process.env.OPENNODE_API_KEY, }, baseURL: path(['paymentsProcessors', 'opennode', 'baseURL'], settings), maxRedirects: 1, diff --git a/src/factories/payments-processors/zebedee-payments-processor-factory.ts b/src/factories/payments-processors/zebedee-payments-processor-factory.ts index ba3841a7..3cdd0e3e 100644 --- a/src/factories/payments-processors/zebedee-payments-processor-factory.ts +++ b/src/factories/payments-processors/zebedee-payments-processor-factory.ts @@ -16,7 +16,7 @@ const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults => return { headers: { 'content-type': 'application/json', - 'apikey': process.env.ZEBEDEE_API_KEY, + apikey: process.env.ZEBEDEE_API_KEY, }, baseURL: path(['paymentsProcessors', 'zebedee', 'baseURL'], settings), maxRedirects: 1, @@ -33,8 +33,8 @@ export const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsPro } if ( - !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) - || !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length + !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) || + !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length ) { const error = new Error('Setting paymentsProcessor.zebedee.ipWhitelist is empty.') console.error('Unable to create payments processor.', error) diff --git a/src/factories/payments-service-factory.ts b/src/factories/payments-service-factory.ts index 1a762e44..bbf3e595 100644 --- a/src/factories/payments-service-factory.ts +++ b/src/factories/payments-service-factory.ts @@ -20,6 +20,6 @@ export const createPaymentsService = () => { userRepository, invoiceRepository, eventRepository, - createSettings + createSettings, ) } diff --git a/src/factories/static-mirroring.worker-factory.ts b/src/factories/static-mirroring.worker-factory.ts index 67f7028e..85df95d4 100644 --- a/src/factories/static-mirroring.worker-factory.ts +++ b/src/factories/static-mirroring.worker-factory.ts @@ -10,10 +10,5 @@ export const staticMirroringWorkerFactory = () => { const eventRepository = new EventRepository(dbClient, readReplicaDbClient) const userRepository = new UserRepository(dbClient, eventRepository) - return new StaticMirroringWorker( - eventRepository, - userRepository, - process, - createSettings, - ) + return new StaticMirroringWorker(eventRepository, userRepository, process, createSettings) } diff --git a/src/factories/web-app-factory.ts b/src/factories/web-app-factory.ts index 4a90fa05..e7e16e0c 100644 --- a/src/factories/web-app-factory.ts +++ b/src/factories/web-app-factory.ts @@ -16,14 +16,20 @@ export const createWebApp = () => { const relayUrl = new URL(settings.info.relay_url) const webRelayUrl = new URL(relayUrl.toString()) - webRelayUrl.protocol = (relayUrl.protocol === 'wss:') ? 'https:' : ':' + webRelayUrl.protocol = relayUrl.protocol === 'wss:' ? 'https:' : ':' const directives = { 'img-src': ["'self'", 'data:', 'https://cdn.zebedee.io/an/nostr/'], 'connect-src': ["'self'", settings.info.relay_url as string, webRelayUrl.toString()], 'default-src': ["'self'"], 'script-src-attr': [`'nonce-${nonce}'`], - 'script-src': ["'self'", `'nonce-${nonce}'`, 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'], + 'script-src': [ + "'self'", + `'nonce-${nonce}'`, + 'https://cdn.jsdelivr.net/npm/', + 'https://unpkg.com/', + 'https://cdnjs.cloudflare.com/ajax/libs/', + ], 'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'], 'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'], } @@ -36,4 +42,4 @@ export const createWebApp = () => { app.use(router) return app -} \ No newline at end of file +} diff --git a/src/factories/websocket-adapter-factory.ts b/src/factories/websocket-adapter-factory.ts index 7b3f83df..03dc305a 100644 --- a/src/factories/websocket-adapter-factory.ts +++ b/src/factories/websocket-adapter-factory.ts @@ -8,12 +8,13 @@ import { messageHandlerFactory } from './message-handler-factory' import { slidingWindowRateLimiterFactory } from './rate-limiter-factory' import { WebSocketAdapter } from '../adapters/web-socket-adapter' - -export const webSocketAdapterFactory = ( - eventRepository: IEventRepository, - userRepository: IUserRepository, - nip05VerificationRepository: INip05VerificationRepository, -) => ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) => +export const webSocketAdapterFactory = + ( + eventRepository: IEventRepository, + userRepository: IUserRepository, + nip05VerificationRepository: INip05VerificationRepository, + ) => + ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) => new WebSocketAdapter( client, request, diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 7582d984..2c5fd5e0 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -7,7 +7,7 @@ import { parseNip05Identifier, verifyNip05Identifier, } from '../utils/nip05' -import { Event, ExpiringEvent } from '../@types/event' +import { Event, ExpiringEvent } from '../@types/event' import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings' import { getEventExpiration, @@ -73,7 +73,10 @@ export class EventMessageHandler implements IMessageHandler { if (await this.isRateLimited(event)) { debug('event %s rejected: rate-limited') - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down')) + this.webSocket.emit( + WebSocketAdapterEvent.Message, + createCommandResult(event.id, false, 'rate-limited: slow down'), + ) return } @@ -108,7 +111,10 @@ export class EventMessageHandler implements IMessageHandler { const strategy = this.strategyFactory([event, this.webSocket]) if (typeof strategy?.execute !== 'function') { - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: event not supported')) + this.webSocket.emit( + WebSocketAdapterEvent.Message, + createCommandResult(event.id, false, 'error: event not supported'), + ) return } @@ -116,7 +122,10 @@ export class EventMessageHandler implements IMessageHandler { await strategy.execute(event) this.processNip05Metadata(event) } catch (_error) { - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event')) + this.webSocket.emit( + WebSocketAdapterEvent.Message, + createCommandResult(event.id, false, 'error: unable to process event'), + ) } } @@ -129,64 +138,54 @@ export class EventMessageHandler implements IMessageHandler { if (this.getRelayPublicKey() === event.pubkey) { return } - const now = Math.floor(Date.now()/1000) + const now = Math.floor(Date.now() / 1000) const limits = this.settings().limits?.event ?? {} if (Array.isArray(limits.content)) { for (const limit of limits.content) { if ( - typeof limit.maxLength !== 'undefined' - && limit.maxLength > 0 - && event.content.length > limit.maxLength - && ( - !Array.isArray(limit.kinds) - || limit.kinds.some(isEventKindOrRangeMatch(event)) - ) + typeof limit.maxLength !== 'undefined' && + limit.maxLength > 0 && + event.content.length > limit.maxLength && + (!Array.isArray(limit.kinds) || limit.kinds.some(isEventKindOrRangeMatch(event))) ) { return `rejected: content is longer than ${limit.maxLength} bytes` } } } else if ( - typeof limits.content?.maxLength !== 'undefined' - && limits.content?.maxLength > 0 - && event.content.length > limits.content.maxLength - && ( - !Array.isArray(limits.content.kinds) - || limits.content.kinds.some(isEventKindOrRangeMatch(event)) - ) + typeof limits.content?.maxLength !== 'undefined' && + limits.content?.maxLength > 0 && + event.content.length > limits.content.maxLength && + (!Array.isArray(limits.content.kinds) || limits.content.kinds.some(isEventKindOrRangeMatch(event))) ) { return `rejected: content is longer than ${limits.content.maxLength} bytes` } if ( - typeof limits.createdAt?.maxPositiveDelta !== 'undefined' - && limits.createdAt.maxPositiveDelta > 0 - && event.created_at > now + limits.createdAt.maxPositiveDelta) { + typeof limits.createdAt?.maxPositiveDelta !== 'undefined' && + limits.createdAt.maxPositiveDelta > 0 && + event.created_at > now + limits.createdAt.maxPositiveDelta + ) { return `rejected: created_at is more than ${limits.createdAt.maxPositiveDelta} seconds in the future` } if ( - typeof limits.createdAt?.maxNegativeDelta !== 'undefined' - && limits.createdAt.maxNegativeDelta > 0 - && event.created_at < now - limits.createdAt.maxNegativeDelta) { + typeof limits.createdAt?.maxNegativeDelta !== 'undefined' && + limits.createdAt.maxNegativeDelta > 0 && + event.created_at < now - limits.createdAt.maxNegativeDelta + ) { return `rejected: created_at is more than ${limits.createdAt.maxNegativeDelta} seconds in the past` } - if ( - typeof limits.eventId?.minLeadingZeroBits !== 'undefined' - && limits.eventId.minLeadingZeroBits > 0 - ) { + if (typeof limits.eventId?.minLeadingZeroBits !== 'undefined' && limits.eventId.minLeadingZeroBits > 0) { const pow = getEventProofOfWork(event.id) if (pow < limits.eventId.minLeadingZeroBits) { return `pow: difficulty ${pow}<${limits.eventId.minLeadingZeroBits}` } } - if ( - typeof limits.pubkey?.minLeadingZeroBits !== 'undefined' - && limits.pubkey.minLeadingZeroBits > 0 - ) { + if (typeof limits.pubkey?.minLeadingZeroBits !== 'undefined' && limits.pubkey.minLeadingZeroBits > 0) { const pow = getPubkeyProofOfWork(event.pubkey) if (pow < limits.pubkey.minLeadingZeroBits) { return `pow: pubkey difficulty ${pow}<${limits.pubkey.minLeadingZeroBits}` @@ -194,41 +193,43 @@ 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)) + typeof limits.pubkey?.whitelist !== 'undefined' && + limits.pubkey.whitelist.length > 0 && + !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) ) { return 'blocked: pubkey not allowed' } if ( - typeof limits.pubkey?.blacklist !== 'undefined' - && limits.pubkey.blacklist.length > 0 - && limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) + typeof limits.pubkey?.blacklist !== 'undefined' && + limits.pubkey.blacklist.length > 0 && + limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) ) { return 'blocked: pubkey not allowed' } if ( - typeof limits.kind?.whitelist !== 'undefined' - && limits.kind.whitelist.length > 0 - && !limits.kind.whitelist.some(isEventKindOrRangeMatch(event))) { + typeof limits.kind?.whitelist !== 'undefined' && + limits.kind.whitelist.length > 0 && + !limits.kind.whitelist.some(isEventKindOrRangeMatch(event)) + ) { return `blocked: event kind ${event.kind} not allowed` } if ( - typeof limits.kind?.blacklist !== 'undefined' - && limits.kind.blacklist.length > 0 - && limits.kind.blacklist.some(isEventKindOrRangeMatch(event))) { + typeof limits.kind?.blacklist !== 'undefined' && + limits.kind.blacklist.length > 0 && + limits.kind.blacklist.some(isEventKindOrRangeMatch(event)) + ) { return `blocked: event kind ${event.kind} not allowed` } } protected async isEventValid(event: Event): Promise { - if (!await isEventIdValid(event)) { + if (!(await isEventIdValid(event))) { return 'invalid: event id does not match' } - if (!await isEventSignatureValid(event)) { + if (!(await isEventSignatureValid(event))) { return 'invalid: event signature verification failed' } @@ -271,17 +272,17 @@ export class EventMessageHandler implements IMessageHandler { } if ( - typeof whitelists?.pubkeys !== 'undefined' - && Array.isArray(whitelists?.pubkeys) - && whitelists.pubkeys.includes(event.pubkey) + typeof whitelists?.pubkeys !== 'undefined' && + Array.isArray(whitelists?.pubkeys) && + whitelists.pubkeys.includes(event.pubkey) ) { return false } if ( - typeof whitelists?.ipAddresses !== 'undefined' - && Array.isArray(whitelists?.ipAddresses) - && whitelists.ipAddresses.includes(this.webSocket.getClientAddress()) + typeof whitelists?.ipAddresses !== 'undefined' && + Array.isArray(whitelists?.ipAddresses) && + whitelists.ipAddresses.includes(this.webSocket.getClientAddress()) ) { return false } @@ -297,11 +298,7 @@ export class EventMessageHandler implements IMessageHandler { ? `${event.pubkey}:events:${period}:${toString(kinds)}` : `${event.pubkey}:events:${period}` - return rateLimiter.hit( - key, - 1, - { period, rate }, - ) + return rateLimiter.hit(key, 1, { period, rate }) } let limited = false @@ -334,9 +331,9 @@ export class EventMessageHandler implements IMessageHandler { } const isApplicableFee = (feeSchedule: FeeSchedule) => - feeSchedule.enabled - && !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) - && !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) + feeSchedule.enabled && + !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) && + !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) if (!Array.isArray(feeSchedules) || !feeSchedules.length) { @@ -379,8 +376,7 @@ export class EventMessageHandler implements IMessageHandler { } private cacheSet(key: string, value: string, ttl: number): void { - this.cache.setKey(key, value, ttl) - .catch((error) => debug('unable to cache %s: %o', key, error)) + this.cache.setKey(key, value, ttl).catch((error) => debug('unable to cache %s: %o', key, error)) } protected addExpirationMetadata(event: Event): Event | ExpiringEvent { @@ -470,18 +466,9 @@ export class EventMessageHandler implements IMessageHandler { } const repo = this.nip05VerificationRepository - Promise.all([ - repo.findByPubkey(event.pubkey), - verifyNip05Identifier(nip05Identifier, event.pubkey), - ]) + Promise.all([repo.findByPubkey(event.pubkey), verifyNip05Identifier(nip05Identifier, event.pubkey)]) .then(([existing, outcome]) => { - const verification = buildMetadataVerification( - event.pubkey, - nip05Identifier, - parsed.domain, - existing, - outcome, - ) + const verification = buildMetadataVerification(event.pubkey, nip05Identifier, parsed.domain, existing, outcome) return repo.upsert(verification) }) .catch((error) => { diff --git a/src/handlers/event-strategies/default-event-strategy.ts b/src/handlers/event-strategies/default-event-strategy.ts index 48b5b886..d22f9589 100644 --- a/src/handlers/event-strategies/default-event-strategy.ts +++ b/src/handlers/event-strategies/default-event-strategy.ts @@ -12,12 +12,12 @@ export class DefaultEventStrategy implements IEventStrategy public constructor( private readonly webSocket: IWebSocketAdapter, private readonly eventRepository: IEventRepository, - ) { } + ) {} public async execute(event: Event): Promise { debug('received event: %o', event) const count = await this.eventRepository.create(event) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:')) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) if (count) { this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) diff --git a/src/handlers/event-strategies/delete-event-strategy.ts b/src/handlers/event-strategies/delete-event-strategy.ts index c25a3b6b..d2584cab 100644 --- a/src/handlers/event-strategies/delete-event-strategy.ts +++ b/src/handlers/event-strategies/delete-event-strategy.ts @@ -14,32 +14,24 @@ export class DeleteEventStrategy implements IEventStrategy> public constructor( private readonly webSocket: IWebSocketAdapter, private readonly eventRepository: IEventRepository, - ) { } + ) {} public async execute(event: Event): Promise { debug('received delete event: %o', event) - const isValidETag = (tag: Tag) => - tag.length >= 2 - && tag[0] === EventTags.Event - && /^[0-9a-f]{64}$/.test(tag[1]) + const isValidETag = (tag: Tag) => tag.length >= 2 && tag[0] === EventTags.Event && /^[0-9a-f]{64}$/.test(tag[1]) const eventIdsToDelete = event.tags.reduce( - (eventIds, tag) => isValidETag(tag) - ? [...eventIds, tag[1]] - : eventIds, - [] as string[] + (eventIds, tag) => (isValidETag(tag) ? [...eventIds, tag[1]] : eventIds), + [] as string[], ) if (eventIdsToDelete.length) { - await this.eventRepository.deleteByPubkeyAndIds( - event.pubkey, - eventIdsToDelete - ) + await this.eventRepository.deleteByPubkeyAndIds(event.pubkey, eventIdsToDelete) } const count = await this.eventRepository.create(event) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:')) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) if (count) { this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) diff --git a/src/handlers/event-strategies/ephemeral-event-strategy.ts b/src/handlers/event-strategies/ephemeral-event-strategy.ts index 716c77de..5abd5283 100644 --- a/src/handlers/event-strategies/ephemeral-event-strategy.ts +++ b/src/handlers/event-strategies/ephemeral-event-strategy.ts @@ -8,16 +8,11 @@ import { WebSocketAdapterEvent } from '../../constants/adapter' const debug = createLogger('ephemeral-event-strategy') export class EphemeralEventStrategy implements IEventStrategy> { - public constructor( - private readonly webSocket: IWebSocketAdapter, - ) { } + public constructor(private readonly webSocket: IWebSocketAdapter) {} public async execute(event: Event): Promise { debug('received ephemeral event: %o', event) - this.webSocket.emit( - WebSocketAdapterEvent.Message, - createCommandResult(event.id, true, ''), - ) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, '')) this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) } } diff --git a/src/handlers/event-strategies/gift-wrap-event-strategy.ts b/src/handlers/event-strategies/gift-wrap-event-strategy.ts index ee46006c..9a7b5e94 100644 --- a/src/handlers/event-strategies/gift-wrap-event-strategy.ts +++ b/src/handlers/event-strategies/gift-wrap-event-strategy.ts @@ -1,69 +1,61 @@ -import { createCommandResult } from '../../utils/messages' -import { createLogger } from '../../factories/logger-factory' -import { Event } from '../../@types/event' -import { EventTags } from '../../constants/base' -import { IEventRepository } from '../../@types/repositories' -import { IEventStrategy } from '../../@types/message-handlers' -import { IWebSocketAdapter } from '../../@types/adapters' -import { validateNip44Payload } from '../../utils/nip44' -import { WebSocketAdapterEvent } from '../../constants/adapter' - -const debug = createLogger('gift-wrap-event-strategy') - -export class GiftWrapEventStrategy implements IEventStrategy> { - public constructor( - private readonly webSocket: IWebSocketAdapter, - private readonly eventRepository: IEventRepository, - ) {} - - public async execute(event: Event): Promise { - debug('received gift wrap event: %o', event) - - const reason = this.validateGiftWrap(event) - if (reason) { - this.webSocket.emit( - WebSocketAdapterEvent.Message, - createCommandResult(event.id, false, `invalid: ${reason}`), - ) - return - } - - const count = await this.eventRepository.create(event) - this.webSocket.emit( - WebSocketAdapterEvent.Message, - createCommandResult(event.id, true, count ? '' : 'duplicate:'), - ) - - if (count) { - this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) - } - } - - private validateGiftWrap(event: Event): string | undefined { - // NIP-17: gift wrap MUST have exactly one p tag (one recipient per wrap) - const recipientTags = event.tags.filter( - (tag) => tag.length >= 2 && tag[0] === EventTags.Pubkey, - ) - - if (recipientTags.length === 0) { - return 'gift wrap event (kind 1059) must have a p tag identifying the recipient' - } - - if (recipientTags.length > 1) { - return 'gift wrap event (kind 1059) must have exactly one p tag' - } - - const recipientPubkey = recipientTags[0][1] - if (!/^[0-9a-f]{64}$/.test(recipientPubkey)) { - return 'gift wrap event (kind 1059) p tag must contain a valid 64-character lowercase hex pubkey' - } - - // Validate that the content is a structurally valid NIP-44 v2 payload - const payloadError = validateNip44Payload(event.content) - if (payloadError) { - return `gift wrap content must be a valid NIP-44 v2 payload: ${payloadError}` - } - - return undefined - } -} +import { createCommandResult } from '../../utils/messages' +import { createLogger } from '../../factories/logger-factory' +import { Event } from '../../@types/event' +import { EventTags } from '../../constants/base' +import { IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' +import { IWebSocketAdapter } from '../../@types/adapters' +import { validateNip44Payload } from '../../utils/nip44' +import { WebSocketAdapterEvent } from '../../constants/adapter' + +const debug = createLogger('gift-wrap-event-strategy') + +export class GiftWrapEventStrategy implements IEventStrategy> { + public constructor( + private readonly webSocket: IWebSocketAdapter, + private readonly eventRepository: IEventRepository, + ) {} + + public async execute(event: Event): Promise { + debug('received gift wrap event: %o', event) + + const reason = this.validateGiftWrap(event) + if (reason) { + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, `invalid: ${reason}`)) + return + } + + const count = await this.eventRepository.create(event) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) + + if (count) { + this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) + } + } + + private validateGiftWrap(event: Event): string | undefined { + // NIP-17: gift wrap MUST have exactly one p tag (one recipient per wrap) + const recipientTags = event.tags.filter((tag) => tag.length >= 2 && tag[0] === EventTags.Pubkey) + + if (recipientTags.length === 0) { + return 'gift wrap event (kind 1059) must have a p tag identifying the recipient' + } + + if (recipientTags.length > 1) { + return 'gift wrap event (kind 1059) must have exactly one p tag' + } + + const recipientPubkey = recipientTags[0][1] + if (!/^[0-9a-f]{64}$/.test(recipientPubkey)) { + return 'gift wrap event (kind 1059) p tag must contain a valid 64-character lowercase hex pubkey' + } + + // Validate that the content is a structurally valid NIP-44 v2 payload + const payloadError = validateNip44Payload(event.content) + if (payloadError) { + return `gift wrap content must be a valid NIP-44 v2 payload: ${payloadError}` + } + + return undefined + } +} diff --git a/src/handlers/event-strategies/parameterized-replaceable-event-strategy.ts b/src/handlers/event-strategies/parameterized-replaceable-event-strategy.ts index cf142ee4..9862b703 100644 --- a/src/handlers/event-strategies/parameterized-replaceable-event-strategy.ts +++ b/src/handlers/event-strategies/parameterized-replaceable-event-strategy.ts @@ -9,17 +9,19 @@ import { WebSocketAdapterEvent } from '../../constants/adapter' const debug = createLogger('parameterized-replaceable-event-strategy') -export class ParameterizedReplaceableEventStrategy - implements IEventStrategy> { +export class ParameterizedReplaceableEventStrategy implements IEventStrategy> { public constructor( private readonly webSocket: IWebSocketAdapter, private readonly eventRepository: IEventRepository, - ) { } + ) {} public async execute(event: Event): Promise { debug('received parameterized replaceable event: %o', event) - const [, deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [null, ''] + const [, deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [ + null, + '', + ] const parameterizedReplaceableEvent: ParameterizedReplaceableEvent = { ...event, @@ -27,7 +29,7 @@ export class ParameterizedReplaceableEventStrategy } const count = await this.eventRepository.upsert(parameterizedReplaceableEvent) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:')) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) if (count) { this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) diff --git a/src/handlers/event-strategies/replaceable-event-strategy.ts b/src/handlers/event-strategies/replaceable-event-strategy.ts index 5f58a822..b95dd35c 100644 --- a/src/handlers/event-strategies/replaceable-event-strategy.ts +++ b/src/handlers/event-strategies/replaceable-event-strategy.ts @@ -12,16 +12,13 @@ export class ReplaceableEventStrategy implements IEventStrategy { debug('received replaceable event: %o', event) try { const count = await this.eventRepository.upsert(event) - this.webSocket.emit( - WebSocketAdapterEvent.Message, - createCommandResult(event.id, true, (count) ? '' : 'duplicate:'), - ) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) if (count) { this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) } @@ -35,10 +32,7 @@ export class ReplaceableEventStrategy implements IEventStrategy> public async execute(event: Event): Promise { debug('received request to vanish event: %o', event) - await this.eventRepository.deleteByPubkeyExceptKinds( - event.pubkey, - [EventKinds.REQUEST_TO_VANISH], - ) + await this.eventRepository.deleteByPubkeyExceptKinds(event.pubkey, [EventKinds.REQUEST_TO_VANISH]) const count = await this.eventRepository.create(event) await this.userRepository.setVanished(event.pubkey, true) - this.webSocket.emit( - WebSocketAdapterEvent.Message, - createCommandResult(event.id, true, count ? '' : 'duplicate:') - ) + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:')) } -} \ No newline at end of file +} diff --git a/src/handlers/request-handlers/get-privacy-request-handler.ts b/src/handlers/request-handlers/get-privacy-request-handler.ts index e4d464f9..eaf931d1 100644 --- a/src/handlers/request-handlers/get-privacy-request-handler.ts +++ b/src/handlers/request-handlers/get-privacy-request-handler.ts @@ -7,7 +7,9 @@ import { escapeHtml } from '../../utils/html' import { getTemplate } from '../../utils/template-cache' export const getPrivacyRequestHandler = (_req: Request, res: Response, next: NextFunction) => { - const { info: { name } } = settings() + const { + info: { name }, + } = settings() let page: string try { diff --git a/src/handlers/request-handlers/get-terms-request-handler.ts b/src/handlers/request-handlers/get-terms-request-handler.ts index c91774b5..66747cc2 100644 --- a/src/handlers/request-handlers/get-terms-request-handler.ts +++ b/src/handlers/request-handlers/get-terms-request-handler.ts @@ -3,10 +3,10 @@ import { escapeHtml } from '../../utils/html' import { getTemplate } from '../../utils/template-cache' import { createSettings as settings } from '../../factories/settings-factory' - - export const getTermsRequestHandler = (_req: Request, res: Response, next: NextFunction) => { - const { info: { name } } = settings() + const { + info: { name }, + } = settings() let page: string try { diff --git a/src/handlers/request-handlers/nodeinfo-handler.ts b/src/handlers/request-handlers/nodeinfo-handler.ts index 0a2ebf73..a2220a4c 100644 --- a/src/handlers/request-handlers/nodeinfo-handler.ts +++ b/src/handlers/request-handlers/nodeinfo-handler.ts @@ -2,39 +2,46 @@ import { NextFunction, Request, Response } from 'express' import packageJson from '../../../package.json' export const nodeinfoHandler = (req: Request, res: Response, next: NextFunction) => { - res.json({ - links: [{ - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: `https://${req.hostname}/nodeinfo/2.0`, - }, { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: `https://${req.hostname}/nodeinfo/2.1`, - }], - }).send() + res + .json({ + links: [ + { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: `https://${req.hostname}/nodeinfo/2.0`, + }, + { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: `https://${req.hostname}/nodeinfo/2.1`, + }, + ], + }) + .send() next() } export const nodeinfo21Handler = (_req: Request, res: Response, next: NextFunction) => { - res.json({ - version: '2.1', - software: { - name: 'nostream', - version: packageJson.version, - repository: packageJson.repository.url, - homepage: packageJson.homepage, - }, - protocols: ['nostr'], - services: { - inbound: [], - outbound: [], - }, - openRegistrations: true, - usage: { - users: {}, - }, - metadata: { - features: ['nostr_relay'], - }, - }).send() + res + .json({ + version: '2.1', + software: { + name: 'nostream', + version: packageJson.version, + repository: packageJson.repository.url, + homepage: packageJson.homepage, + }, + protocols: ['nostr'], + services: { + inbound: [], + outbound: [], + }, + openRegistrations: true, + usage: { + users: {}, + }, + metadata: { + features: ['nostr_relay'], + }, + }) + .send() next() -} \ No newline at end of file +} diff --git a/src/handlers/request-handlers/rate-limiter-middleware.ts b/src/handlers/request-handlers/rate-limiter-middleware.ts index aac85229..72c7da87 100644 --- a/src/handlers/request-handlers/rate-limiter-middleware.ts +++ b/src/handlers/request-handlers/rate-limiter-middleware.ts @@ -24,10 +24,7 @@ export const rateLimiterMiddleware = async (request: Request, response: Response } export async function isRateLimited(remoteAddress: string, settings: Settings): Promise { - const { - rateLimits, - ipWhitelist = [], - } = settings.limits?.connection ?? {} + const { rateLimits, ipWhitelist = [] } = settings.limits?.connection ?? {} if (typeof rateLimits === 'undefined') { return false @@ -40,11 +37,7 @@ export async function isRateLimited(remoteAddress: string, settings: Settings): const rateLimiter = slidingWindowRateLimiterFactory() const hit = (period: number, rate: number) => - rateLimiter.hit( - `${remoteAddress}:connection:${period}`, - 1, - { period: period, rate: rate }, - ) + rateLimiter.hit(`${remoteAddress}:connection:${period}`, 1, { period: period, rate: rate }) let limited = false for (const { rate, period } of rateLimits) { diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index a39b943d..9c557d21 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -22,9 +22,7 @@ export const rootRequestHandler = (request: Request, response: Response, next: N const content = settings.limits?.event?.content - const pubkey = rawPubkey.startsWith('npub1') - ? fromBech32(rawPubkey) - : rawPubkey + const pubkey = rawPubkey.startsWith('npub1') ? fromBech32(rawPubkey) : rawPubkey const relayInformationDocument = { name, @@ -36,34 +34,35 @@ export const rootRequestHandler = (request: Request, response: Response, next: N software: packageJson.repository.url, version: packageJson.version, limitation: { - max_message_length: settings.network.maxPayloadSize, - max_subscriptions: settings.limits?.client?.subscription?.maxSubscriptions, - max_filters: settings.limits?.client?.subscription?.maxFilterValues, - max_limit: settings.limits?.client?.subscription?.maxLimit, - max_subid_length: settings.limits?.client?.subscription?.maxSubscriptionIdLength, - min_prefix: settings.limits?.client?.subscription?.minPrefixLength, - max_event_tags: 2500, - max_content_length: Array.isArray(content) - ? content[0].maxLength // best guess since we have per-kind limits - : content?.maxLength, - min_pow_difficulty: settings.limits?.event?.eventId?.minLeadingZeroBits, - auth_required: false, - payment_required: settings.payments?.enabled, + max_message_length: settings.network.maxPayloadSize, + max_subscriptions: settings.limits?.client?.subscription?.maxSubscriptions, + max_filters: settings.limits?.client?.subscription?.maxFilterValues, + max_limit: settings.limits?.client?.subscription?.maxLimit, + max_subid_length: settings.limits?.client?.subscription?.maxSubscriptionIdLength, + min_prefix: settings.limits?.client?.subscription?.minPrefixLength, + max_event_tags: 2500, + max_content_length: Array.isArray(content) + ? content[0].maxLength // best guess since we have per-kind limits + : content?.maxLength, + min_pow_difficulty: settings.limits?.event?.eventId?.minLeadingZeroBits, + auth_required: false, + payment_required: settings.payments?.enabled, }, payments_url: paymentsUrl.toString(), - fees: Object - .getOwnPropertyNames(settings.payments.feeSchedules) - .reduce((prev, feeName) => { + fees: Object.getOwnPropertyNames(settings.payments.feeSchedules).reduce( + (prev, feeName) => { const feeSchedules = settings.payments.feeSchedules[feeName] as FeeSchedule[] return { ...prev, - [feeName]: feeSchedules.reduce((fees, fee) => (fee.enabled) - ? [...fees, { amount: fee.amount, unit: 'msats' }] - : fees, []), + [feeName]: feeSchedules.reduce( + (fees, fee) => (fee.enabled ? [...fees, { amount: fee.amount, unit: 'msats' }] : fees), + [], + ), } - - }, {} as Record), + }, + {} as Record, + ), } response @@ -75,12 +74,11 @@ export const rootRequestHandler = (request: Request, response: Response, next: N return } - const admissionFeeEnabled = pathEq(['payments', 'enabled'], true, settings) - && pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings) + const admissionFeeEnabled = + pathEq(['payments', 'enabled'], true, settings) && + pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings) const admissionFee = path(['payments', 'feeSchedules', 'admission', '0'], settings) - const amount = admissionFeeEnabled && admissionFee - ? (BigInt(admissionFee.amount) / 1000n).toString() - : '0' + const amount = admissionFeeEnabled && admissionFee ? (BigInt(admissionFee.amount) / 1000n).toString() : '0' let page: string try { diff --git a/src/handlers/request-handlers/with-controller-request-handler.ts b/src/handlers/request-handlers/with-controller-request-handler.ts index e7465251..28ee7341 100644 --- a/src/handlers/request-handlers/with-controller-request-handler.ts +++ b/src/handlers/request-handlers/with-controller-request-handler.ts @@ -3,16 +3,11 @@ import { Request, Response } from 'express' import { Factory } from '../../@types/base' import { IController } from '../../@types/controllers' -export const withController = (controllerFactory: Factory) => async ( - request: Request, - response: Response, -) => { - try { - return await controllerFactory().handleRequest(request, response) - } catch (_error) { - response - .status(500) - .setHeader('content-type', 'text/plain') - .send('Error handling request') +export const withController = + (controllerFactory: Factory) => async (request: Request, response: Response) => { + try { + return await controllerFactory().handleRequest(request, response) + } catch (_error) { + response.status(500).setHeader('content-type', 'text/plain').send('Error handling request') + } } -} diff --git a/src/handlers/subscribe-message-handler.ts b/src/handlers/subscribe-message-handler.ts index 87fbde95..457cc4ea 100644 --- a/src/handlers/subscribe-message-handler.ts +++ b/src/handlers/subscribe-message-handler.ts @@ -2,7 +2,11 @@ import { anyPass, equals, isNil, map, propSatisfies, uniqWith } from 'ramda' // import { addAbortSignal } from 'stream' import { pipeline } from 'stream/promises' -import { createEndOfStoredEventsNoticeMessage, createNoticeMessage, createOutgoingEventMessage } from '../utils/messages' +import { + createEndOfStoredEventsNoticeMessage, + createNoticeMessage, + createOutgoingEventMessage, +} from '../utils/messages' import { IAbortable, IMessageHandler } from '../@types/message-handlers' import { isEventMatchingFilter, isExpiredEvent, toNostrEvent } from '../utils/event' import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream' @@ -55,12 +59,12 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { const sendEOSE = () => this.webSocket.emit(WebSocketAdapterEvent.Message, createEndOfStoredEventsNoticeMessage(subscriptionId)) const isSubscribedToEvent = SubscribeMessageHandler.isClientSubscribedToEvent(filters) - const isNotExpired = (event: Event)=>{ + const isNotExpired = (event: Event) => { if (isExpiredEvent(event)) { return false } return true - } + } const findEvents = this.eventRepository.findByFilters(filters).stream() @@ -79,7 +83,7 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { } catch (error) { if (error instanceof Error && error.name === 'AbortError') { debug('subscription %s aborted: %o', subscriptionId, error) - findEvents.destroy() + findEvents.destroy() } else { debug('error streaming events: %o', error) } @@ -97,13 +101,11 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { const subscriptionLimits = this.settings().limits?.client?.subscription if (existingSubscription?.length && equals(filters, existingSubscription)) { - return `Duplicate subscription ${subscriptionId}: Ignoring` + return `Duplicate subscription ${subscriptionId}: Ignoring` } const maxSubscriptions = subscriptionLimits?.maxSubscriptions ?? 0 - if (maxSubscriptions > 0 - && !existingSubscription?.length && subscriptions.size + 1 > maxSubscriptions - ) { + if (maxSubscriptions > 0 && !existingSubscription?.length && subscriptions.size + 1 > maxSubscriptions) { return `Too many subscriptions: Number of subscriptions must be less than or equal to ${maxSubscriptions}` } @@ -115,13 +117,10 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { } if ( - typeof subscriptionLimits.maxSubscriptionIdLength === 'number' - && subscriptionId.length > subscriptionLimits.maxSubscriptionIdLength + typeof subscriptionLimits.maxSubscriptionIdLength === 'number' && + subscriptionId.length > subscriptionLimits.maxSubscriptionIdLength ) { return `Subscription ID too long: Subscription ID must be less or equal to ${subscriptionLimits.maxSubscriptionIdLength}` } - - } } - diff --git a/src/handlers/unsubscribe-message-handler.ts b/src/handlers/unsubscribe-message-handler.ts index 288dea5f..82f2b18c 100644 --- a/src/handlers/unsubscribe-message-handler.ts +++ b/src/handlers/unsubscribe-message-handler.ts @@ -4,9 +4,7 @@ import { UnsubscribeMessage } from '../@types/messages' import { WebSocketAdapterEvent } from '../constants/adapter' export class UnsubscribeMessageHandler implements IMessageHandler { - public constructor( - private readonly webSocket: IWebSocketAdapter, - ) { } + public constructor(private readonly webSocket: IWebSocketAdapter) {} public async handleMessage(message: UnsubscribeMessage): Promise { this.webSocket.emit(WebSocketAdapterEvent.Unsubscribe, message[1]) diff --git a/src/payments-processors/lnbits-payment-processor.ts b/src/payments-processors/lnbits-payment-processor.ts index 4f7b1e2e..31eaee57 100644 --- a/src/payments-processors/lnbits-payment-processor.ts +++ b/src/payments-processors/lnbits-payment-processor.ts @@ -42,7 +42,7 @@ export class LNbitsCreateInvoiceResponse implements CreateInvoiceResponse { export class LNbitsPaymentsProcessor implements IPaymentsProcessor { public constructor( private httpClient: AxiosInstance, - private settings: Factory + private settings: Factory, ) {} public async getInvoice(invoiceId: string): Promise { @@ -61,7 +61,7 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { invoice.amountPaid = BigInt(Math.floor(data.details.amount / 1000)) } invoice.unit = InvoiceUnit.SATS - invoice.status = data.paid?InvoiceStatus.COMPLETED:InvoiceStatus.PENDING + invoice.status = data.paid ? InvoiceStatus.COMPLETED : InvoiceStatus.PENDING invoice.description = data.details.memo invoice.confirmedAt = data.paid ? new Date(data.details.time * 1000) : null invoice.expiresAt = new Date(data.details.expiry * 1000) @@ -77,16 +77,14 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { public async createInvoice(request: CreateInvoiceRequest): Promise { debug('create invoice: %o', request) - const { - amount: amountMsats, - description, - requestId: internalId, - } = request + const { amount: amountMsats, description, requestId: internalId } = request const callbackURL = new URL(this.settings().paymentsProcessors?.lnbits?.callbackBaseURL) - const hmacExpiry = (Date.now() + (1 * 24 * 60 * 60 * 1000)).toString() - callbackURL.searchParams.set('hmac', hmacExpiry + ':' + - hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), hmacExpiry).toString('hex')) + const hmacExpiry = (Date.now() + 1 * 24 * 60 * 60 * 1000).toString() + callbackURL.searchParams.set( + 'hmac', + hmacExpiry + ':' + hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), hmacExpiry).toString('hex'), + ) const body = { amount: Number(amountMsats / 1000n), @@ -106,9 +104,12 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { debug('response: %o', response.data) - const invoiceResponse = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, { - maxRedirects: 1, - }) + const invoiceResponse = await this.httpClient.get( + `/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, + { + maxRedirects: 1, + }, + ) debug('invoice data response: %o', invoiceResponse.data) const invoice = new LNbitsCreateInvoiceResponse() @@ -118,7 +119,7 @@ export class LNbitsPaymentsProcessor implements IPaymentsProcessor { invoice.bolt11 = data.details.bolt11 invoice.amountRequested = BigInt(Math.floor(data.details.amount / 1000)) invoice.unit = InvoiceUnit.SATS - invoice.status = data.paid?InvoiceStatus.COMPLETED:InvoiceStatus.PENDING + invoice.status = data.paid ? InvoiceStatus.COMPLETED : InvoiceStatus.PENDING invoice.description = data.details.memo invoice.confirmedAt = null invoice.expiresAt = new Date(data.details.expiry * 1000) diff --git a/src/payments-processors/lnurl-payments-processor.ts b/src/payments-processors/lnurl-payments-processor.ts index 30fe6420..4d290e85 100644 --- a/src/payments-processors/lnurl-payments-processor.ts +++ b/src/payments-processors/lnurl-payments-processor.ts @@ -12,7 +12,7 @@ const debug = createLogger('lnurl-payments-processor') export class LnurlPaymentsProcessor implements IPaymentsProcessor { public constructor( private httpClient: AxiosInstance, - private settings: Factory + private settings: Factory, ) {} public async getInvoice(invoice: LnurlInvoice): Promise { @@ -35,14 +35,12 @@ export class LnurlPaymentsProcessor implements IPaymentsProcessor { public async createInvoice(request: CreateInvoiceRequest): Promise { debug('create invoice: %o', request) - const { - amount: amountMsats, - description, - requestId, - } = request + const { amount: amountMsats, description, requestId } = request try { - const response = await this.httpClient.get(`${this.settings().paymentsProcessors?.lnurl?.invoiceURL}/callback?amount=${amountMsats}&comment=${description}`) + const response = await this.httpClient.get( + `${this.settings().paymentsProcessors?.lnurl?.invoiceURL}/callback?amount=${amountMsats}&comment=${description}`, + ) const result = { id: randomUUID(), diff --git a/src/payments-processors/nodeless-payments-processor.ts b/src/payments-processors/nodeless-payments-processor.ts index c9a78e15..1652d86e 100644 --- a/src/payments-processors/nodeless-payments-processor.ts +++ b/src/payments-processors/nodeless-payments-processor.ts @@ -11,7 +11,7 @@ const debug = createLogger('nodeless-payments-processor') export class NodelessPaymentsProcessor implements IPaymentsProcessor { public constructor( private httpClient: AxiosInstance, - private settings: Factory + private settings: Factory, ) {} public async getInvoice(invoiceId: string): Promise { @@ -34,11 +34,7 @@ export class NodelessPaymentsProcessor implements IPaymentsProcessor { public async createInvoice(request: CreateInvoiceRequest): Promise { debug('create invoice: %O', request) - const { - amount: amountMsats, - description, - requestId, - } = request + const { amount: amountMsats, description, requestId } = request const amountSats = Number(amountMsats / 1000n) diff --git a/src/payments-processors/opennode-payments-processor.ts b/src/payments-processors/opennode-payments-processor.ts index 4c006d90..3b1f4aa8 100644 --- a/src/payments-processors/opennode-payments-processor.ts +++ b/src/payments-processors/opennode-payments-processor.ts @@ -11,7 +11,7 @@ const debug = createLogger('opennode-payments-processor') export class OpenNodePaymentsProcessor implements IPaymentsProcessor { public constructor( private httpClient: AxiosInstance, - private settings: Factory + private settings: Factory, ) {} public async getInvoice(invoiceId: string): Promise { @@ -32,11 +32,7 @@ export class OpenNodePaymentsProcessor implements IPaymentsProcessor { public async createInvoice(request: CreateInvoiceRequest): Promise { debug('create invoice: %o', request) - const { - amount: amountMsats, - description, - requestId, - } = request + const { amount: amountMsats, description, requestId } = request const amountSats = Number(amountMsats / 1000n) diff --git a/src/payments-processors/zebedee-payments-processor.ts b/src/payments-processors/zebedee-payments-processor.ts index d24427bd..3a147b42 100644 --- a/src/payments-processors/zebedee-payments-processor.ts +++ b/src/payments-processors/zebedee-payments-processor.ts @@ -11,7 +11,7 @@ const debug = createLogger('zebedee-payments-processor') export class ZebedeePaymentsProcessor implements IPaymentsProcessor { public constructor( private httpClient: AxiosInstance, - private settings: Factory + private settings: Factory, ) {} public async getInvoice(invoiceId: string): Promise { @@ -32,11 +32,7 @@ export class ZebedeePaymentsProcessor implements IPaymentsProcessor { public async createInvoice(request: CreateInvoiceRequest): Promise { debug('create invoice: %o', request) - const { - amount: amountMsats, - description, - requestId, - } = request + const { amount: amountMsats, description, requestId } = request const body = { amount: amountMsats.toString(), diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index 76b8b648..312d98ad 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -28,7 +28,12 @@ import { toPairs, } from 'ramda' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' +import { + ContextMetadataKey, + EventDeduplicationMetadataKey, + EventExpirationTimeMetadataKey, + EventKinds, +} from '../constants/base' import { DatabaseClient, EventId } from '../@types/base' import { DBEvent, Event } from '../@types/event' import { EventPurgeCounts, EventRetentionOptions, IEventRepository, IQueryResult } from '../@types/repositories' @@ -46,8 +51,8 @@ const groupByLengthSpec = groupBy( [equals(64), always('exact')], [even, always('even')], [T, always('odd')], - ]) - ) + ]), + ), ) const debug = createLogger('event-repository') @@ -56,7 +61,7 @@ export class EventRepository implements IEventRepository { public constructor( private readonly masterDbClient: DatabaseClient, private readonly readReplicaDbClient: DatabaseClient, - ) { } + ) {} public findByFilters(filters: SubscriptionFilter[]): IQueryResult { debug('querying for %o', filters) @@ -76,28 +81,23 @@ export class EventRepository implements IEventRepository { groupByLengthSpec, evolve({ exact: (pubkeys: string[]) => - tableFields.forEach((tableField) => - bd.orWhereIn(tableField, pubkeys.map(toBuffer)) - ), + tableFields.forEach((tableField) => bd.orWhereIn(tableField, pubkeys.map(toBuffer))), even: forEach((prefix: string) => tableFields.forEach((tableField) => - bd.orWhereRaw( - `substring("${tableField}" from 1 for ?) = ?`, - [prefix.length >> 1, toBuffer(prefix)] - ) - ) + bd.orWhereRaw(`substring("${tableField}" from 1 for ?) = ?`, [ + prefix.length >> 1, + toBuffer(prefix), + ]), + ), ), odd: forEach((prefix: string) => tableFields.forEach((tableField) => - bd.orWhereRaw( - `substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`, - [ - (prefix.length >> 1) + 1, - `\\x${prefix}0`, - `\\x${prefix}f`, - ], - ) - ) + bd.orWhereRaw(`substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`, [ + (prefix.length >> 1) + 1, + `\\x${prefix}0`, + `\\x${prefix}f`, + ]), + ), ), } as any), ), @@ -140,19 +140,21 @@ export class EventRepository implements IEventRepository { ifElse( isEmpty, () => andWhereRaw('1 = 0', bd), - forEach((criterion: string) => void orWhereRaw( - 'event_tags.tag_name = ? AND event_tags.tag_value = ?', - [filterName[1], criterion], - bd, - )), + forEach( + (criterion: string) => + void orWhereRaw( + 'event_tags.tag_name = ? AND event_tags.tag_value = ?', + [filterName[1], criterion], + bd, + ), + ), )(criteria) }) }), )(currentFilter as any) if (isTagQuery) { - builder.leftJoin('event_tags', 'events.event_id', 'event_tags.event_id') - .select('events.*') + builder.leftJoin('event_tags', 'events.event_id', 'event_tags.event_id').select('events.*') } return builder @@ -206,10 +208,7 @@ export class EventRepository implements IEventRepository { debug('inserting event: %o', event) const row = this.toInsertRow(event) - return this.masterDbClient('events') - .insert(row) - .onConflict() - .ignore() + return this.masterDbClient('events').insert(row).onConflict().ignore() } public upsert(event: Event): Promise { @@ -223,20 +222,25 @@ export class EventRepository implements IEventRepository { // NIP-33: Parameterized Replaceable Events .onConflict( this.masterDbClient.raw( - '(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)' - ) + '(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)', + ), ) .merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row)) .where(function () { - this.where('events.event_created_at', '<', row.event_created_at) - .orWhere(function () { - this.where('events.event_created_at', '=', row.event_created_at) - .andWhere('events.event_id', '>', row.event_id) - }) + this.where('events.event_created_at', '<', row.event_created_at).orWhere(function () { + this.where('events.event_created_at', '=', row.event_created_at).andWhere( + 'events.event_id', + '>', + row.event_id, + ) + }) }) return { - then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + then: ( + onfulfilled: (value: number) => T1 | PromiseLike, + onrejected: (reason: any) => T2 | PromiseLike, + ) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), toString: (): string => query.toString(), } as Promise @@ -253,10 +257,18 @@ export class EventRepository implements IEventRepository { .insert(rows) .onConflict( this.masterDbClient.raw( - '(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)' - ) + '(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)', + ), ) - .merge(['deleted_at', 'event_content', 'event_created_at', 'event_id', 'event_signature', 'event_tags', 'expires_at']) + .merge([ + 'deleted_at', + 'event_content', + 'event_created_at', + 'event_id', + 'event_signature', + 'event_tags', + 'expires_at', + ]) .whereRaw('"events"."event_created_at" < "excluded"."event_created_at"') .then(prop('rowCount') as () => number, () => 0) } @@ -336,25 +348,28 @@ export class EventRepository implements IEventRepository { }) } - const retentionLimit = now - (maxDays * 86400) + const retentionLimit = now - maxDays * 86400 const batchSize = 1000 - debug('deleting expired and retained events (retentionLimit: %d, now: %d, batchSize: %d)', retentionLimit, now, batchSize) + debug( + 'deleting expired and retained events (retentionLimit: %d, now: %d, batchSize: %d)', + retentionLimit, + now, + batchSize, + ) const kindWhitelist = [ ...(Array.isArray(options?.kindWhitelist) ? options.kindWhitelist : []), EventKinds.REQUEST_TO_VANISH, ].reduce<(number | [number, number])[]>((result, item) => { - const key = Array.isArray(item) - ? `range:${item[0]}-${item[1]}` - : `kind:${item}` - - if (!result.some((existing) => { - const existingKey = Array.isArray(existing) - ? `range:${existing[0]}-${existing[1]}` - : `kind:${existing}` - return existingKey === key - })) { + const key = Array.isArray(item) ? `range:${item[0]}-${item[1]}` : `kind:${item}` + + if ( + !result.some((existing) => { + const existingKey = Array.isArray(existing) ? `range:${existing[0]}-${existing[1]}` : `kind:${existing}` + return existingKey === key + }) + ) { result.push(item) } @@ -364,9 +379,7 @@ export class EventRepository implements IEventRepository { const candidates = this.masterDbClient('events') .select('event_id') .where(function () { - this.where('expires_at', '<', now) - .orWhereNotNull('deleted_at') - .orWhere('event_created_at', '<', retentionLimit) + this.where('expires_at', '<', now).orWhereNotNull('deleted_at').orWhere('event_created_at', '<', retentionLimit) }) .modify((query) => { query.whereNot((builder) => { @@ -389,21 +402,27 @@ export class EventRepository implements IEventRepository { .whereIn('event_id', candidates) .del(['deleted_at', 'expires_at', 'event_created_at']) - const mapToCounts = (deletedRows: Pick[]): EventPurgeCounts => deletedRows.reduce((counts, row) => { - if (row.deleted_at) { - counts.deleted += 1 - } else if (typeof row.expires_at === 'number' && row.expires_at < now) { - counts.expired += 1 - } else if (row.event_created_at < retentionLimit) { - counts.retained += 1 - } - - return counts - }, { - deleted: 0, - expired: 0, - retained: 0, - }) + const mapToCounts = ( + deletedRows: Pick[], + ): EventPurgeCounts => + deletedRows.reduce( + (counts, row) => { + if (row.deleted_at) { + counts.deleted += 1 + } else if (typeof row.expires_at === 'number' && row.expires_at < now) { + counts.expired += 1 + } else if (row.event_created_at < retentionLimit) { + counts.retained += 1 + } + + return counts + }, + { + deleted: 0, + expired: 0, + retained: 0, + }, + ) const getPromise = () => query.then((rows: any) => mapToCounts(rows)) diff --git a/src/repositories/invoice-repository.ts b/src/repositories/invoice-repository.ts index 922e69be..f002a150 100644 --- a/src/repositories/invoice-repository.ts +++ b/src/repositories/invoice-repository.ts @@ -1,16 +1,4 @@ -import { - always, - applySpec, - head, - ifElse, - is, - map, - omit, - pipe, - prop, - propSatisfies, - toString, -} from 'ramda' +import { always, applySpec, head, ifElse, is, map, omit, pipe, prop, propSatisfies, toString } from 'ramda' import { DBInvoice, Invoice, InvoiceStatus } from '../@types/invoice' import { fromDBInvoice, toBuffer } from '../utils/transform' @@ -22,7 +10,7 @@ import { randomUUID } from 'crypto' const debug = createLogger('invoice-repository') export class InvoiceRepository implements IInvoiceRepository { - public constructor(private readonly dbClient: DatabaseClient) { } + public constructor(private readonly dbClient: DatabaseClient) {} public async confirmInvoice( invoiceId: string, @@ -33,14 +21,7 @@ export class InvoiceRepository implements IInvoiceRepository { debug('confirming invoice %s at %s: %s', invoiceId, confirmedAt, amountPaid) try { - await client.raw( - 'select confirm_invoice(?, ?, ?)', - [ - invoiceId, - amountPaid.toString(), - confirmedAt.toISOString(), - ] - ) + await client.raw('select confirm_invoice(?, ?, ?)', [invoiceId, amountPaid.toString(), confirmedAt.toISOString()]) } catch (error) { console.error('Unable to confirm invoice. Reason:', error.message) @@ -48,13 +29,8 @@ export class InvoiceRepository implements IInvoiceRepository { } } - public async findById( - id: string, - client: DatabaseClient = this.dbClient, - ): Promise { - const [dbInvoice] = await client('invoices') - .where('id', id) - .select() + public async findById(id: string, client: DatabaseClient = this.dbClient): Promise { + const [dbInvoice] = await client('invoices').where('id', id).select() if (!dbInvoice) { return @@ -63,11 +39,7 @@ export class InvoiceRepository implements IInvoiceRepository { return fromDBInvoice(dbInvoice) } - public async findPendingInvoices( - offset = 0, - limit = 10, - client: DatabaseClient = this.dbClient, - ): Promise { + public async findPendingInvoices(offset = 0, limit = 10, client: DatabaseClient = this.dbClient): Promise { const dbInvoices = await client('invoices') .where('status', InvoiceStatus.PENDING) .offset(offset) @@ -77,10 +49,7 @@ export class InvoiceRepository implements IInvoiceRepository { return dbInvoices.map(fromDBInvoice) } - public updateStatus( - invoice: Invoice, - client: DatabaseClient = this.dbClient, - ): Promise { + public updateStatus(invoice: Invoice, client: DatabaseClient = this.dbClient): Promise { debug('updating invoice status: %o', invoice) const query = client('invoices') @@ -95,17 +64,14 @@ export class InvoiceRepository implements IInvoiceRepository { return { then: ( onfulfilled: (value: Invoice | undefined) => T1 | PromiseLike, - onrejected: (reason: any) => T2 | PromiseLike + onrejected: (reason: any) => T2 | PromiseLike, ) => query.then(pipe(map(fromDBInvoice), head)).then(onfulfilled, onrejected), catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), toString: (): string => query.toString(), } as Promise } - public upsert( - invoice: Invoice, - client: DatabaseClient = this.dbClient - ): Promise { + public upsert(invoice: Invoice, client: DatabaseClient = this.dbClient): Promise { debug('upserting invoice: %o', invoice) const row = applySpec({ @@ -140,11 +106,14 @@ export class InvoiceRepository implements IInvoiceRepository { 'expires_at', 'created_at', 'verify_url', - ])(row) + ])(row), ) return { - then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + then: ( + onfulfilled: (value: number) => T1 | PromiseLike, + onrejected: (reason: any) => T2 | PromiseLike, + ) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), toString: (): string => query.toString(), } as Promise diff --git a/src/repositories/nip05-verification-repository.ts b/src/repositories/nip05-verification-repository.ts index 46ee2911..f8f8678d 100644 --- a/src/repositories/nip05-verification-repository.ts +++ b/src/repositories/nip05-verification-repository.ts @@ -21,9 +21,7 @@ const fromDBNip05Verification = applySpec({ }) export class Nip05VerificationRepository implements INip05VerificationRepository { - public constructor( - private readonly dbClient: DatabaseClient, - ) {} + public constructor(private readonly dbClient: DatabaseClient) {} public async findByPubkey(pubkey: Pubkey): Promise { debug('find by pubkey: %s', pubkey) @@ -56,18 +54,15 @@ export class Nip05VerificationRepository implements INip05VerificationRepository updated_at: now, } - const query = this.dbClient('nip05_verifications') - .insert(row) - .onConflict('pubkey') - .merge({ - nip05: row.nip05, - domain: row.domain, - is_verified: row.is_verified, - last_verified_at: row.last_verified_at, - last_checked_at: row.last_checked_at, - failure_count: row.failure_count, - updated_at: now, - }) + const query = this.dbClient('nip05_verifications').insert(row).onConflict('pubkey').merge({ + nip05: row.nip05, + domain: row.domain, + is_verified: row.is_verified, + last_verified_at: row.last_verified_at, + last_checked_at: row.last_checked_at, + failure_count: row.failure_count, + updated_at: now, + }) return { then: ( @@ -100,8 +95,6 @@ export class Nip05VerificationRepository implements INip05VerificationRepository public async deleteByPubkey(pubkey: Pubkey): Promise { debug('delete by pubkey: %s', pubkey) - return this.dbClient('nip05_verifications') - .where('pubkey', toBuffer(pubkey)) - .delete() + return this.dbClient('nip05_verifications').where('pubkey', toBuffer(pubkey)).delete() } } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index e040468b..9f117b7a 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -5,23 +5,17 @@ import { fromDBUser, toBuffer } from '../utils/transform' import { IEventRepository, IUserRepository } from '../@types/repositories' import { createLogger } from '../factories/logger-factory' - const debug = createLogger('user-repository') export class UserRepository implements IUserRepository { public constructor( private readonly dbClient: DatabaseClient, private readonly eventRepository: IEventRepository, - ) { } + ) {} - public async findByPubkey( - pubkey: Pubkey, - client: DatabaseClient = this.dbClient - ): Promise { + public async findByPubkey(pubkey: Pubkey, client: DatabaseClient = this.dbClient): Promise { debug('find by pubkey: %s', pubkey) - const [dbuser] = await client('users') - .where('pubkey', toBuffer(pubkey)) - .select() + const [dbuser] = await client('users').where('pubkey', toBuffer(pubkey)).select() if (!dbuser) { return @@ -30,10 +24,7 @@ export class UserRepository implements IUserRepository { return fromDBUser(dbuser) } - public async upsert( - user: Partial, - client: DatabaseClient = this.dbClient, - ): Promise { + public async upsert(user: Partial, client: DatabaseClient = this.dbClient): Promise { debug('upsert: %o', user) const date = new Date() @@ -50,16 +41,13 @@ export class UserRepository implements IUserRepository { const query = client('users') .insert(row) .onConflict('pubkey') - .merge( - omit([ - 'pubkey', - 'balance', - 'created_at', - ])(row) - ) + .merge(omit(['pubkey', 'balance', 'created_at'])(row)) return { - then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + then: ( + onfulfilled: (value: number) => T1 | PromiseLike, + onrejected: (reason: any) => T2 | PromiseLike, + ) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), toString: (): string => query.toString(), } as Promise @@ -69,10 +57,7 @@ export class UserRepository implements IUserRepository { * Returns vanish state from users.is_vanished, or lazily hydrates a user row from events once * when no users row exists (single upsert; no duplicate inserts). */ - public async isVanished( - pubkey: Pubkey, - client: DatabaseClient = this.dbClient - ): Promise { + public async isVanished(pubkey: Pubkey, client: DatabaseClient = this.dbClient): Promise { const existing = await this.findByPubkey(pubkey, client) if (existing) { return existing.isVanished @@ -83,19 +68,11 @@ export class UserRepository implements IUserRepository { return vanishedFromEvents } - public setVanished( - pubkey: Pubkey, - vanished: boolean, - client: DatabaseClient = this.dbClient - ): Promise { + public setVanished(pubkey: Pubkey, vanished: boolean, client: DatabaseClient = this.dbClient): Promise { return this.upsertVanishState(pubkey, vanished, client) } - private upsertVanishState( - pubkey: Pubkey, - isVanished: boolean, - client: DatabaseClient, - ): Promise { + private upsertVanishState(pubkey: Pubkey, isVanished: boolean, client: DatabaseClient): Promise { debug('upsert vanish state for %s: %o', pubkey, isVanished) const date = new Date() @@ -115,22 +92,19 @@ export class UserRepository implements IUserRepository { }) return { - then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + then: ( + onfulfilled: (value: number) => T1 | PromiseLike, + onrejected: (reason: any) => T2 | PromiseLike, + ) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), toString: (): string => query.toString(), } as Promise } - public async getBalanceByPubkey( - pubkey: Pubkey, - client: DatabaseClient = this.dbClient - ): Promise { + public async getBalanceByPubkey(pubkey: Pubkey, client: DatabaseClient = this.dbClient): Promise { debug('get balance for pubkey: %s', pubkey) - const [user] = await client('users') - .select('balance') - .where('pubkey', toBuffer(pubkey)) - .limit(1) + const [user] = await client('users').select('balance').where('pubkey', toBuffer(pubkey)).limit(1) if (!user) { return 0n @@ -139,21 +113,11 @@ export class UserRepository implements IUserRepository { return BigInt(user.balance) } - public async admitUser( - pubkey: Pubkey, - admittedAt: Date, - client: DatabaseClient = this.dbClient, - ): Promise { + public async admitUser(pubkey: Pubkey, admittedAt: Date, client: DatabaseClient = this.dbClient): Promise { debug('admit user: %s at %s', pubkey, admittedAt) try { - await client.raw( - 'select admit_user(?, ?)', - [ - toBuffer(pubkey), - admittedAt.toISOString(), - ] - ) + await client.raw('select admit_user(?, ?)', [toBuffer(pubkey), admittedAt.toISOString()]) } catch (error) { console.error('Unable to admit user. Reason:', error.message) diff --git a/src/routes/admissions/index.ts b/src/routes/admissions/index.ts index 8e63fb89..70844731 100644 --- a/src/routes/admissions/index.ts +++ b/src/routes/admissions/index.ts @@ -4,7 +4,6 @@ import { withController } from '../../handlers/request-handlers/with-controller- const admissionRouter = Router() -admissionRouter - .get('/check/:pubkey', withController(createGetAdmissionCheckController)) +admissionRouter.get('/check/:pubkey', withController(createGetAdmissionCheckController)) -export default admissionRouter \ No newline at end of file +export default admissionRouter diff --git a/src/routes/callbacks/index.ts b/src/routes/callbacks/index.ts index 5b86e0bf..d522e31c 100644 --- a/src/routes/callbacks/index.ts +++ b/src/routes/callbacks/index.ts @@ -11,11 +11,15 @@ const router = Router() router .post('/zebedee', json(), withController(createZebedeeCallbackController)) .post('/lnbits', json(), withController(createLNbitsCallbackController)) - .post('/nodeless', json({ - verify(req, _res, buf) { - (req as any).rawBody = buf - }, - }), withController(createNodelessCallbackController)) + .post( + '/nodeless', + json({ + verify(req, _res, buf) { + ;(req as any).rawBody = buf + }, + }), + withController(createNodelessCallbackController), + ) .post('/opennode', json(), withController(createOpenNodeCallbackController)) export default router diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts index beefa741..5592d964 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -12,4 +12,4 @@ invoiceRouter .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) .post('/', urlencoded({ extended: true }), withController(createPostInvoiceController)) -export default invoiceRouter \ No newline at end of file +export default invoiceRouter diff --git a/src/schemas/base-schema.ts b/src/schemas/base-schema.ts index 9bb2cac4..5af6c308 100644 --- a/src/schemas/base-schema.ts +++ b/src/schemas/base-schema.ts @@ -14,10 +14,11 @@ export const signatureSchema = z.string().regex(lowerHexRegex).length(128) export const subscriptionSchema = z.string().min(1) -export const createdAtSchema = z.number().int().min(0).refine( - (value) => Number.isSafeInteger(value) && Math.log10(value) < 10, - { message: 'Invalid timestamp' } -) +export const createdAtSchema = z + .number() + .int() + .min(0) + .refine((value) => Number.isSafeInteger(value) && Math.log10(value) < 10, { message: 'Invalid timestamp' }) // [, 0..*] export const tagSchema = z.tuple([z.string().min(1)]).rest(z.string()) diff --git a/src/schemas/event-schema.ts b/src/schemas/event-schema.ts index 18223645..6aa2cf22 100644 --- a/src/schemas/event-schema.ts +++ b/src/schemas/event-schema.ts @@ -1,14 +1,6 @@ import { z } from 'zod' -import { - createdAtSchema, - idSchema, - kindSchema, - pubkeySchema, - signatureSchema, - tagSchema, -} from './base-schema' - +import { createdAtSchema, idSchema, kindSchema, pubkeySchema, signatureSchema, tagSchema } from './base-schema' /** * { @@ -25,13 +17,15 @@ import { * "sig": <64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field>, * } */ -export const eventSchema = z.object({ - // NIP-01 - id: idSchema, - pubkey: pubkeySchema, - created_at: createdAtSchema, - kind: kindSchema, - tags: z.array(tagSchema), - content: z.string(), - sig: signatureSchema, -}).strict() +export const eventSchema = z + .object({ + // NIP-01 + id: idSchema, + pubkey: pubkeySchema, + created_at: createdAtSchema, + kind: kindSchema, + tags: z.array(tagSchema), + content: z.string(), + sig: signatureSchema, + }) + .strict() diff --git a/src/schemas/filter-schema.ts b/src/schemas/filter-schema.ts index f03ad663..dc1baa27 100644 --- a/src/schemas/filter-schema.ts +++ b/src/schemas/filter-schema.ts @@ -4,21 +4,24 @@ import { createdAtSchema, kindSchema, prefixSchema } from './base-schema' const knownFilterKeys = new Set(['ids', 'authors', 'kinds', 'since', 'until', 'limit']) -export const filterSchema = z.object({ - ids: z.array(prefixSchema).optional(), - authors: z.array(prefixSchema).optional(), - kinds: z.array(kindSchema).optional(), - since: createdAtSchema.optional(), - until: createdAtSchema.optional(), - limit: z.number().int().min(0).optional(), -}).catchall(z.array(z.string().min(1).max(1024))).superRefine((data, ctx) => { - for (const key of Object.keys(data)) { - if (!knownFilterKeys.has(key) && !/^#[a-z]$/.test(key)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Unknown key: ${key}`, - path: [key], - }) +export const filterSchema = z + .object({ + ids: z.array(prefixSchema).optional(), + authors: z.array(prefixSchema).optional(), + kinds: z.array(kindSchema).optional(), + since: createdAtSchema.optional(), + until: createdAtSchema.optional(), + limit: z.number().int().min(0).optional(), + }) + .catchall(z.array(z.string().min(1).max(1024))) + .superRefine((data, ctx) => { + for (const key of Object.keys(data)) { + if (!knownFilterKeys.has(key) && !/^#[a-z]$/.test(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Unknown key: ${key}`, + path: [key], + }) + } } - } -}) + }) diff --git a/src/schemas/lnbits-callback-schema.ts b/src/schemas/lnbits-callback-schema.ts index 05913a9c..e92d3508 100644 --- a/src/schemas/lnbits-callback-schema.ts +++ b/src/schemas/lnbits-callback-schema.ts @@ -1,10 +1,14 @@ import { idSchema } from './base-schema' import { z } from 'zod' -export const lnbitsCallbackQuerySchema = z.object({ - hmac: z.string().regex(/^[0-9]{1,20}:[0-9a-f]{64}$/), -}).strict() +export const lnbitsCallbackQuerySchema = z + .object({ + hmac: z.string().regex(/^[0-9]{1,20}:[0-9a-f]{64}$/), + }) + .strict() -export const lnbitsCallbackBodySchema = z.object({ - payment_hash: idSchema, -}).strict() +export const lnbitsCallbackBodySchema = z + .object({ + payment_hash: idSchema, + }) + .strict() diff --git a/src/schemas/message-schema.ts b/src/schemas/message-schema.ts index 9dcaf847..28a1ae75 100644 --- a/src/schemas/message-schema.ts +++ b/src/schemas/message-schema.ts @@ -5,41 +5,31 @@ import { filterSchema } from './filter-schema' import { MessageType } from '../@types/messages' import { subscriptionSchema } from './base-schema' -export const eventMessageSchema = z.tuple([ - z.literal(MessageType.EVENT), - eventSchema, -]) +export const eventMessageSchema = z.tuple([z.literal(MessageType.EVENT), eventSchema]) -export const reqMessageSchema = z.tuple([ - z.literal(MessageType.REQ), - z.string().max(256).min(1), -]).rest(filterSchema).superRefine((val, ctx) => { - if (val.length < 3) { - ctx.addIssue({ - code: z.ZodIssueCode.too_small, - minimum: 3, - type: 'array', - inclusive: true, - message: 'REQ message must contain at least one filter', - }) - } else if (val.length > 12) { - ctx.addIssue({ - code: z.ZodIssueCode.too_big, - maximum: 12, - type: 'array', - inclusive: true, - message: 'REQ message must contain at most 12 elements', - }) - } -}) +export const reqMessageSchema = z + .tuple([z.literal(MessageType.REQ), z.string().max(256).min(1)]) + .rest(filterSchema) + .superRefine((val, ctx) => { + if (val.length < 3) { + ctx.addIssue({ + code: z.ZodIssueCode.too_small, + minimum: 3, + type: 'array', + inclusive: true, + message: 'REQ message must contain at least one filter', + }) + } else if (val.length > 12) { + ctx.addIssue({ + code: z.ZodIssueCode.too_big, + maximum: 12, + type: 'array', + inclusive: true, + message: 'REQ message must contain at most 12 elements', + }) + } + }) -export const closeMessageSchema = z.tuple([ - z.literal(MessageType.CLOSE), - subscriptionSchema, -]) +export const closeMessageSchema = z.tuple([z.literal(MessageType.CLOSE), subscriptionSchema]) -export const messageSchema = z.union([ - eventMessageSchema, - reqMessageSchema, - closeMessageSchema, -]) +export const messageSchema = z.union([eventMessageSchema, reqMessageSchema, closeMessageSchema]) diff --git a/src/schemas/nodeless-callback-schema.ts b/src/schemas/nodeless-callback-schema.ts index 58d1fe96..8413c88d 100644 --- a/src/schemas/nodeless-callback-schema.ts +++ b/src/schemas/nodeless-callback-schema.ts @@ -1,15 +1,19 @@ import { pubkeySchema } from './base-schema' import { z } from 'zod' -export const nodelessCallbackBodySchema = z.object({ - id: z.string().optional(), - uuid: z.string(), - status: z.string(), - amount: z.number(), - metadata: z.object({ - requestId: pubkeySchema, - description: z.string().optional(), - unit: z.string().optional(), - createdAt: z.union([z.string(), z.date()]).optional(), - }).passthrough(), -}).strict() +export const nodelessCallbackBodySchema = z + .object({ + id: z.string().optional(), + uuid: z.string(), + status: z.string(), + amount: z.number(), + metadata: z + .object({ + requestId: pubkeySchema, + description: z.string().optional(), + unit: z.string().optional(), + createdAt: z.union([z.string(), z.date()]).optional(), + }) + .passthrough(), + }) + .strict() diff --git a/src/schemas/opennode-callback-schema.ts b/src/schemas/opennode-callback-schema.ts index e94585cc..e11bdffd 100644 --- a/src/schemas/opennode-callback-schema.ts +++ b/src/schemas/opennode-callback-schema.ts @@ -1,20 +1,28 @@ import { pubkeySchema } from './base-schema' import { z } from 'zod' -export const opennodeCallbackBodySchema = z.object({ - id: z.string(), - status: z.string(), - order_id: pubkeySchema, - description: z.string().or(z.literal('')).optional(), - amount: z.number().optional(), - price: z.number().optional(), - created_at: z.union([z.number(), z.string()]).optional(), - lightning_invoice: z.object({ - payreq: z.string().optional(), - expires_at: z.number().optional(), - }).passthrough().optional(), - lightning: z.object({ - payreq: z.string().optional(), - expires_at: z.string().optional(), - }).passthrough().optional(), -}).passthrough() +export const opennodeCallbackBodySchema = z + .object({ + id: z.string(), + status: z.string(), + order_id: pubkeySchema, + description: z.string().or(z.literal('')).optional(), + amount: z.number().optional(), + price: z.number().optional(), + created_at: z.union([z.number(), z.string()]).optional(), + lightning_invoice: z + .object({ + payreq: z.string().optional(), + expires_at: z.number().optional(), + }) + .passthrough() + .optional(), + lightning: z + .object({ + payreq: z.string().optional(), + expires_at: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough() diff --git a/src/schemas/zebedee-callback-schema.ts b/src/schemas/zebedee-callback-schema.ts index 29e3a64d..9eb01dc7 100644 --- a/src/schemas/zebedee-callback-schema.ts +++ b/src/schemas/zebedee-callback-schema.ts @@ -1,17 +1,21 @@ import { pubkeySchema } from './base-schema' import { z } from 'zod' -export const zebedeeCallbackBodySchema = z.object({ - id: z.string(), - status: z.string(), - internalId: pubkeySchema, - amount: z.union([z.string(), z.number()]), - description: z.string(), - unit: z.string(), - expiresAt: z.string().optional(), - confirmedAt: z.string().optional(), - createdAt: z.string().optional(), - invoice: z.object({ - request: z.string(), - }).strict(), -}).passthrough() +export const zebedeeCallbackBodySchema = z + .object({ + id: z.string(), + status: z.string(), + internalId: pubkeySchema, + amount: z.union([z.string(), z.number()]), + description: z.string(), + unit: z.string(), + expiresAt: z.string().optional(), + confirmedAt: z.string().optional(), + createdAt: z.string().optional(), + invoice: z + .object({ + request: z.string(), + }) + .strict(), + }) + .passthrough() diff --git a/src/scripts/export-events.ts b/src/scripts/export-events.ts index 9246473a..6893da6f 100644 --- a/src/scripts/export-events.ts +++ b/src/scripts/export-events.ts @@ -37,15 +37,10 @@ async function exportEvents(): Promise { abortController.abort() } - process - .on('SIGINT', onSignal) - .on('SIGTERM', onSignal) + process.on('SIGINT', onSignal).on('SIGTERM', onSignal) try { - const firstEvent = await db('events') - .select('event_id') - .whereNull('deleted_at') - .first() + const firstEvent = await db('events').select('event_id').whereNull('deleted_at').first() if (abortController.signal.aborted) { return @@ -112,9 +107,7 @@ async function exportEvents(): Promise { throw error } finally { - process - .off('SIGINT', onSignal) - .off('SIGTERM', onSignal) + process.off('SIGINT', onSignal).off('SIGTERM', onSignal) await db.destroy() } diff --git a/src/services/event-import-service.ts b/src/services/event-import-service.ts index 83855a93..25e6df1c 100644 --- a/src/services/event-import-service.ts +++ b/src/services/event-import-service.ts @@ -26,9 +26,10 @@ const enrichEventMetadata = (event: Event): Event => { } if (isParameterizedReplaceableEvent(event)) { - const [, ...deduplication] = event.tags.find( - (tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication, - ) ?? [null, ''] + const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [ + null, + '', + ] enriched = { ...enriched, [EventDeduplicationMetadataKey]: deduplication } } @@ -65,75 +66,65 @@ const getErrorMessage = (error: unknown): string => { export const createEventBatchPersister = (eventRepository: IEventRepository) => - async (events: Event[]): Promise => { - if (!events.length) { - return 0 - } - - let inserted = 0 + async (events: Event[]): Promise => { + if (!events.length) { + return 0 + } - const regularEvents: Event[] = [] - const replaceableEvents: Event[] = [] + let inserted = 0 - for (const event of events) { - if (isEphemeralEvent(event)) { - continue - } + const regularEvents: Event[] = [] + const replaceableEvents: Event[] = [] - if (isDeleteEvent(event)) { - // flush pending batches before applying deletes - inserted += await eventRepository.createMany(regularEvents.splice(0)) - inserted += await eventRepository.upsertMany(replaceableEvents.splice(0)) - - const eventIdsToDelete = event.tags.reduce( - (ids, tag) => - tag.length >= 2 - && tag[0] === EventTags.Event - && /^[0-9a-f]{64}$/.test(tag[1]) - ? [...ids, tag[1]] - : ids, - [] as string[] - ) - - if (eventIdsToDelete.length) { - await eventRepository.deleteByPubkeyAndIds(event.pubkey, eventIdsToDelete) - } + for (const event of events) { + if (isEphemeralEvent(event)) { + continue + } - inserted += await eventRepository.create(enrichEventMetadata(event)) - continue - } + if (isDeleteEvent(event)) { + // flush pending batches before applying deletes + inserted += await eventRepository.createMany(regularEvents.splice(0)) + inserted += await eventRepository.upsertMany(replaceableEvents.splice(0)) - const enrichedEvent = enrichEventMetadata(event) + const eventIdsToDelete = event.tags.reduce( + (ids, tag) => + tag.length >= 2 && tag[0] === EventTags.Event && /^[0-9a-f]{64}$/.test(tag[1]) ? [...ids, tag[1]] : ids, + [] as string[], + ) - if (isReplaceableEvent(event) || isParameterizedReplaceableEvent(event)) { - replaceableEvents.push(enrichedEvent) - continue + if (eventIdsToDelete.length) { + await eventRepository.deleteByPubkeyAndIds(event.pubkey, eventIdsToDelete) } - regularEvents.push(enrichedEvent) + inserted += await eventRepository.create(enrichEventMetadata(event)) + continue } - // flush remaining - inserted += await eventRepository.createMany(regularEvents) - inserted += await eventRepository.upsertMany(replaceableEvents) + const enrichedEvent = enrichEventMetadata(event) - return inserted + if (isReplaceableEvent(event) || isParameterizedReplaceableEvent(event)) { + replaceableEvents.push(enrichedEvent) + continue + } + + regularEvents.push(enrichedEvent) } + // flush remaining + inserted += await eventRepository.createMany(regularEvents) + inserted += await eventRepository.upsertMany(replaceableEvents) + + return inserted + } + export class EventImportService { - public constructor( - private readonly persistBatch: (events: Event[]) => Promise, - ) {} - - public async importFromJsonl( - filePath: string, - options: EventImportOptions = {}, - ): Promise { - const batchSize = ( - typeof options.batchSize === 'number' - && Number.isInteger(options.batchSize) - && options.batchSize > 0 - ) ? options.batchSize : DEFAULT_BATCH_SIZE + public constructor(private readonly persistBatch: (events: Event[]) => Promise) {} + + public async importFromJsonl(filePath: string, options: EventImportOptions = {}): Promise { + const batchSize = + typeof options.batchSize === 'number' && Number.isInteger(options.batchSize) && options.batchSize > 0 + ? options.batchSize + : DEFAULT_BATCH_SIZE const onLineError = options.onLineError ?? (() => undefined) const onProgress = options.onProgress ?? (() => undefined) @@ -159,9 +150,7 @@ export class EventImportService { const inserted = await this.persistBatch(batch) if (!Number.isInteger(inserted) || inserted < 0 || inserted > batchSize) { - throw new Error( - `Invalid insert count (${inserted}) for batch size ${batchSize}`, - ) + throw new Error(`Invalid insert count (${inserted}) for batch size ${batchSize}`) } stats.inserted += inserted @@ -195,11 +184,11 @@ export class EventImportService { try { event = validateEventSchema(JSON.parse(trimmedLine)) as Event - if (!await isEventIdValid(event)) { + if (!(await isEventIdValid(event))) { throw new Error('invalid: event id does not match') } - if (!await isEventSignatureValid(event)) { + if (!(await isEventSignatureValid(event))) { throw new Error('invalid: event signature verification failed') } } catch (error) { diff --git a/src/services/maintenance-service.ts b/src/services/maintenance-service.ts index 0fd36f28..c873a821 100644 --- a/src/services/maintenance-service.ts +++ b/src/services/maintenance-service.ts @@ -29,7 +29,9 @@ export class MaintenanceService implements IMaintenanceService { }) const totalDeleted = deletedCounts.deleted + deletedCounts.expired + deletedCounts.retained if (totalDeleted > 0) { - console.info(`[Maintenance] Deleted events: deleted=${deletedCounts.deleted}, expired=${deletedCounts.expired}, retained=${deletedCounts.retained}.`) + console.info( + `[Maintenance] Deleted events: deleted=${deletedCounts.deleted}, expired=${deletedCounts.expired}, retained=${deletedCounts.retained}.`, + ) } } catch (error) { console.error('Unable to purge events. Reason:', error) diff --git a/src/services/payments-service.ts b/src/services/payments-service.ts index 40233314..237e8907 100644 --- a/src/services/payments-service.ts +++ b/src/services/payments-service.ts @@ -21,7 +21,7 @@ export class PaymentsService implements IPaymentsService { private readonly userRepository: IUserRepository, private readonly invoiceRepository: IInvoiceRepository, private readonly eventRepository: IEventRepository, - private readonly settings: () => Settings + private readonly settings: () => Settings, ) {} public async getPendingInvoices(): Promise { @@ -38,7 +38,7 @@ export class PaymentsService implements IPaymentsService { public async getInvoiceFromPaymentsProcessor(invoice: Invoice | string): Promise> { try { return await this.paymentsProcessor.getInvoice( - typeof invoice === 'string' || invoice?.verifyURL ? invoice : invoice.id + typeof invoice === 'string' || invoice?.verifyURL ? invoice : invoice.id, ) } catch (error) { console.log('Unable to get invoice from payments processor. Reason:', error) @@ -47,11 +47,7 @@ export class PaymentsService implements IPaymentsService { } } - public async createInvoice( - pubkey: Pubkey, - amount: bigint, - description: string, - ): Promise { + public async createInvoice(pubkey: Pubkey, amount: bigint, description: string): Promise { debug('create invoice for %s for %s: %s', pubkey, amount.toString(), description) const transaction = new Transaction(this.dbClient) @@ -60,13 +56,11 @@ export class PaymentsService implements IPaymentsService { await this.userRepository.upsert({ pubkey }, transaction.transaction) - const invoiceResponse = await this.paymentsProcessor.createInvoice( - { - amount, - description, - requestId: pubkey, - }, - ) + const invoiceResponse = await this.paymentsProcessor.createInvoice({ + amount, + description, + requestId: pubkey, + }) const date = new Date() @@ -133,9 +127,7 @@ export class PaymentsService implements IPaymentsService { } } - public async confirmInvoice( - invoice: Invoice, - ): Promise { + public async confirmInvoice(invoice: Invoice): Promise { debug('confirm invoice %s: %O', invoice.id, invoice) const transaction = new Transaction(this.dbClient) @@ -158,7 +150,7 @@ export class PaymentsService implements IPaymentsService { invoice.id, invoice.amountPaid, invoice.confirmedAt, - transaction.transaction + transaction.transaction, ) const currentSettings = this.settings() @@ -171,25 +163,17 @@ export class PaymentsService implements IPaymentsService { amountPaidMsat *= 1000n * 100000000n } - const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled - && !feeSchedule.whitelists?.pubkeys?.some((prefix) => invoice.pubkey.startsWith(prefix)) + const isApplicableFee = (feeSchedule: FeeSchedule) => + feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => invoice.pubkey.startsWith(prefix)) const admissionFeeSchedules = currentSettings.payments?.feeSchedules?.admission ?? [] - const admissionFeeAmount = admissionFeeSchedules - .reduce((sum, feeSchedule) => { - return sum + (isApplicableFee(feeSchedule) ? BigInt(feeSchedule.amount) : 0n) - }, 0n) - - if ( - admissionFeeAmount > 0n - && amountPaidMsat >= admissionFeeAmount - ) { - const date = new Date() - await this.userRepository.admitUser( - invoice.pubkey, - date, - transaction.transaction, - ) - } + const admissionFeeAmount = admissionFeeSchedules.reduce((sum, feeSchedule) => { + return sum + (isApplicableFee(feeSchedule) ? BigInt(feeSchedule.amount) : 0n) + }, 0n) + + if (admissionFeeAmount > 0n && amountPaidMsat >= admissionFeeAmount) { + const date = new Date() + await this.userRepository.admitUser(invoice.pubkey, date, transaction.transaction) + } await transaction.commit() } catch (error) { @@ -205,9 +189,7 @@ export class PaymentsService implements IPaymentsService { const currentSettings = this.settings() const { - info: { - relay_url: relayUrl, - }, + info: { relay_url: relayUrl }, } = currentSettings const relayPrivkey = getRelayPrivateKey(relayUrl) diff --git a/src/tor/client.ts b/src/tor/client.ts index b3fd89e1..6619ed1f 100644 --- a/src/tor/client.ts +++ b/src/tor/client.ts @@ -6,14 +6,10 @@ import { Tor } from 'tor-control-ts' import { createLogger } from '../factories/logger-factory' import { TorConfig } from '../@types/tor' - const debug = createLogger('tor-client') const getPrivateKeyFile = () => { - return join( - process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'), - 'v3_onion_private_key' - ) + return join(process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'), 'v3_onion_private_key') } export const createTorConfig = (): TorConfig => { @@ -34,9 +30,9 @@ export const getTorClient = async () => { if (config.host !== undefined) { debug('connecting') client = new Tor(config) - try{ + try { await client.connect() - }catch(_error){ + } catch (_error) { client = undefined } debug('connected') @@ -47,16 +43,12 @@ export const getTorClient = async () => { } export const closeTorClient = async () => { if (client) { - await client.quit() client = undefined } } -export const addOnion = async ( - port: number, - host?: string -): Promise => { +export const addOnion = async (port: number, host?: string): Promise => { let privateKey = null const path = getPrivateKeyFile() @@ -82,10 +74,10 @@ export const addOnion = async ( await writeFile(path, hiddenService.PrivateKey, 'utf8') return hiddenService.ServiceID - }else{ + } else { throw new Error(JSON.stringify(hiddenService)) } - }else{ + } else { throw new Error('not connect') } } diff --git a/src/utils/event.ts b/src/utils/event.ts index 2d6bc2ca..54288269 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -31,76 +31,68 @@ export const toNostrEvent: (event: DBEvent) => Event = applySpec({ sig: pipe(prop('event_signature') as () => Buffer, fromBuffer), }) -export const isEventKindOrRangeMatch = ({ kind }: Event) => +export const isEventKindOrRangeMatch = + ({ kind }: Event) => (item: EventKinds | EventKindsRange) => - typeof item === 'number' - ? item === kind - : kind >= item[0] && kind <= item[1] + typeof item === 'number' ? item === kind : kind >= item[0] && kind <= item[1] -export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => { - const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix) +export const isEventMatchingFilter = + (filter: SubscriptionFilter) => + (event: Event): boolean => { + const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix) - // NIP-01: Basic protocol flow description + // NIP-01: Basic protocol flow description - if (Array.isArray(filter.ids) && ( - !filter.ids.some(startsWith(event.id)) - )) { - return false - } + if (Array.isArray(filter.ids) && !filter.ids.some(startsWith(event.id))) { + return false + } - if (Array.isArray(filter.kinds) && !filter.kinds.includes(event.kind)) { - return false - } + if (Array.isArray(filter.kinds) && !filter.kinds.includes(event.kind)) { + return false + } - if (typeof filter.since === 'number' && event.created_at < filter.since) { - return false - } + if (typeof filter.since === 'number' && event.created_at < filter.since) { + return false + } - if (typeof filter.until === 'number' && event.created_at > filter.until) { - return false - } + if (typeof filter.until === 'number' && event.created_at > filter.until) { + return false + } + + if (Array.isArray(filter.authors)) { + if (!filter.authors.some(startsWith(event.pubkey))) { + return false + } + } + + // NIP-27: Multicast + // const targetMulticastGroups: string[] = event.tags.reduce( + // (acc, tag) => (tag[0] === EventTags.Multicast) + // ? [...acc, tag[1]] + // : acc, + // [] as string[] + // ) + + // if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) { + // return false + // } + + // NIP-01: Support #e and #p tags + // NIP-12: Support generic tag queries - if (Array.isArray(filter.authors)) { if ( - !filter.authors.some(startsWith(event.pubkey)) + Object.entries(filter) + .filter(([key, criteria]) => isGenericTagQuery(key) && Array.isArray(criteria)) + .some(([key, criteria]) => { + return !event.tags.some((tag) => tag[0] === key[1] && criteria.includes(tag[1])) + }) ) { return false } - } - // NIP-27: Multicast - // const targetMulticastGroups: string[] = event.tags.reduce( - // (acc, tag) => (tag[0] === EventTags.Multicast) - // ? [...acc, tag[1]] - // : acc, - // [] as string[] - // ) - - // if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) { - // return false - // } - - // NIP-01: Support #e and #p tags - // NIP-12: Support generic tag queries - - if ( - Object.entries(filter) - .filter( - ([key, criteria]) => - isGenericTagQuery(key) && Array.isArray(criteria), - ) - .some(([key, criteria]) => { - return !event.tags.some( - (tag) => tag[0] === key[1] && criteria.includes(tag[1]), - ) - }) - ) { - return false + return true } - return true -} - export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise => { const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event)))) @@ -108,7 +100,7 @@ export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEv } export const isEventIdValid = async (event: Event): Promise => { - return event.id === await getEventHash(event) + return event.id === (await getEventHash(event)) } export const isEventSignatureValid = async (event: Event): Promise => { @@ -149,10 +141,12 @@ export const getPublicKey = (privkey: string) => { return publicKeyCache[privkey] } -export const signEvent = (privkey: string | Buffer | undefined) => async (event: UnsignedEvent): Promise => { - const sig = await secp256k1.schnorr.sign(event.id, privkey as any) - return { ...event, sig: Buffer.from(sig).toString('hex') } -} +export const signEvent = + (privkey: string | Buffer | undefined) => + async (event: UnsignedEvent): Promise => { + const sig = await secp256k1.schnorr.sign(event.id, privkey as any) + return { ...event, sig: Buffer.from(sig).toString('hex') } + } export const broadcastEvent = async (event: Event): Promise => { return new Promise((resolve, reject) => { @@ -178,10 +172,12 @@ export const broadcastEvent = async (event: Event): Promise => { } export const isReplaceableEvent = (event: Event): boolean => { - return event.kind === EventKinds.SET_METADATA - || event.kind === EventKinds.CONTACT_LIST - || event.kind === EventKinds.CHANNEL_METADATA - || (event.kind >= EventKinds.REPLACEABLE_FIRST && event.kind <= EventKinds.REPLACEABLE_LAST) + return ( + event.kind === EventKinds.SET_METADATA || + event.kind === EventKinds.CONTACT_LIST || + event.kind === EventKinds.CHANNEL_METADATA || + (event.kind >= EventKinds.REPLACEABLE_FIRST && event.kind <= EventKinds.REPLACEABLE_LAST) + ) } export const isEphemeralEvent = (event: Event): boolean => { @@ -189,8 +185,9 @@ export const isEphemeralEvent = (event: Event): boolean => { } export const isParameterizedReplaceableEvent = (event: Event): boolean => { - return event.kind >= EventKinds.PARAMETERIZED_REPLACEABLE_FIRST - && event.kind <= EventKinds.PARAMETERIZED_REPLACEABLE_LAST + return ( + event.kind >= EventKinds.PARAMETERIZED_REPLACEABLE_FIRST && event.kind <= EventKinds.PARAMETERIZED_REPLACEABLE_LAST + ) } export const isDeleteEvent = (event: Event): boolean => { @@ -206,9 +203,7 @@ export const isRequestToVanishEvent = (event: Event, relayUrl?: string): boolean return true } - const relayTags = event.tags - .filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay) - .map((tag) => tag[1]) + const relayTags = event.tags.filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay).map((tag) => tag[1]) return relayTags.length > 0 && relayTags.every((relay) => relay === relayUrl || relay === ALL_RELAYS) } @@ -237,7 +232,7 @@ export const getEventExpiration = (event: Event): number | undefined => { const expirationTime = Number(rawExpirationTime) - if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime) < 10)) { + if (Number.isSafeInteger(expirationTime) && Math.log10(expirationTime) < 10) { return expirationTime } } diff --git a/src/utils/html.ts b/src/utils/html.ts index 45eb0c9a..2903a47a 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -9,8 +9,7 @@ const HTML_ESCAPES: Record = { /** * Escape a string for safe interpolation into HTML text or attribute values. */ -export const escapeHtml = (value: string): string => - value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]) +export const escapeHtml = (value: string): string => value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]) /** * Serialize a value for safe embedding inside an inline ')).to.equal( - '<script>alert("it's a & test")</script>' + '<script>alert("it's a & test")</script>', ) }) diff --git a/test/unit/utils/http.spec.ts b/test/unit/utils/http.spec.ts index 2a7ef9bb..f00c50a6 100644 --- a/test/unit/utils/http.spec.ts +++ b/test/unit/utils/http.spec.ts @@ -22,29 +22,14 @@ describe('getRemoteAddress', () => { }) it('returns address using network.remote_ip_address when set', () => { - expect( - getRemoteAddress( - request, - { network: { 'remote_ip_header': header } } as any, - ) - ).to.equal(address) + expect(getRemoteAddress(request, { network: { remote_ip_header: header } } as any)).to.equal(address) }) it('returns address using network.remoteIpAddress when set', () => { - expect( - getRemoteAddress( - request, - { network: { remoteIpHeader: header } } as any, - ) - ).to.equal(address) + expect(getRemoteAddress(request, { network: { remoteIpHeader: header } } as any)).to.equal(address) }) it('returns address from socket when header is unset', () => { - expect( - getRemoteAddress( - request, - { network: { } } as any, - ) - ).to.equal(socketAddress) + expect(getRemoteAddress(request, { network: {} } as any)).to.equal(socketAddress) }) }) diff --git a/test/unit/utils/messages.spec.ts b/test/unit/utils/messages.spec.ts index 84960d91..1ca17ef5 100644 --- a/test/unit/utils/messages.spec.ts +++ b/test/unit/utils/messages.spec.ts @@ -22,7 +22,11 @@ describe('createOutgoingEventMessage', () => { const event: Event = { id: 'some id', } as any - expect(createOutgoingEventMessage('subscriptionId', event)).to.deep.equal([MessageType.EVENT, 'subscriptionId', event]) + expect(createOutgoingEventMessage('subscriptionId', event)).to.deep.equal([ + MessageType.EVENT, + 'subscriptionId', + event, + ]) }) }) @@ -36,12 +40,7 @@ describe('createEndOfStoredEventsNoticeMessage', () => { describe('createCommandResult', () => { it('returns an OK message with success=true and a reason', () => { const eventId = 'b1601d26958e6508b7b9df0af609c652346c09392b6534d93aead9819a51b4ef' - expect(createCommandResult(eventId, true, '')).to.deep.equal([ - MessageType.OK, - eventId, - true, - '', - ]) + expect(createCommandResult(eventId, true, '')).to.deep.equal([MessageType.OK, eventId, true, '']) }) it('returns an OK message with success=false and a rejection reason', () => { @@ -95,11 +94,6 @@ describe('createRelayedEventMessage', () => { }) it('returns an EVENT message with secret appended when a secret is provided', () => { - expect(createRelayedEventMessage(event, 'my-secret')).to.deep.equal([ - MessageType.EVENT, - event, - 'my-secret', - ]) + expect(createRelayedEventMessage(event, 'my-secret')).to.deep.equal([MessageType.EVENT, event, 'my-secret']) }) }) - diff --git a/test/unit/utils/nip05.spec.ts b/test/unit/utils/nip05.spec.ts index 67ff3fa5..e5d47790 100644 --- a/test/unit/utils/nip05.spec.ts +++ b/test/unit/utils/nip05.spec.ts @@ -211,8 +211,12 @@ describe('NIP-05 utils', () => { const config = axiosGetStub.firstCall.args[1] expect(config.maxRedirects).to.equal(1) - expect(config.maxContentLength).to.be.a('number').and.to.be.at.most(64 * 1024) - expect(config.maxBodyLength).to.be.a('number').and.to.be.at.most(64 * 1024) + expect(config.maxContentLength) + .to.be.a('number') + .and.to.be.at.most(64 * 1024) + expect(config.maxBodyLength) + .to.be.a('number') + .and.to.be.at.most(64 * 1024) expect(config.validateStatus(200)).to.be.true expect(config.validateStatus(301)).to.be.false expect(typeof config.beforeRedirect).to.equal('function') diff --git a/test/unit/utils/nip44.spec.ts b/test/unit/utils/nip44.spec.ts index 86dfc916..29ec4a50 100644 --- a/test/unit/utils/nip44.spec.ts +++ b/test/unit/utils/nip44.spec.ts @@ -1,12 +1,7 @@ import * as secp256k1 from '@noble/secp256k1' import { expect } from 'chai' -import { - getConversationKey, - nip44Decrypt, - nip44Encrypt, - validateNip44Payload, -} from '../../../src/utils/nip44' +import { getConversationKey, nip44Decrypt, nip44Encrypt, validateNip44Payload } from '../../../src/utils/nip44' // --------------------------------------------------------------------------- // Helpers — compute pub from sec using the same library the relay uses diff --git a/test/unit/utils/proof-of-work.spec.ts b/test/unit/utils/proof-of-work.spec.ts index 062e06cc..fd64d1ad 100644 --- a/test/unit/utils/proof-of-work.spec.ts +++ b/test/unit/utils/proof-of-work.spec.ts @@ -2,8 +2,26 @@ import { expect } from 'chai' import { getLeadingZeroBits } from '../../../src/utils/proof-of-work' describe('getLeadingZeroBits()', () => { - ['80', '40', '20', '10', '08', '04', '02', '01', '0080', '0040', '0020', '0010', '0008', '0004', '0002', '0001', '0000'].forEach((input, index) => { - it(`returns ${index} given ${input}`, () => { + ;[ + '80', + '40', + '20', + '10', + '08', + '04', + '02', + '01', + '0080', + '0040', + '0020', + '0010', + '0008', + '0004', + '0002', + '0001', + '0000', + ].forEach((input, index) => { + it(`returns ${index} given ${input}`, () => { expect(getLeadingZeroBits(Buffer.from(input, 'hex'))).to.equal(index) }) }) diff --git a/test/unit/utils/settings.spec.ts b/test/unit/utils/settings.spec.ts index 987d1d54..d96b0e04 100644 --- a/test/unit/utils/settings.spec.ts +++ b/test/unit/utils/settings.spec.ts @@ -19,11 +19,15 @@ describe('SettingsStatic', () => { }) it('returns string ending with .nostr/ by default', () => { - expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.to.match(/.nostr/) + expect(SettingsStatic.getSettingsFileBasePath()) + .to.be.a('string') + .and.to.match(/.nostr/) }) - it('returns path begins with user\'s home dir by default', () => { - expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.equal(`${join(process.cwd(), '.nostr')}`) + it("returns path begins with user's home dir by default", () => { + expect(SettingsStatic.getSettingsFileBasePath()) + .to.be.a('string') + .and.equal(`${join(process.cwd(), '.nostr')}`) }) it('returns path with NOSTR_CONFIG_DIR if set', () => { @@ -46,11 +50,15 @@ describe('SettingsStatic', () => { }) it('returns string ending with settings.json by default', () => { - expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.to.match(/settings\.yaml$/) + expect(SettingsStatic.getDefaultSettingsFilePath()) + .to.be.a('string') + .and.to.match(/settings\.yaml$/) }) - it('returns path begins with user\'s home dir by default', () => { - expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.equal(join(process.cwd(), 'resources', 'default-settings.yaml')) + it("returns path begins with user's home dir by default", () => { + expect(SettingsStatic.getDefaultSettingsFilePath()) + .to.be.a('string') + .and.equal(join(process.cwd(), 'resources', 'default-settings.yaml')) }) }) @@ -70,10 +78,7 @@ describe('SettingsStatic', () => { expect(SettingsStatic.loadAndParseYamlFile('/some/path/file.yaml')).to.equal('content') - expect(readFileSyncStub).to.have.been.calledOnceWithExactly( - '/some/path/file.yaml', - { encoding: 'utf-8' } - ) + expect(readFileSyncStub).to.have.been.calledOnceWithExactly('/some/path/file.yaml', { encoding: 'utf-8' }) }) }) @@ -93,10 +98,7 @@ describe('SettingsStatic', () => { expect(SettingsStatic.loadAndParseJsonFile('/some/path/file.json')).to.equal('content') - expect(readFileSyncStub).to.have.been.calledOnceWithExactly( - '/some/path/file.json', - { encoding: 'utf-8' } - ) + expect(readFileSyncStub).to.have.been.calledOnceWithExactly('/some/path/file.json', { encoding: 'utf-8' }) }) }) @@ -116,10 +118,7 @@ describe('SettingsStatic', () => { expect(SettingsStatic.loadAndParseJsonFile('/some/path/file.json')).to.have.property('key', 'value') - expect(readFileSyncStub).to.have.been.calledOnceWithExactly( - '/some/path/file.json', - { encoding: 'utf-8' }, - ) + expect(readFileSyncStub).to.have.been.calledOnceWithExactly('/some/path/file.json', { encoding: 'utf-8' }) }) }) @@ -139,10 +138,7 @@ describe('SettingsStatic', () => { expect(SettingsStatic.loadSettings('/some/path', SettingsFileTypes.yaml)).to.equal('content') - expect(readFileSyncStub).to.have.been.calledOnceWithExactly( - '/some/path', - { encoding: 'utf-8' } - ) + expect(readFileSyncStub).to.have.been.calledOnceWithExactly('/some/path', { encoding: 'utf-8' }) }) }) @@ -188,10 +184,7 @@ describe('SettingsStatic', () => { expect(existsSyncStub).to.have.been.calledOnceWithExactly('/some/path/settings.yaml') expect(getSettingsFileBasePathStub).to.have.been.calledOnce - expect(saveSettingsStub).to.have.been.calledOnceWithExactly( - '/some/path/settings.yaml', - Sinon.match.object, - ) + expect(saveSettingsStub).to.have.been.calledOnceWithExactly('/some/path/settings.yaml', Sinon.match.object) expect(loadSettingsStub).to.have.been.called }) @@ -207,10 +200,7 @@ describe('SettingsStatic', () => { expect(existsSyncStub).to.have.been.calledOnceWithExactly('/some/path/settings.json') expect(getSettingsFileBasePathStub).to.have.been.calledOnce - expect(saveSettingsStub).to.have.been.calledOnceWithExactly( - '/some/path/settings.json', - Sinon.match.object, - ) + expect(saveSettingsStub).to.have.been.calledOnceWithExactly('/some/path/settings.json', Sinon.match.object) expect(loadSettingsStub).to.have.been.called }) @@ -222,7 +212,6 @@ describe('SettingsStatic', () => { readdirSyncStub.returns(['settings.yaml']) settingsFileTypeStub.returns('yaml') - expect(SettingsStatic.createSettings()).to.be.an('object') expect(existsSyncStub).to.have.been.calledWithExactly('/some/path/settings.yaml') @@ -256,12 +245,12 @@ describe('SettingsStatic', () => { }) it('saves settings to given path', () => { - SettingsStatic.saveSettings('/some/path', {key: 'value'} as any) + SettingsStatic.saveSettings('/some/path', { key: 'value' } as any) expect(writeFileSyncStub).to.have.been.calledOnceWithExactly( join('/some/path', 'settings.yaml'), Sinon.match.string, - { encoding: 'utf-8' } + { encoding: 'utf-8' }, ) }) }) diff --git a/test/unit/utils/sliding-window-rate-limiter.spec.ts b/test/unit/utils/sliding-window-rate-limiter.spec.ts index 29dd1698..7e48495f 100644 --- a/test/unit/utils/sliding-window-rate-limiter.spec.ts +++ b/test/unit/utils/sliding-window-rate-limiter.spec.ts @@ -49,22 +49,16 @@ describe('SlidingWindowRateLimiter', () => { it('returns true if rate limited', async () => { const now = Date.now() - getRangeFromSortedSetStub.resolves([ - `${now}:6`, - `${now}:4`, - `${now}:1`, - ]) + getRangeFromSortedSetStub.resolves([`${now}:6`, `${now}:4`, `${now}:1`]) const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 }) expect(actualResult).to.be.true }) - it('returns false if not rate limited',async () => { + it('returns false if not rate limited', async () => { const now = Date.now() - getRangeFromSortedSetStub.resolves([ - `${now}:10`, - ]) + getRangeFromSortedSetStub.resolves([`${now}:10`]) const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 }) diff --git a/test/unit/utils/stream.spec.ts b/test/unit/utils/stream.spec.ts index 6915663f..b17a036e 100644 --- a/test/unit/utils/stream.spec.ts +++ b/test/unit/utils/stream.spec.ts @@ -11,7 +11,7 @@ const { expect } = chai describe('streamMap', () => { it('transforms chunk with given function', () => { const spy = sinon.spy() - const sum = ({ a, b }: { a: number, b: number }) => ({ sum: a + b }) + const sum = ({ a, b }: { a: number; b: number }) => ({ sum: a + b }) const stream = streamMap(sum) stream.on('data', spy) diff --git a/test/unit/utils/template-cache.spec.ts b/test/unit/utils/template-cache.spec.ts index 0183f6be..1d48bf68 100644 --- a/test/unit/utils/template-cache.spec.ts +++ b/test/unit/utils/template-cache.spec.ts @@ -15,7 +15,11 @@ describe('getTemplate', () => { }) afterEach(() => { - try { fs.unlinkSync(tmpFile) } catch (_e) { /* ignore */ } + try { + fs.unlinkSync(tmpFile) + } catch (_e) { + /* ignore */ + } }) it('returns the file content', () => { diff --git a/test/unit/utils/transform.spec.ts b/test/unit/utils/transform.spec.ts index 12aac512..f8dd8088 100644 --- a/test/unit/utils/transform.spec.ts +++ b/test/unit/utils/transform.spec.ts @@ -2,7 +2,6 @@ import { expect } from 'chai' import { fromBuffer, toBuffer, toJSON } from '../../../src/utils/transform' - describe('toJSON', () => { it('returns given value JSON stringified', () => { expect(toJSON({ a: 1 })).to.equal('{"a":1}') @@ -11,7 +10,7 @@ describe('toJSON', () => { describe('toBuffer', () => { it('returns buffer given a hex string', () => { - expect(toBuffer('aa55')).to.deep.equal(Buffer.from([0xAA, 0x55])) + expect(toBuffer('aa55')).to.deep.equal(Buffer.from([0xaa, 0x55])) }) })