Skip to content

Commit a0029bd

Browse files
authored
Merge branch 'main' into fix/nip01-replaceable-tiebreaker
2 parents db13bd6 + b172235 commit a0029bd

36 files changed

Lines changed: 5344 additions & 12528 deletions

.env.example

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# --- REQUIRED ---
2+
SECRET=change_me_to_something_long_and_random # Generate: openssl rand -hex 128
3+
4+
# --- POSTGRESQL ---
5+
DB_HOST=localhost
6+
DB_PORT=5432
7+
DB_NAME=nostr_ts_relay
8+
DB_USER=nostr_ts_relay
9+
DB_PASSWORD=nostr_ts_relay
10+
# Alternatively, use a URI:
11+
# DB_URI=postgresql://nostr_ts_relay:nostr_ts_relay@localhost:5432/nostr_ts_relay
12+
13+
# --- DB POOL TUNING (Optional) ---
14+
# DB_MIN_POOL_SIZE=0
15+
# DB_MAX_POOL_SIZE=3
16+
# DB_ACQUIRE_CONNECTION_TIMEOUT=60000
17+
18+
# --- REDIS (Required for Rate Limiting) ---
19+
REDIS_HOST=localhost
20+
REDIS_PORT=6379
21+
# REDIS_USER=default
22+
# REDIS_PASSWORD=
23+
# Alternatively, use a URI:
24+
# REDIS_URI=redis://localhost:6379
25+
26+
# --- SERVER CONFIG ---
27+
RELAY_PORT=8008
28+
WORKER_COUNT=2 # Defaults to CPU count. Use 1 or 2 for local testing.
29+
# NOSTR_CONFIG_DIR=.nostr # Where settings.yaml lives
30+
31+
# --- DEBUGGING ---
32+
# Useful namespaces: maintenance-worker, database-client:*, cache-client, etc.
33+
# DEBUG=maintenance-worker
34+
35+
# --- RELAY PRIVATE KEY (Optional) ---
36+
# RELAY_PRIVATE_KEY=your_hex_private_key
37+
38+
# --- PAYMENTS (Only if enabled in settings.yaml) ---
39+
# ZEBEDEE_API_KEY=
40+
# NODELESS_API_KEY=
41+
# NODELESS_WEBHOOK_SECRET=
42+
# OPENNODE_API_KEY=
43+
# LNBITS_API_KEY=
44+
45+
# --- READ REPLICAS (Optional) ---
46+
# READ_REPLICA_ENABLED=false
47+
# READ_REPLICAS=2
48+
# RR0_DB_HOST=localhost
49+
# RR0_DB_PORT=5432
50+
# RR0_DB_NAME=nostr_ts_relay
51+
# RR0_DB_USER=your_psql_username
52+
# RR0_DB_PASSWORD=your_psql_password
53+
# RR0_DB_MIN_POOL_SIZE=0
54+
# RR0_DB_MAX_POOL_SIZE=3
55+
# RR0_DB_ACQUIRE_CONNECTION_TIMEOUT=60000
56+
# RR1_DB_HOST=localhost
57+
# RR1_DB_PORT=5432
58+
# RR1_DB_NAME=nostr_ts_relay
59+
# RR1_DB_USER=your_psql_username
60+
# RR1_DB_PASSWORD=your_psql_password
61+
# RR1_DB_MIN_POOL_SIZE=0
62+
# RR1_DB_MAX_POOL_SIZE=3
63+
# RR1_DB_ACQUIRE_CONNECTION_TIMEOUT=60000
64+
65+
# --- TOR (Optional) ---
66+
# TOR_HOST=localhost
67+
# TOR_CONTROL_PORT=9051
68+
# TOR_PASSWORD=
69+
# HIDDEN_SERVICE_PORT=80

.github/workflows/checks.yml

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,12 @@ jobs:
7777
- name: Run coverage for unit tests
7878
run: npm run cover:unit
7979
if: ${{ always() }}
80-
- uses: actions/upload-artifact@v3
80+
- uses: actions/upload-artifact@v4
8181
name: Upload coverage report for unit tests
8282
if: ${{ always() }}
8383
with:
84-
path: .coverage/*/lcov.info
84+
name: unit-coverage-lcov
85+
path: .coverage/unit/lcov.info
8586
- name: Coveralls
8687
uses: coverallsapp/github-action@master
8788
if: ${{ always() }}
@@ -122,29 +123,12 @@ jobs:
122123
flag-name: Integration
123124
parallel: true
124125
github-token: ${{ secrets.GITHUB_TOKEN }}
125-
- uses: actions/upload-artifact@v3
126+
- uses: actions/upload-artifact@v4
126127
name: Upload coverage report for integration tests
127128
if: ${{ always() }}
128129
with:
129-
path: .coverage/*/lcov.info
130-
sonarcloud:
131-
name: Sonarcloud
132-
needs: [test-units-and-cover, test-integrations-and-cover]
133-
runs-on: ubuntu-latest
134-
steps:
135-
- name: Checkout
136-
uses: actions/checkout@v3
137-
with:
138-
fetch-depth: 0
139-
- uses: actions/download-artifact@v3
140-
name: Download unit & integration coverage reports
141-
with:
142-
path: .coverage
143-
- name: SonarCloud Scan
144-
uses: sonarsource/sonarcloud-github-action@master
145-
env:
146-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
147-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
130+
name: integration-coverage-lcov
131+
path: .coverage/integration/lcov.info
148132
post-tests:
149133
name: Post Tests
150134
needs: [test-units-and-cover, test-integrations-and-cover]

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ node_modules/
2525
.dccache
2626
.DS_Store
2727

