Skip to content

Commit 211b668

Browse files
Merge remote-tracking branch 'upstream/main' into issue-432-biome-migration
# Conflicts: # package.json
2 parents ec3379c + f7869b0 commit 211b668

22 files changed

Lines changed: 1717 additions & 54 deletions

CONFIGURATION.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,10 @@ Running `nostream` for the first time creates the settings file in `<project_roo
119119
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
120120
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
121121
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
122-
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
122+
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
123+
| nip05.mode | NIP-05 verification mode: `enabled` requires verification, `passive` verifies without blocking, `disabled` does nothing. Defaults to `disabled`. |
124+
| nip05.verifyExpiration | Time in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week). |
125+
| nip05.verifyUpdateFrequency | Minimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours). |
126+
| nip05.maxConsecutiveFailures | Number of consecutive verification failures before giving up on an author. Defaults to 20. |
127+
| nip05.domainWhitelist | List of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish. |
128+
| nip05.domainBlacklist | List of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected. |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
exports.up = function (knex) {
2+
return knex.schema.createTable('nip05_verifications', function (table) {
3+
table.binary('pubkey').notNullable().primary()
4+
table.text('nip05').notNullable()
5+
table.text('domain').notNullable()
6+
table.boolean('is_verified').notNullable().defaultTo(false)
7+
table.timestamp('last_verified_at', { useTz: true }).nullable()
8+
table.timestamp('last_checked_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
9+
table.integer('failure_count').notNullable().defaultTo(0)
10+
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
11+
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
12+
13+
table.index(['domain'], 'idx_nip05_verifications_domain')
14+
table.index(['is_verified'], 'idx_nip05_verifications_is_verified')
15+
table.index(['last_checked_at'], 'idx_nip05_verifications_last_checked_at')
16+
})
17+
}
18+
19+
exports.down = function (knex) {
20+
return knex.schema.dropTable('nip05_verifications')
21+
}

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@
124124
},
125125
"dependencies": {
126126
"@noble/secp256k1": "1.7.1",
127+
"accepts": "^1.3.8",
127128
"axios": "^1.15.0",
128129
"bech32": "2.0.0",
129130
"debug": "4.3.4",
130131
"dotenv": "16.0.3",
131-
"accepts": "^1.3.8",
132132
"express": "4.22.1",
133133
"helmet": "6.0.1",
134134
"js-yaml": "4.1.1",

resources/default-settings.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ paymentsProcessors:
3535
opennode:
3636
baseURL: api.opennode.com
3737
callbackBaseURL: https://nostream.your-domain.com/callbacks/opennode
38+
nip05:
39+
# NIP-05 verification of event authors as a spam reduction measure.
40+
# mode: 'enabled' requires NIP-05 for publishing (except kind 0),
41+
# 'passive' verifies but never blocks, 'disabled' does nothing.
42+
mode: disabled
43+
# How long (ms) a successful verification remains valid before re-check.
44+
# Matches nostr-rs-relay default of 1 week.
45+
verifyExpiration: 604800000
46+
# Minimum interval (ms) between re-verification attempts for a given author.
47+
# Matches nostr-rs-relay default of 24 hours.
48+
verifyUpdateFrequency: 86400000
49+
# How many consecutive failed checks before giving up on verifying an author.
50+
# Matches nostr-rs-relay default of 20.
51+
maxConsecutiveFailures: 20
52+
# Only allow authors with NIP-05 at these domains (empty = allow all)
53+
domainWhitelist: []
54+
# Block authors with NIP-05 at these domains
55+
domainBlacklist: []
3856
network:
3957
maxPayloadSize: 524288
4058
# Comment the next line if using CloudFlare proxy

src/@types/nip05.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Pubkey } from './base'
2+
3+
export interface Nip05Verification {
4+
pubkey: Pubkey
5+
nip05: string
6+
domain: string
7+
isVerified: boolean
8+
lastVerifiedAt: Date | null
9+
lastCheckedAt: Date
10+
failureCount: number
11+
createdAt: Date
12+
updatedAt: Date
13+
}
14+
15+
export interface DBNip05Verification {
16+
pubkey: Buffer
17+
nip05: string
18+
domain: string
19+
is_verified: boolean
20+
last_verified_at: Date | null
21+
last_checked_at: Date
22+
failure_count: number
23+
created_at: Date
24+
updated_at: Date
25+
}

src/@types/repositories.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DBEvent, Event } from './event'
55
import { EventKinds } from '../constants/base'
66
import { EventKindsRange } from './settings'
77
import { Invoice } from './invoice'
8+
import { Nip05Verification } from './nip05'
89
import { SubscriptionFilter } from './subscription'
910
import { User } from './user'
1011

@@ -64,3 +65,14 @@ export interface IUserRepository {
6465
getBalanceByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise<bigint>
6566
admitUser(pubkey: Pubkey, admittedAt: Date, client?: DatabaseClient): Promise<void>
6667
}
68+
69+
export interface INip05VerificationRepository {
70+
findByPubkey(pubkey: Pubkey): Promise<Nip05Verification | undefined>
71+
upsert(verification: Nip05Verification): Promise<number>
72+
findPendingVerifications(
73+
updateFrequencyMs: number,
74+
maxFailures: number,
75+
limit: number,
76+
): Promise<Nip05Verification[]>
77+
deleteByPubkey(pubkey: Pubkey): Promise<number>
78+
}

src/@types/settings.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,29 @@ export interface Mirroring {
228228
static?: Mirror[]
229229
}
230230

231+
export type Nip05Mode = 'enabled' | 'passive' | 'disabled'
232+
233+
export interface Nip05Settings {
234+
mode: Nip05Mode
235+
/**
236+
* Maximum age (in ms) of a successful verification before an author is blocked.
237+
* Defaults to 604800000 (7 days) when unset.
238+
*/
239+
verifyExpiration?: number
240+
/**
241+
* Minimum interval (in ms) between background re-verifications per author.
242+
* Defaults to 86400000 (24 hours) when unset.
243+
*/
244+
verifyUpdateFrequency?: number
245+
/**
246+
* Number of consecutive verification failures after which an author is no longer
247+
* re-checked. Defaults to 20 when unset.
248+
*/
249+
maxConsecutiveFailures?: number
250+
domainWhitelist?: string[]
251+
domainBlacklist?: string[]
252+
}
253+
231254
export interface Settings {
232255
info: Info
233256
payments?: Payments
@@ -236,4 +259,5 @@ export interface Settings {
236259
workers?: Worker
237260
limits?: Limits
238261
mirroring?: Mirroring
262+
nip05?: Nip05Settings
239263
}

src/app/maintenance-worker.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,71 @@
1+
import {
2+
DEFAULT_NIP05_MAX_CONSECUTIVE_FAILURES,
3+
DEFAULT_NIP05_VERIFY_UPDATE_FREQUENCY_MS,
4+
Nip05VerificationOutcome,
5+
verifyNip05Identifier,
6+
} from '../utils/nip05'
17
import { IMaintenanceService, IPaymentsService } from '../@types/services'
28
import { mergeDeepLeft, path, pipe } from 'ramda'
39
import { IRunnable } from '../@types/base'
410

511
import { createLogger } from '../factories/logger-factory'
612
import { delayMs } from '../utils/misc'
13+
import { INip05VerificationRepository } from '../@types/repositories'
714
import { InvoiceStatus } from '../@types/invoice'
15+
import { Nip05Verification } from '../@types/nip05'
816
import { Settings } from '../@types/settings'
917

1018
const UPDATE_INVOICE_INTERVAL = 60000
19+
const NIP05_REVERIFICATION_BATCH_SIZE = 50
1120
const CLEAR_OLD_EVENTS_TIMEOUT_MS = 5000
1221

1322
const debug = createLogger('maintenance-worker')
1423

24+
/**
25+
* Merge a re-verification outcome onto an existing verification row.
26+
*
27+
* Definitive outcomes (`verified`, `mismatch`, `invalid`) update `isVerified`
28+
* and `lastVerifiedAt`. Transient `error` outcomes only bump `failureCount` /
29+
* `lastCheckedAt` so a previously-verified author keeps their grace period
30+
* until `verifyExpiration` elapses. This prevents a single network blip from
31+
* immediately blocking publishing.
32+
*/
33+
export function applyReverificationOutcome(
34+
existing: Nip05Verification,
35+
outcome: Nip05VerificationOutcome,
36+
): Nip05Verification {
37+
const now = new Date()
38+
const base: Nip05Verification = {
39+
...existing,
40+
lastCheckedAt: now,
41+
updatedAt: now,
42+
}
43+
44+
switch (outcome.status) {
45+
case 'verified':
46+
return {
47+
...base,
48+
isVerified: true,
49+
lastVerifiedAt: now,
50+
failureCount: 0,
51+
}
52+
case 'mismatch':
53+
case 'invalid':
54+
return {
55+
...base,
56+
isVerified: false,
57+
lastVerifiedAt: null,
58+
failureCount: existing.failureCount + 1,
59+
}
60+
case 'error':
61+
default:
62+
return {
63+
...base,
64+
failureCount: existing.failureCount + 1,
65+
}
66+
}
67+
}
68+
1569
export class MaintenanceWorker implements IRunnable {
1670
private interval: NodeJS.Timeout | undefined
1771
private isRunning = false
@@ -21,6 +75,7 @@ export class MaintenanceWorker implements IRunnable {
2175
private readonly paymentsService: IPaymentsService,
2276
private readonly maintenanceService: IMaintenanceService,
2377
private readonly settings: () => Settings,
78+
private readonly nip05VerificationRepository: INip05VerificationRepository,
2479
) {
2580
this.process
2681
.on('SIGINT', this.onExit.bind(this))
@@ -65,6 +120,8 @@ export class MaintenanceWorker implements IRunnable {
65120
const currentSettings = this.settings()
66121
const clearOldEventsPromise = this.clearOldEventsSafely()
67122

123+
await this.processNip05Reverifications(currentSettings)
124+
68125
if (!path(['payments','enabled'], currentSettings)) {
69126
await clearOldEventsPromise
70127
return
@@ -120,6 +177,43 @@ export class MaintenanceWorker implements IRunnable {
120177
await clearOldEventsPromise
121178
}
122179

180+
private async processNip05Reverifications(currentSettings: Settings): Promise<void> {
181+
const nip05Settings = currentSettings.nip05
182+
if (!nip05Settings || nip05Settings.mode === 'disabled') {
183+
return
184+
}
185+
186+
try {
187+
const updateFrequency = nip05Settings.verifyUpdateFrequency ?? DEFAULT_NIP05_VERIFY_UPDATE_FREQUENCY_MS
188+
const maxFailures = nip05Settings.maxConsecutiveFailures ?? DEFAULT_NIP05_MAX_CONSECUTIVE_FAILURES
189+
190+
const pendingVerifications = await this.nip05VerificationRepository.findPendingVerifications(
191+
updateFrequency,
192+
maxFailures,
193+
NIP05_REVERIFICATION_BATCH_SIZE,
194+
)
195+
196+
if (!pendingVerifications.length) {
197+
return
198+
}
199+
200+
debug('found %d NIP-05 verifications to re-check', pendingVerifications.length)
201+
202+
for (const verification of pendingVerifications) {
203+
try {
204+
const outcome = await verifyNip05Identifier(verification.nip05, verification.pubkey)
205+
const updated = applyReverificationOutcome(verification, outcome)
206+
await this.nip05VerificationRepository.upsert(updated)
207+
await delayMs(200 + Math.floor(Math.random() * 100))
208+
} catch (error) {
209+
debug('failed to re-verify NIP-05 for %s: %o', verification.pubkey, error)
210+
}
211+
}
212+
} catch (error) {
213+
debug('NIP-05 re-verification batch failed: %o', error)
214+
}
215+
}
216+
123217
private onError(error: Error) {
124218
debug('error: %o', error)
125219
throw error
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { createMaintenanceService } from './maintenance-service-factory'
22
import { createPaymentsService } from './payments-service-factory'
33
import { createSettings } from './settings-factory'
4+
import { getMasterDbClient } from '../database/client'
45
import { MaintenanceWorker } from '../app/maintenance-worker'
6+
import { Nip05VerificationRepository } from '../repositories/nip05-verification-repository'
57

68
export const maintenanceWorkerFactory = () => {
9+
const dbClient = getMasterDbClient()
10+
const nip05VerificationRepository = new Nip05VerificationRepository(dbClient)
711
return new MaintenanceWorker(
812
process,
913
createPaymentsService(),
1014
createMaintenanceService(),
11-
createSettings
15+
createSettings,
16+
nip05VerificationRepository,
1217
)
1318
}

0 commit comments

Comments
 (0)