Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,10 @@ Running `nostream` for the first time creates the settings file in `<project_roo
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
| nip05.mode | NIP-05 verification mode: `enabled` requires verification, `passive` verifies without blocking, `disabled` does nothing. Defaults to `disabled`. |
| nip05.verifyExpiration | Time in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week). |
| nip05.verifyUpdateFrequency | Minimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours). |
| nip05.maxConsecutiveFailures | Number of consecutive verification failures before giving up on an author. Defaults to 20. |
| nip05.domainWhitelist | List of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish. |
| nip05.domainBlacklist | List of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected. |
21 changes: 21 additions & 0 deletions migrations/20260409_120000_create_nip05_verifications_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
exports.up = function (knex) {
return knex.schema.createTable('nip05_verifications', function (table) {
table.binary('pubkey').notNullable().primary()
table.text('nip05').notNullable()
table.text('domain').notNullable()
table.boolean('is_verified').notNullable().defaultTo(false)
table.timestamp('last_verified_at', { useTz: true }).nullable()
table.timestamp('last_checked_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
table.integer('failure_count').notNullable().defaultTo(0)
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())

table.index(['domain'], 'idx_nip05_verifications_domain')
table.index(['is_verified'], 'idx_nip05_verifications_is_verified')
table.index(['last_checked_at'], 'idx_nip05_verifications_last_checked_at')
})
}

exports.down = function (knex) {
return knex.schema.dropTable('nip05_verifications')
}
18 changes: 18 additions & 0 deletions resources/default-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ paymentsProcessors:
opennode:
baseURL: api.opennode.com
callbackBaseURL: https://nostream.your-domain.com/callbacks/opennode
nip05:
# NIP-05 verification of event authors as a spam reduction measure.
# mode: 'enabled' requires NIP-05 for publishing (except kind 0),
# 'passive' verifies but never blocks, 'disabled' does nothing.
mode: disabled
# How long (ms) a successful verification remains valid before re-check.
# Matches nostr-rs-relay default of 1 week.
verifyExpiration: 604800000
# Minimum interval (ms) between re-verification attempts for a given author.
# Matches nostr-rs-relay default of 24 hours.
verifyUpdateFrequency: 86400000
# How many consecutive failed checks before giving up on verifying an author.
# Matches nostr-rs-relay default of 20.
maxConsecutiveFailures: 20
# Only allow authors with NIP-05 at these domains (empty = allow all)
domainWhitelist: []
# Block authors with NIP-05 at these domains
domainBlacklist: []
network:
maxPayloadSize: 524288
# Comment the next line if using CloudFlare proxy
Expand Down
25 changes: 25 additions & 0 deletions src/@types/nip05.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Pubkey } from './base'

export interface Nip05Verification {
pubkey: Pubkey
nip05: string
domain: string
isVerified: boolean
lastVerifiedAt: Date | null
lastCheckedAt: Date
failureCount: number
createdAt: Date
updatedAt: Date
}

export interface DBNip05Verification {
pubkey: Buffer
nip05: string
domain: string
is_verified: boolean
last_verified_at: Date | null
last_checked_at: Date
failure_count: number
created_at: Date
updated_at: Date
}
12 changes: 12 additions & 0 deletions src/@types/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PassThrough } from 'stream'
import { DatabaseClient, EventId, Pubkey } from './base'
import { DBEvent, Event } from './event'
import { Invoice } from './invoice'
import { Nip05Verification } from './nip05'
import { SubscriptionFilter } from './subscription'
import { User } from './user'

Expand Down Expand Up @@ -48,3 +49,14 @@ export interface IUserRepository {
upsert(user: Partial<User>, client?: DatabaseClient): Promise<number>
getBalanceByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise<bigint>
}

export interface INip05VerificationRepository {
findByPubkey(pubkey: Pubkey): Promise<Nip05Verification | undefined>
upsert(verification: Nip05Verification): Promise<number>
findPendingVerifications(
updateFrequencyMs: number,
maxFailures: number,
limit: number,
): Promise<Nip05Verification[]>
deleteByPubkey(pubkey: Pubkey): Promise<number>
}
12 changes: 12 additions & 0 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ export interface Mirroring {
static?: Mirror[]
}

export type Nip05Mode = 'enabled' | 'passive' | 'disabled'

export interface Nip05Settings {
mode: Nip05Mode
verifyExpiration: number
verifyUpdateFrequency: number
maxConsecutiveFailures: number
domainWhitelist?: string[]
domainBlacklist?: string[]
}

export interface Settings {
info: Info
payments?: Payments
Expand All @@ -221,4 +232,5 @@ export interface Settings {
workers?: Worker
limits?: Limits
mirroring?: Mirroring
nip05?: Nip05Settings
}
54 changes: 54 additions & 0 deletions src/app/maintenance-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { IRunnable } from '../@types/base'

import { createLogger } from '../factories/logger-factory'
import { delayMs } from '../utils/misc'
import { INip05VerificationRepository } from '../@types/repositories'
import { InvoiceStatus } from '../@types/invoice'
import { IPaymentsService } from '../@types/services'
import { Nip05Verification } from '../@types/nip05'
import { Settings } from '../@types/settings'
import { verifyNip05Identifier } from '../utils/nip05'

const UPDATE_INVOICE_INTERVAL = 60000
const NIP05_REVERIFICATION_BATCH_SIZE = 50

const debug = createLogger('maintenance-worker')

Expand All @@ -18,6 +22,7 @@ export class MaintenanceWorker implements IRunnable {
private readonly process: NodeJS.Process,
private readonly paymentsService: IPaymentsService,
private readonly settings: () => Settings,
private readonly nip05VerificationRepository: INip05VerificationRepository,
) {
this.process
.on('SIGINT', this.onExit.bind(this))
Expand All @@ -34,6 +39,8 @@ export class MaintenanceWorker implements IRunnable {
private async onSchedule(): Promise<void> {
const currentSettings = this.settings()

await this.processNip05Reverifications(currentSettings)

if (!path(['payments','enabled'], currentSettings)) {
return
}
Expand Down Expand Up @@ -86,6 +93,53 @@ export class MaintenanceWorker implements IRunnable {
}
}

private async processNip05Reverifications(currentSettings: Settings): Promise<void> {
const nip05Settings = currentSettings.nip05
if (!nip05Settings || nip05Settings.mode === 'disabled') {
return
}

try {
const updateFrequency = nip05Settings.verifyUpdateFrequency ?? 86400000
const maxFailures = nip05Settings.maxConsecutiveFailures ?? 20

const pendingVerifications = await this.nip05VerificationRepository.findPendingVerifications(
updateFrequency,
maxFailures,
NIP05_REVERIFICATION_BATCH_SIZE,
)

if (!pendingVerifications.length) {
return
}

debug('found %d NIP-05 verifications to re-check', pendingVerifications.length)

for (const verification of pendingVerifications) {
try {
const verified = await verifyNip05Identifier(verification.nip05, verification.pubkey)
const now = new Date()

const updated: Nip05Verification = {
...verification,
isVerified: verified,
lastVerifiedAt: verified ? now : verification.lastVerifiedAt,
lastCheckedAt: now,
failureCount: verified ? 0 : verification.failureCount + 1,
updatedAt: now,
}

await this.nip05VerificationRepository.upsert(updated)
await delayMs(200 + Math.floor(Math.random() * 100))
} catch (error) {
debug('failed to re-verify NIP-05 for %s: %o', verification.pubkey, error)
}
}
} catch (error) {
debug('NIP-05 re-verification batch failed: %o', error)
}
}

private onError(error: Error) {
debug('error: %o', error)
throw error
Expand Down
6 changes: 5 additions & 1 deletion src/factories/maintenance-worker-factory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createPaymentsService } from './payments-service-factory'
import { createSettings } from './settings-factory'
import { getMasterDbClient } from '../database/client'
import { MaintenanceWorker } from '../app/maintenance-worker'
import { Nip05VerificationRepository } from '../repositories/nip05-verification-repository'

export const maintenanceWorkerFactory = () => {
return new MaintenanceWorker(process, createPaymentsService(), createSettings)
const dbClient = getMasterDbClient()
const nip05VerificationRepository = new Nip05VerificationRepository(dbClient)
return new MaintenanceWorker(process, createPaymentsService(), createSettings, nip05VerificationRepository)
}
4 changes: 3 additions & 1 deletion src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IEventRepository, IUserRepository } from '../@types/repositories'
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'
Expand All @@ -11,6 +11,7 @@ import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handl
export const messageHandlerFactory = (
eventRepository: IEventRepository,
userRepository: IUserRepository,
nip05VerificationRepository: INip05VerificationRepository,
) => ([message, adapter]: [IncomingMessage, IWebSocketAdapter]) => {
switch (message[0]) {
case MessageType.EVENT:
Expand All @@ -22,6 +23,7 @@ export const messageHandlerFactory = (
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
nip05VerificationRepository,
)
}
case MessageType.REQ:
Expand Down
5 changes: 3 additions & 2 deletions src/factories/websocket-adapter-factory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IncomingMessage } from 'http'
import { WebSocket } from 'ws'

import { IEventRepository, IUserRepository } from '../@types/repositories'
import { IEventRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories'
import { createSettings } from './settings-factory'
import { IWebSocketServerAdapter } from '../@types/adapters'
import { messageHandlerFactory } from './message-handler-factory'
Expand All @@ -12,12 +12,13 @@ import { WebSocketAdapter } from '../adapters/web-socket-adapter'
export const webSocketAdapterFactory = (
eventRepository: IEventRepository,
userRepository: IUserRepository,
nip05VerificationRepository: INip05VerificationRepository,
) => ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) =>
new WebSocketAdapter(
client,
request,
webSocketServerAdapter,
messageHandlerFactory(eventRepository, userRepository),
messageHandlerFactory(eventRepository, userRepository, nip05VerificationRepository),
slidingWindowRateLimiterFactory,
createSettings,
)
4 changes: 3 additions & 1 deletion src/factories/worker-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppWorker } from '../app/worker'
import { createSettings } from '../factories/settings-factory'
import { createWebApp } from './web-app-factory'
import { EventRepository } from '../repositories/event-repository'
import { Nip05VerificationRepository } from '../repositories/nip05-verification-repository'
import { UserRepository } from '../repositories/user-repository'
import { webSocketAdapterFactory } from './websocket-adapter-factory'
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
Expand All @@ -17,6 +18,7 @@ export const workerFactory = (): AppWorker => {
const readReplicaDbClient = getReadReplicaDbClient()
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
const userRepository = new UserRepository(dbClient)
const nip05VerificationRepository = new Nip05VerificationRepository(dbClient)

const settings = createSettings()

Expand Down Expand Up @@ -58,7 +60,7 @@ export const workerFactory = (): AppWorker => {
const adapter = new WebSocketServerAdapter(
server,
webSocketServer,
webSocketAdapterFactory(eventRepository, userRepository),
webSocketAdapterFactory(eventRepository, userRepository, nip05VerificationRepository),
createSettings,
)

Expand Down
Loading