28+
# Local Setup
29+
redis.conf
30+
users.acl
31+
postgresql.local.conf
32+
2833
# generate output
2934
dist
3035

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v18.8.0
1+
v24.14.1

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:18-alpine3.16 AS build
1+
FROM node:24-alpine AS build
22

33
WORKDIR /build
44

@@ -10,7 +10,7 @@ COPY . .
1010

1111
RUN npm run build
1212

13-
FROM node:18-alpine3.16
13+
FROM node:24-alpine
1414

1515
LABEL org.opencontainers.image.title="Nostream"
1616
LABEL org.opencontainers.image.source=https://github.com/cameri/nostream

Dockerfile.railwayapp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## Author Saransh Sharma @cynsar foundation
2-
FROM node:18-alpine3.16 as build
2+
FROM node:24-alpine as build
33

44
ARG PORT
55
ARG PGHOST
@@ -29,7 +29,7 @@ COPY . .
2929

3030
RUN npm run build
3131

32-
FROM node:18-alpine3.16
32+
FROM node:24-alpine
3333

3434
ARG PORT
3535
ARG PGHOST

Dockerfile.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:18-alpine3.16
1+
FROM node:24-alpine
22

33
WORKDIR /code
44

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ NIPs with a relay-specific implementation are listed here.
6767
### Standalone setup
6868
- PostgreSQL 14.0
6969
- Redis
70-
- Node v18
70+
- Node v24
7171
- Typescript
7272

