Skip to content

Commit c3f4d2b

Browse files
authored
Merge branch 'main' into test/payment-callback-controllers-unit-tests
2 parents 1a1adb3 + 27d8f8a commit c3f4d2b

54 files changed

Lines changed: 3838 additions & 1470 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintignore

Lines changed: 0 additions & 5 deletions
This file was deleted.

.eslintrc.js

Lines changed: 0 additions & 53 deletions
This file was deleted.

.github/workflows/checks.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
cache: npm
4141
- name: Install package dependencies
4242
run: npm ci
43-
- name: Run ESLint
43+
- name: Run Biome
4444
run: npm run lint
4545
- name: Run Knip
4646
run: npm run knip
@@ -56,7 +56,7 @@ jobs:
5656
cache: npm
5757
- name: Install package dependencies
5858
run: npm ci
59-
- name: Run ESLint
59+
- name: Run build check
6060
run: npm run build:check
6161
test-units-and-cover:
6262
name: Unit Tests And Coverage

.github/workflows/pr-title.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: PR Title Check
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- edited
8+
- synchronize
9+
- reopened
10+
11+
jobs:
12+
pr-title-lint:
13+
name: Lint PR title
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: amannn/action-semantic-pull-request@v5
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+
with:
20+
requireScope: false
21+
subjectPattern: ^.+$
22+
subjectPatternError: PR title must follow Conventional Commits format (e.g. "feat: add feature" or "fix(scope): fix bug").

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. |

CONTRIBUTING.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Run dead code and dependency analysis before opening a pull request:
1414
npm run knip
1515
```
1616

17-
`npm run lint` now runs Knip first, then ESLint.
17+
`npm run lint` now runs Biome.
1818

1919
## Pull Request Process
2020

@@ -24,3 +24,12 @@ npm run knip
2424
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
2525
3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
2626
do not have permission to do that, you may request the second reviewer to merge it for you.
27+
28+
## Code Quality
29+
30+
Run Biome checks before opening a pull request:
31+
32+
```
33+
npm run lint
34+
npm run format:check
35+
```

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@ Start:
470470
471471
## Tests
472472
473+
### Linting and formatting (Biome)
474+
475+
Run code quality checks with Biome:
476+
477+
```
478+
npm run lint
479+
npm run lint:fix
480+
npm run format
481+
npm run format:check
482+
```
483+
473484
### Unit tests
474485
475486
Open a terminal and change to the project's directory:
@@ -570,6 +581,52 @@ To see the integration test coverage report open `.coverage/integration/lcov-rep
570581
open .coverage/integration/lcov-report/index.html
571582
```
572583
584+
585+
## Security & Load Testing
586+
587+
Nostream includes a specialized security tester to simulate Slowloris-style connection holding and event flood (spam) attacks. This is used to verify relay resilience and prevent memory leaks.
588+
589+
### Running the Tester
590+
```bash
591+
# Simulates 5,000 idle "zombie" connections + 100 events/sec spam
592+
npm run test:load -- --zombies 5000 --spam-rate 100
593+
```
594+
595+
### Analyzing Memory (Heap Snapshots)
596+
To verify that connections are being correctly evicted and memory reclaimed:
597+
1. Ensure the relay is running with `--inspect` enabled (see `docker-compose.yml`).
598+
2. Open **Chrome DevTools** (`chrome://inspect`) and connect to the relay process.
599+
3. In the **Memory** tab, take a **Heap Snapshot** (Baseline).
600+
4. Run the load tester.
601+
5. Wait for the eviction cycle (default: 120s) and take a second **Heap Snapshot**.
602+
6. Switch the view to **Comparison** and select the Baseline snapshot.
603+
7. Verify that object counts (e.g., `WebSocketAdapter`, `SocketAddress`) return to baseline levels.
604+
605+
### Server-Side Monitoring
606+
To observe client and subscription counts in real-time during a test, you can instrument `src/adapters/web-socket-server-adapter.ts`:
607+
608+
1. Locate the `onHeartbeat()` method.
609+
2. Add the following logging logic:
610+
```typescript
611+
private onHeartbeat() {
612+
let totalSubs = 0;
613+
let totalClients = 0;
614+
this.webSocketServer.clients.forEach((webSocket) => {
615+
totalClients++;
616+
const webSocketAdapter = this.webSocketsAdapters.get(webSocket) as IWebSocketAdapter;
617+
if (webSocketAdapter) {
618+
webSocketAdapter.emit(WebSocketAdapterEvent.Heartbeat);
619+
totalSubs += webSocketAdapter.getSubscriptions().size;
620+
}
621+
});
622+
console.log(`[HEARTBEAT] Clients: ${totalClients} | Total subscriptions: ${totalSubs} | Heap Used: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1)} MB`);
623+
}
624+
```
625+
3. View the live output via Docker logs:
626+
```bash
627+
docker compose logs -f nostream
628+
```
629+
=======
573630
## Export Events
574631

575632
Export all stored events to a [JSON Lines](https://jsonlines.org/) (`.jsonl`) file. Each line is a valid NIP-01 Nostr event JSON object. The export streams rows from the database using cursors, so it works safely on relays with millions of events without loading them into memory.
@@ -618,6 +675,7 @@ Delete only selected kinds older than N days:
618675
By default, the script asks for explicit confirmation (`Type 'DELETE' to confirm`).
619676
Use `--force` to skip the prompt.
620677

678+
621679
## Configuration
622680

623681
You can change the default folder by setting the `NOSTR_CONFIG_DIR` environment variable to a different path.

biome.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
3+
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
4+
"files": {
5+
"ignoreUnknown": false,
6+
"includes": [
7+
"**",
8+
"!**/node_modules/**",
9+
"!**/dist/**",
10+
"!**/.test-reports/**",
11+
"!**/.coverage/**",
12+
"!**/.nostr/**",
13+
"!**/tslint.json"
14+
]
15+
},
16+
"formatter": {
17+
"enabled": true,
18+
"indentStyle": "space",
19+
"lineWidth": 120
20+
},
21+
"linter": {
22+
"enabled": true,
23+
"rules": {
24+
"recommended": false,
25+
"correctness": { "noUnusedVariables": "error" },
26+
"style": { "useBlockStatements": "warn" },
27+
"suspicious": { "noExplicitAny": "off" }
28+
}
29+
},
30+
"javascript": {
31+
"formatter": {
32+
"quoteStyle": "single",
33+
"semicolons": "asNeeded",
34+
"trailingCommas": "all"
35+
}
36+
}
37+
}
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+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
exports.up = async function (knex) {
2+
await knex.schema.alterTable('users', (table) => {
3+
table.boolean('is_vanished').notNullable().defaultTo(false)
4+
})
5+
6+
await knex.raw(`
7+
UPDATE users u
8+
SET is_vanished = true
9+
WHERE EXISTS (
10+
SELECT 1 FROM events e
11+
WHERE e.event_pubkey = u.pubkey
12+
AND e.event_kind = 62
13+
AND e.deleted_at IS NULL
14+
)
15+
`)
16+
}
17+
18+
exports.down = function (knex) {
19+
return knex.schema.alterTable('users', (table) => {
20+
table.dropColumn('is_vanished')
21+
})
22+
}

0 commit comments

Comments
 (0)