7373
### Docker setups
@@ -230,6 +230,29 @@ Print the Tor hostname:
230230
./scripts/print_tor_hostname
231231
```
232232
233+
### Importing events from JSON Lines
234+
235+
You can import NIP-01 events from a `.jsonl` file directly into the relay database.
236+
237+
Basic import:
238+
```
239+
npm run import -- ./events.jsonl
240+
```
241+
242+
Set a custom batch size (default: `1000`):
243+
```
244+
npm run import -- ./events.jsonl --batch-size 500
245+
```
246+
247+
The importer:
248+
249+
- Processes the file line-by-line to keep memory usage bounded.
250+
- Validates NIP-01 schema, event id hash, and Schnorr signature before insertion.
251+
- Inserts in database transactions per batch.
252+
- Skips duplicates without failing the whole import.
253+
- Prints progress in the format:
254+
`[Processed: 50,000 | Inserted: 45,000 | Skipped: 5,000 | Errors: 0]`
255+
233256
### Running as a Service
234257
235258
By default this server will run continuously until you stop it with Ctrl+C or until the system restarts.

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ services:
114114
retries: 5
115115

116116
nostream-migrate:
117-
image: node:18-alpine3.16
117+
image: node:24-alpine
118118
container_name: nostream-migrate
119119
environment:
120120
DB_HOST: nostream-db

docs/REDIS.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Redis
2+
3+
Nostream uses Redis as a cache layer, currently for rate limiting incoming requests. This document covers how to configure Redis to work with Nostream, including the recommended ACL-based setup for production environments.
4+
5+
## Overview
6+
7+
Nostream uses Redis 7.0.5 (Alpine 3.16) and connects to it via the `redis` npm package (v4.5.1). Currently, Redis is used exclusively for rate limiting — throttling incoming requests from clients to prevent abuse.
8+
9+
The Redis client is initialized as a singleton instance in `src/cache/client.ts`, meaning a single connection is shared across the entire application. This connection is wrapped by a `RedisAdapter` which exposes only the specific Redis operations
10+
Nostream needs.
11+
12+
Rate limiting is implemented using a sliding window strategy, which uses Redis sorted sets to track request timestamps. This allows Nostream to accurately enforce rate limits over a rolling time window rather than a fixed one, preventing clients from bursting requests at window boundaries.
13+
14+
## Requirements
15+
16+
- Redis 6.0 or higher (for ACL support)
17+
- Nostream ships with Redis 7.0.5 (Alpine) by default via Docker Compose
18+
19+
If you are using your own Redis instance instead of the one provided by Docker Compose, ensure it is running Redis 6.0 or higher to take advantage of the ACL configuration described in this document.
20+
21+
## Configuration
22+
23+
### Default Setup
24+
25+
By default, Nostream connects to Redis using a single password on the default user via the `--requirepass` flag. This is configured in `docker-compose.yml`:
26+
27+
```yaml
28+
command: redis-server --loglevel warning --requirepass nostr_ts_relay
29+
```
30+
31+
While this works, it grants the connecting user full access to all Redis commands which is not recommended for production environments.
32+
33+
Nostream reads the Redis connection details from the following environment variables:
34+
35+
```
36+
REDIS_URI=redis://default:nostr_ts_relay@localhost:6379
37+
38+
# or individually:
39+
REDIS_HOST=localhost
40+
REDIS_PORT=6379
41+
REDIS_USER=default
42+
REDIS_PASSWORD=nostr_ts_relay
43+
```
44+
45+
### ACL Setup (Recommended)
46+
47+
Redis ACL (Access Control List), introduced in Redis 6.0, allows you to create restricted users that can only execute specific commands. This is recommended for production environments as it follows the principle of least privilege — Nostream only gets access to the commands it actually needs.
48+
49+
#### Required Commands
50+
51+
Nostream uses the following Redis commands internally:
52+
53+
| Command | Used For |
54+
|---|---|
55+
| `EXISTS` | Checking if a rate limit key exists |
56+
| `GET` | Retrieving a cached value |
57+
| `SET` | Storing a cached value |
58+
| `ZADD` | Adding a request timestamp to the sliding window |
59+
| `ZRANGE` | Reading request timestamps from the sliding window |
60+
| `ZREMRANGEBYSCORE` | Removing expired timestamps from the sliding window |
61+
| `EXPIRE` | Setting expiry on rate limit keys |
62+
63+
#### Example Configuration
64+
65+
**Using redis.conf:**
66+
67+
Add the following to your `redis.conf`:
68+
69+
```conf
70+
aclfile /etc/redis/users.acl
71+
```
72+
73+
Then create `/etc/redis/users.acl` with the following:
74+
75+
```
76+
user nostream on >your_password ~* &* +EXISTS +GET +SET +ZADD +ZRANGE +ZREMRANGEBYSCORE +EXPIRE
77+
```
78+
79+
**Using redis-cli:**
80+
81+
You can also set the ACL rule directly via `redis-cli`:
82+
83+
```bash
84+
ACL SETUSER nostream on >your_password ~* &* +EXISTS +GET +SET +ZADD +ZRANGE +ZREMRANGEBYSCORE +EXPIRE
85+
```
86+
87+
Verify the user was created correctly:
88+
89+
```bash
90+
ACL GETUSER nostream
91+
```
92+
93+
**Updating docker-compose.yml:**
94+
95+
Replace the default `--requirepass` flag with the ACL file approach:
96+
97+
```yaml
98+
nostream-cache:
99+
image: redis:7.0.5-alpine3.16
100+
container_name: nostream-cache
101+
volumes:
102+
- cache:/data
103+
- ./redis.conf:/usr/local/etc/redis/redis.conf
104+
- ./users.acl:/etc/redis/users.acl
105+
command: redis-server /usr/local/etc/redis/redis.conf
106+
networks:
107+
default:
108+
restart: always
109+
healthcheck:
110+
test: [ "CMD", "redis-cli", "-u", "redis://nostream:your_password@localhost:6379", "ping" ]
111+
interval: 1s
112+
timeout: 5s
113+
retries: 5
114+
```
115+
116+
Then update your `.env` file:
117+
118+
```
119+
REDIS_URI=redis://nostream:your_password@localhost:6379
120+
```
121+
122+
## Troubleshooting
123+
124+
**NOAUTH Authentication required**
125+
126+
Redis is requiring authentication but no password was provided. Ensure your `REDIS_URI` or `REDIS_PASSWORD` environment variables are set correctly.
127+
128+
**WRONGPASS invalid username-password pair**
129+
130+
The username or password provided is incorrect. Double check your `REDIS_USER` and `REDIS_PASSWORD` environment variables match what was configured in your ACL setup.
131+
132+
**NOPERM this user has no permissions to run the command**
133+
134+
The Redis user does not have permission to run a specific command. Ensure all 7 required commands are granted in your ACL rule:
135+
136+
```
137+
+EXISTS +GET +SET +ZADD +ZRANGE +ZREMRANGEBYSCORE +EXPIRE
138+
```
139+
140+
**Connection refused (ECONNREFUSED)**
141+
142+
Redis is not running or is not reachable at the configured host and port. Verify:
143+
- Redis is running (`docker ps` if using Docker)
144+
- `REDIS_HOST` and `REDIS_PORT` are correct
145+
- No firewall is blocking the connection

0 commit comments

Comments
 (0)