From 79830983efe99d73a6ed5ec9ffbaabfa6b5fbf30 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:27:51 +0530 Subject: [PATCH 1/8] test(nip05): register sinon-chai for NIP-05 unit tests (#35) --- test/unit/utils/nip05.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/utils/nip05.spec.ts b/test/unit/utils/nip05.spec.ts index 67ff3fa5..84cd231f 100644 --- a/test/unit/utils/nip05.spec.ts +++ b/test/unit/utils/nip05.spec.ts @@ -2,8 +2,10 @@ import axios from 'axios' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import Sinon from 'sinon' +import sinonChai from 'sinon-chai' chai.use(chaiAsPromised) +chai.use(sinonChai) import { extractNip05FromEvent, From 19701718e7c60bdf348a81ff016a15efcaf1318e Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:29:31 +0530 Subject: [PATCH 2/8] ci: validate Docker Compose overlay merges --- .github/workflows/checks.yml | 12 +++++ package.json | 4 ++ scripts/verify-compose-overlays.mjs | 73 +++++++++++++++++++++++++++++ scripts/verify_compose_overlays | 3 ++ 4 files changed, 92 insertions(+) create mode 100644 scripts/verify-compose-overlays.mjs create mode 100644 scripts/verify_compose_overlays diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5d752368..841af8be 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -58,6 +58,16 @@ jobs: run: npm ci - name: Run ESLint run: npm run build:check + compose-validate: + name: Validate Docker Compose overlays (Tor / I2P) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Validate merged compose files + env: + SECRET: ci_placeholder_not_for_production_use_repeat_to_64chars_aaaaaaaa + run: node ./scripts/verify-compose-overlays.mjs test-units-and-cover: name: Unit Tests And Coverage runs-on: ubuntu-latest @@ -65,6 +75,7 @@ jobs: - commit-lint - lint - build-check + - compose-validate steps: - name: Checkout uses: actions/checkout@v3 @@ -100,6 +111,7 @@ jobs: - commit-lint - lint - build-check + - compose-validate steps: - name: Checkout uses: actions/checkout@v3 diff --git a/package.json b/package.json index 72e62788..0042d9b2 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ "tor:docker:compose:start": "./scripts/start_with_tor", "tor:hostname": "./scripts/print_tor_hostname", "tor:docker:compose:stop": "./scripts/stop", + "i2p:docker:compose:start": "./scripts/start_with_i2p", + "i2p:hostname": "./scripts/print_i2p_hostname", + "i2p:docker:compose:stop": "./scripts/stop", + "compose:validate": "node ./scripts/verify-compose-overlays.mjs", "docker:integration:run": "docker compose -f ./test/integration/docker-compose.yml run --rm tests", "docker:test:integration": "npm run docker:integration:run -- npm run test:integration", "docker:cover:integration": "npm run docker:integration:run -- npm run cover:integration", diff --git a/scripts/verify-compose-overlays.mjs b/scripts/verify-compose-overlays.mjs new file mode 100644 index 00000000..82569249 --- /dev/null +++ b/scripts/verify-compose-overlays.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Validates docker-compose.yml merged with Tor / I2P overlays (config -q). + * Requires Docker Desktop / Engine. Does not start containers. + */ +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +process.chdir(root) + +function resolveDockerExe() { + if (process.env.DOCKER_EXE && existsSync(process.env.DOCKER_EXE)) { + return process.env.DOCKER_EXE + } + if (process.platform === 'win32' && process.env.ProgramFiles) { + const candidate = join( + process.env.ProgramFiles, + 'Docker', + 'Docker', + 'resources', + 'bin', + 'docker.exe', + ) + if (existsSync(candidate)) { + return candidate + } + } + return 'docker' +} + +const dockerExe = resolveDockerExe() + +const dockerCheck = spawnSync(dockerExe, ['compose', 'version'], { encoding: 'utf8' }) +if (dockerCheck.error || dockerCheck.status !== 0) { + process.stderr.write( + 'docker is not installed or not on PATH. Install Docker Desktop / Engine, then run:\n' + + ' npm run compose:validate\n' + + 'On Windows, ensure Docker Desktop is running, or set DOCKER_EXE to the full path to docker.exe.\n', + ) + process.exit(1) +} + +if (!process.env.SECRET || process.env.SECRET.length < 16) { + process.env.SECRET = + 'ci_placeholder_not_for_production_use_repeat_to_64chars_aaaaaaaa' +} + +const runs = [ + ['docker-compose.yml', 'docker-compose.i2p.yml'], + ['docker-compose.yml', 'docker-compose.tor.yml'], + ['docker-compose.yml', 'docker-compose.tor.yml', 'docker-compose.i2p.yml'], +] + +for (const files of runs) { + const args = ['compose'] + for (const f of files) { + args.push('-f', f) + } + args.push('config', '-q') + const label = files.join(' + ') + process.stdout.write(`== ${label} ==\n`) + const r = spawnSync(dockerExe, args, { stdio: 'inherit' }) + if (r.status !== 0) { + process.stderr.write(`compose validation failed for: ${label}\n`) + process.exit(r.status ?? 1) + } + process.stdout.write('OK\n') +} + +process.stdout.write('All compose overlay merges validate successfully.\n') diff --git a/scripts/verify_compose_overlays b/scripts/verify_compose_overlays new file mode 100644 index 00000000..60d07a4c --- /dev/null +++ b/scripts/verify_compose_overlays @@ -0,0 +1,3 @@ +#!/bin/bash +# Delegates to Node so the same validation runs on Windows (npm) and Linux/macOS. +exec node "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/verify-compose-overlays.mjs" From de828e8a19851d766d769a1714dfc36218b54490 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:33:35 +0530 Subject: [PATCH 3/8] feat(i2p): add i2pd sidecar and tunnel configuration(#35) --- docker-compose.i2p.yml | 14 ++++++++++++++ i2p/i2pd.conf | 8 ++++++++ i2p/tunnels.conf | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 docker-compose.i2p.yml create mode 100644 i2p/i2pd.conf create mode 100644 i2p/tunnels.conf diff --git a/docker-compose.i2p.yml b/docker-compose.i2p.yml new file mode 100644 index 00000000..2125298a --- /dev/null +++ b/docker-compose.i2p.yml @@ -0,0 +1,14 @@ +services: + i2pd: + image: purplei2p/i2pd:release-2.59.0 + container_name: i2pd + depends_on: + - nostream + volumes: + - ${PWD}/.nostr/i2p/data:/home/i2pd/data + - ${PWD}/i2p/i2pd.conf:/home/i2pd/data/i2pd.conf:ro + - ${PWD}/i2p/tunnels.conf:/home/i2pd/data/tunnels.conf:ro + restart: on-failure + networks: + default: + ipv4_address: 10.10.10.252 diff --git a/i2p/i2pd.conf b/i2p/i2pd.conf new file mode 100644 index 00000000..26d44dd1 --- /dev/null +++ b/i2p/i2pd.conf @@ -0,0 +1,8 @@ +# Minimal i2pd configuration for nostream. +# Data and keys are persisted via the Docker volume mount at /home/i2pd/data. + +[limits] +transittunnels = 256 + +[precomputation] +elgamal = true diff --git a/i2p/tunnels.conf b/i2p/tunnels.conf new file mode 100644 index 00000000..d98d9431 --- /dev/null +++ b/i2p/tunnels.conf @@ -0,0 +1,5 @@ +[nostream] +type = http +host = 10.10.10.2 +port = 8008 +keys = nostream.dat From 065638ceae4c6d5b861435bff569625828d28213 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:36:11 +0530 Subject: [PATCH 4/8] feat(docker): pin nostream IPv4 for Tor/I2P sidecars (#35) --- docker-compose.yml | 295 +++++++++++++++++++++++---------------------- 1 file changed, 148 insertions(+), 147 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 46b50904..48ff0d57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,147 +1,148 @@ -services: - nostream: - build: . - container_name: nostream - environment: - SECRET: ${SECRET} - RELAY_PORT: 8008 - # Master - NOSTR_CONFIG_DIR: /home/node/.nostr - DB_HOST: nostream-db - DB_PORT: 5432 - DB_USER: nostr_ts_relay - DB_PASSWORD: nostr_ts_relay - DB_NAME: nostr_ts_relay - DB_MIN_POOL_SIZE: 16 - DB_MAX_POOL_SIZE: 64 - DB_ACQUIRE_CONNECTION_TIMEOUT: 60000 - # Read Replica - READ_REPLICAS: 2 - READ_REPLICA_ENABLED: 'false' - # Read Replica No. 1 - RR0_DB_HOST: db - RR0_DB_PORT: 5432 - RR0_DB_USER: nostr_ts_relay - RR0_DB_PASSWORD: nostr_ts_relay - RR0_DB_NAME: nostr_ts_relay - RR0_DB_MIN_POOL_SIZE: 16 - RR0_DB_MAX_POOL_SIZE: 64 - RR0_DB_ACQUIRE_CONNECTION_TIMEOUT: 10000 - # Read Replica No. 2 - RR1_DB_HOST: db - RR1_DB_PORT: 5432 - RR1_DB_USER: nostr_ts_relay - RR1_DB_PASSWORD: nostr_ts_relay - RR1_DB_NAME: nostr_ts_relay - RR1_DB_MIN_POOL_SIZE: 16 - RR1_DB_MAX_POOL_SIZE: 64 - RR1_DB_ACQUIRE_CONNECTION_TIMEOUT: 10000 - # Add RR2, RR3, etc. to configure more read replicas - # Redis - REDIS_HOST: nostream-cache - REDIS_PORT: 6379 - REDIS_USER: default - REDIS_PASSWORD: nostr_ts_relay - TOR_HOST: tor_proxy - TOR_CONTROL_PORT: 9051 - TOR_PASSWORD: nostr_ts_relay - HIDDEN_SERVICE_PORT: 80 - # Payments Processors - # Zebedee - ZEBEDEE_API_KEY: ${ZEBEDEE_API_KEY} - # Nodeless.io - NODELESS_API_KEY: ${NODELESS_API_KEY} - NODELESS_WEBHOOK_SECRET: ${NODELESS_WEBHOOK_SECRET} - # OpenNode - OPENNODE_API_KEY: ${OPENNODE_API_KEY} - # Lnbits - LNBITS_API_KEY: ${LNBITS_API_KEY} - # Enable DEBUG for troubleshooting. Examples: - # DEBUG: "primary:*" - # DEBUG: "worker:*" - # DEBUG: "knex:query" - user: node:node - volumes: - - ${PWD}/.nostr:/home/node/.nostr - ports: - - 127.0.0.1:8008:8008 - depends_on: - nostream-cache: - condition: service_healthy - nostream-db: - condition: service_healthy - nostream-migrate: - condition: service_completed_successfully - restart: on-failure - networks: - default: - - nostream-db: - image: postgres:15 - container_name: nostream-db - environment: - POSTGRES_DB: nostr_ts_relay - POSTGRES_USER: nostr_ts_relay - POSTGRES_PASSWORD: nostr_ts_relay - volumes: - - ${PWD}/.nostr/data:/var/lib/postgresql/data - - ${PWD}/.nostr/db-logs:/var/log/postgresql - - ${PWD}/postgresql.conf:/postgresql.conf - networks: - default: - command: postgres -c 'config_file=/postgresql.conf' - restart: always - healthcheck: - test: ["CMD-SHELL", "pg_isready -U nostr_ts_relay"] - interval: 5s - timeout: 5s - retries: 5 - start_period: 360s - - nostream-cache: - image: redis:7.0.5-alpine3.16 - container_name: nostream-cache - volumes: - - cache:/data - command: redis-server --loglevel warning --requirepass nostr_ts_relay - networks: - default: - restart: always - healthcheck: - test: [ "CMD", "redis-cli", "ping", "|", "grep", "PONG" ] - interval: 1s - timeout: 5s - retries: 5 - - nostream-migrate: - image: node:24-alpine - container_name: nostream-migrate - environment: - DB_HOST: nostream-db - DB_PORT: 5432 - DB_USER: nostr_ts_relay - DB_PASSWORD: nostr_ts_relay - DB_NAME: nostr_ts_relay - entrypoint: - - sh - - -c - - 'cd code && npm install --no-save --quiet knex@2.4.0 pg@8.8.0 && npx knex migrate:latest' - volumes: - - ./migrations:/code/migrations - - ./knexfile.js:/code/knexfile.js - depends_on: - nostream-db: - condition: service_healthy - networks: - default: - -networks: - default: - name: nostream - ipam: - driver: default - config: - - subnet: 10.10.10.0/24 - -volumes: - cache: +services: + nostream: + build: . + container_name: nostream + environment: + SECRET: ${SECRET} + RELAY_PORT: 8008 + # Master + NOSTR_CONFIG_DIR: /home/node/.nostr + DB_HOST: nostream-db + DB_PORT: 5432 + DB_USER: nostr_ts_relay + DB_PASSWORD: nostr_ts_relay + DB_NAME: nostr_ts_relay + DB_MIN_POOL_SIZE: 16 + DB_MAX_POOL_SIZE: 64 + DB_ACQUIRE_CONNECTION_TIMEOUT: 60000 + # Read Replica + READ_REPLICAS: 2 + READ_REPLICA_ENABLED: 'false' + # Read Replica No. 1 + RR0_DB_HOST: db + RR0_DB_PORT: 5432 + RR0_DB_USER: nostr_ts_relay + RR0_DB_PASSWORD: nostr_ts_relay + RR0_DB_NAME: nostr_ts_relay + RR0_DB_MIN_POOL_SIZE: 16 + RR0_DB_MAX_POOL_SIZE: 64 + RR0_DB_ACQUIRE_CONNECTION_TIMEOUT: 10000 + # Read Replica No. 2 + RR1_DB_HOST: db + RR1_DB_PORT: 5432 + RR1_DB_USER: nostr_ts_relay + RR1_DB_PASSWORD: nostr_ts_relay + RR1_DB_NAME: nostr_ts_relay + RR1_DB_MIN_POOL_SIZE: 16 + RR1_DB_MAX_POOL_SIZE: 64 + RR1_DB_ACQUIRE_CONNECTION_TIMEOUT: 10000 + # Add RR2, RR3, etc. to configure more read replicas + # Redis + REDIS_HOST: nostream-cache + REDIS_PORT: 6379 + REDIS_USER: default + REDIS_PASSWORD: nostr_ts_relay + TOR_HOST: tor_proxy + TOR_CONTROL_PORT: 9051 + TOR_PASSWORD: nostr_ts_relay + HIDDEN_SERVICE_PORT: 80 + # Payments Processors + # Zebedee + ZEBEDEE_API_KEY: ${ZEBEDEE_API_KEY} + # Nodeless.io + NODELESS_API_KEY: ${NODELESS_API_KEY} + NODELESS_WEBHOOK_SECRET: ${NODELESS_WEBHOOK_SECRET} + # OpenNode + OPENNODE_API_KEY: ${OPENNODE_API_KEY} + # Lnbits + LNBITS_API_KEY: ${LNBITS_API_KEY} + # Enable DEBUG for troubleshooting. Examples: + # DEBUG: "primary:*" + # DEBUG: "worker:*" + # DEBUG: "knex:query" + user: node:node + volumes: + - ${PWD}/.nostr:/home/node/.nostr + ports: + - 127.0.0.1:8008:8008 + depends_on: + nostream-cache: + condition: service_healthy + nostream-db: + condition: service_healthy + nostream-migrate: + condition: service_completed_successfully + restart: on-failure + networks: + default: + ipv4_address: 10.10.10.2 + + nostream-db: + image: postgres:15 + container_name: nostream-db + environment: + POSTGRES_DB: nostr_ts_relay + POSTGRES_USER: nostr_ts_relay + POSTGRES_PASSWORD: nostr_ts_relay + volumes: + - ${PWD}/.nostr/data:/var/lib/postgresql/data + - ${PWD}/.nostr/db-logs:/var/log/postgresql + - ${PWD}/postgresql.conf:/postgresql.conf + networks: + default: + command: postgres -c 'config_file=/postgresql.conf' + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nostr_ts_relay"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 360s + + nostream-cache: + image: redis:7.0.5-alpine3.16 + container_name: nostream-cache + volumes: + - cache:/data + command: redis-server --loglevel warning --requirepass nostr_ts_relay + networks: + default: + restart: always + healthcheck: + test: [ "CMD", "redis-cli", "ping", "|", "grep", "PONG" ] + interval: 1s + timeout: 5s + retries: 5 + + nostream-migrate: + image: node:24-alpine + container_name: nostream-migrate + environment: + DB_HOST: nostream-db + DB_PORT: 5432 + DB_USER: nostr_ts_relay + DB_PASSWORD: nostr_ts_relay + DB_NAME: nostr_ts_relay + entrypoint: + - sh + - -c + - 'cd code && npm install --no-save --quiet knex@2.4.0 pg@8.8.0 && npx knex migrate:latest' + volumes: + - ./migrations:/code/migrations + - ./knexfile.js:/code/knexfile.js + depends_on: + nostream-db: + condition: service_healthy + networks: + default: + +networks: + default: + name: nostream + ipam: + driver: default + config: + - subnet: 10.10.10.0/24 + +volumes: + cache: From 67d1eb25794530fdf1a142339a42068b64150120 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:37:41 +0530 Subject: [PATCH 5/8] feat(i2p): add start/stop helpers and npm scripts (#35) --- scripts/print_i2p_hostname | 21 +++++++++++++++++++++ scripts/start_with_i2p | 38 ++++++++++++++++++++++++++++++++++++++ scripts/stop | 2 ++ 3 files changed, 61 insertions(+) create mode 100644 scripts/print_i2p_hostname create mode 100644 scripts/start_with_i2p diff --git a/scripts/print_i2p_hostname b/scripts/print_i2p_hostname new file mode 100644 index 00000000..4b594efd --- /dev/null +++ b/scripts/print_i2p_hostname @@ -0,0 +1,21 @@ +#!/bin/bash + +PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +KEYS_FILE="$PROJECT_ROOT/.nostr/i2p/data/nostream.dat" + +if [ ! -f "$KEYS_FILE" ]; then + echo "I2P destination keys not found. Is the i2pd container running?" + echo "Expected: $KEYS_FILE" + exit 1 +fi + +# i2pd writes a binary .dat file for tunnel keys. The base32 hostname is +# derived from the public key portion, but extracting it from the binary +# format is non-trivial without a helper. Instead, read it from the i2pd +# web console or container logs. +echo "I2P destination keys exist at: $KEYS_FILE" +echo "" +echo "To find your .b32.i2p address, use one of these methods:" +echo " 1. docker exec i2pd cat /home/i2pd/data/nostream.dat | head -c 391 | base32 | tr '[:upper:]' '[:lower:]' | tr '+/' '-~'" +echo " 2. Check the i2pd web console at http://127.0.0.1:7070/?page=i2p_tunnels" +echo " 3. docker logs i2pd 2>&1 | grep -i 'nostream'" diff --git a/scripts/start_with_i2p b/scripts/start_with_i2p new file mode 100644 index 00000000..0d3f5283 --- /dev/null +++ b/scripts/start_with_i2p @@ -0,0 +1,38 @@ +#!/bin/bash +PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" +DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" +I2P_DATA_DIR="$PROJECT_ROOT/.nostr/i2p/data" +NOSTR_CONFIG_DIR="${PROJECT_ROOT}/.nostr" +SETTINGS_FILE="${NOSTR_CONFIG_DIR}/settings.yaml" +DEFAULT_SETTINGS_FILE="${PROJECT_ROOT}/resources/default-settings.yaml" +CURRENT_DIR=$(pwd) + +if [[ ${CURRENT_DIR} =~ /scripts$ ]]; then + echo "Please run this script from the Nostream root folder, not the scripts directory." + echo "To do this, change up one directory, and then run the following command:" + echo "./scripts/start_with_i2p" + exit 1 +fi + +if [ "$EUID" -eq 0 ] + then echo "Error: Nostream should not be run as root." + exit 1 +fi + +if [[ ! -d "${NOSTR_CONFIG_DIR}" ]]; then + echo "Creating folder ${NOSTR_CONFIG_DIR}" + mkdir -p "${NOSTR_CONFIG_DIR}" +fi + +if [[ ! -f "${SETTINGS_FILE}" ]]; then + echo "Copying ${DEFAULT_SETTINGS_FILE} to ${SETTINGS_FILE}" + cp "${DEFAULT_SETTINGS_FILE}" "${SETTINGS_FILE}" +fi + +mkdir -p $I2P_DATA_DIR + +docker compose \ + -f $DOCKER_COMPOSE_FILE \ + -f $DOCKER_COMPOSE_I2P_FILE \ + up --build --remove-orphans $@ diff --git a/scripts/stop b/scripts/stop index 7122edf3..d63a59d3 100755 --- a/scripts/stop +++ b/scripts/stop @@ -2,10 +2,12 @@ PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" DOCKER_COMPOSE_TOR_FILE="${PROJECT_ROOT}/docker-compose.tor.yml" +DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" DOCKER_COMPOSE_LOCAL_FILE="${PROJECT_ROOT}/docker-compose.local.yml" docker compose \ -f $DOCKER_COMPOSE_FILE \ -f $DOCKER_COMPOSE_TOR_FILE \ + -f $DOCKER_COMPOSE_I2P_FILE \ -f $DOCKER_COMPOSE_LOCAL_FILE \ down $@ From 4af3eac431c292a329f025782be91de61ccc0869 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 22:38:58 +0530 Subject: [PATCH 6/8] docs: document I2P deployment and compose validation (#35) --- .env.example | 5 +++++ CONFIGURATION.md | 17 +++++++++++++++++ README.md | 16 ++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/.env.example b/.env.example index 6dd0975b..7f335c89 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,8 @@ WORKER_COUNT=2 # Defaults to CPU count. Use 1 or 2 for local testing. # TOR_CONTROL_PORT=9051 # TOR_PASSWORD= # HIDDEN_SERVICE_PORT=80 + +# --- I2P (Optional) --- +# To enable I2P, use: ./scripts/start_with_i2p +# I2P tunnel configuration lives in i2p/tunnels.conf and i2p/i2pd.conf. +# No application-level env vars are needed; the i2pd sidecar handles everything. diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 908fbe76..2e22460f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -58,6 +58,23 @@ The following environment variables can be set: | DEBUG | Debugging filter | | | ZEBEDEE_API_KEY | Zebedee Project API Key | | +## I2P + +I2P support is provided as a sidecar container (i2pd) via `docker-compose.i2p.yml`, mirroring the Tor setup. No application-level environment variables are needed — the i2pd container creates an I2P server tunnel that forwards traffic to nostream's WebSocket port. + +Configuration files live in the `i2p/` directory: + +| File | Description | +|------|-------------| +| `i2p/tunnels.conf` | Defines the I2P server tunnel pointing at nostream (port 8008). | +| `i2p/i2pd.conf` | Minimal i2pd daemon configuration. | + +Tunnel keys are persisted at `.nostr/i2p/data/` so the `.b32.i2p` address survives container restarts. + +- Start with I2P: `./scripts/start_with_i2p` +- Print hostname hints: `./scripts/print_i2p_hostname` +- Validate compose merges without starting containers (requires Docker): `npm run compose:validate` (same logic as the `compose-validate` CI job). + If you've set READ_REPLICAS to 4, you should configure RR0_ through RR3_. # Settings diff --git a/README.md b/README.md index 4b391489..1f731442 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,22 @@ Print the Tor hostname: ./scripts/print_tor_hostname ``` +Start with I2P: + ``` + ./scripts/start_with_i2p + ``` + +Print the I2P hostname: + ``` + ./scripts/print_i2p_hostname + ``` + +Verify that Docker Compose files merge correctly (no stack started; requires Docker on PATH): + ``` + npm run compose:validate + ``` + This runs `docker compose … config -q` for the I2P-only, Tor-only, and Tor+I2P overlay combinations (same check as the `compose-validate` CI job). + ### Importing events from JSON Lines You can import NIP-01 events from a `.jsonl` file directly into the relay database. From 7d1e372bf88143b1522067baf98821b991464546 Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sat, 18 Apr 2026 23:42:29 +0530 Subject: [PATCH 7/8] chore: required review changes (#35) --- .github/workflows/checks.yml | 12 ----- CONFIGURATION.md | 1 - README.md | 8 +--- package.json | 1 - scripts/verify-compose-overlays.mjs | 73 ----------------------------- scripts/verify_compose_overlays | 3 -- 6 files changed, 1 insertion(+), 97 deletions(-) delete mode 100644 scripts/verify-compose-overlays.mjs delete mode 100644 scripts/verify_compose_overlays diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 701f0c57..7bc5bf8e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -58,16 +58,6 @@ jobs: run: npm ci - name: Run build check run: npm run build:check - compose-validate: - name: Validate Docker Compose overlays (Tor / I2P) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Validate merged compose files - env: - SECRET: ci_placeholder_not_for_production_use_repeat_to_64chars_aaaaaaaa - run: node ./scripts/verify-compose-overlays.mjs test-units-and-cover: name: Unit Tests And Coverage runs-on: ubuntu-latest @@ -75,7 +65,6 @@ jobs: - commit-lint - lint - build-check - - compose-validate steps: - name: Checkout uses: actions/checkout@v3 @@ -111,7 +100,6 @@ jobs: - commit-lint - lint - build-check - - compose-validate steps: - name: Checkout uses: actions/checkout@v3 diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 2e22460f..6e017870 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -73,7 +73,6 @@ Tunnel keys are persisted at `.nostr/i2p/data/` so the `.b32.i2p` address surviv - Start with I2P: `./scripts/start_with_i2p` - Print hostname hints: `./scripts/print_i2p_hostname` -- Validate compose merges without starting containers (requires Docker): `npm run compose:validate` (same logic as the `compose-validate` CI job). If you've set READ_REPLICAS to 4, you should configure RR0_ through RR3_. diff --git a/README.md b/README.md index 4d0b3b50..5ddb1b6d 100644 --- a/README.md +++ b/README.md @@ -244,12 +244,6 @@ Print the I2P hostname: ./scripts/print_i2p_hostname ``` -Verify that Docker Compose files merge correctly (no stack started; requires Docker on PATH): - ``` - npm run compose:validate - ``` - This runs `docker compose … config -q` for the I2P-only, Tor-only, and Tor+I2P overlay combinations (same check as the `compose-validate` CI job). - ### Importing events from JSON Lines You can import NIP-01 events from a `.jsonl` file directly into the relay database. @@ -642,7 +636,7 @@ To observe client and subscription counts in real-time during a test, you can in ```bash docker compose logs -f nostream ``` -======= + ## Export Events 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. diff --git a/package.json b/package.json index 5b153fad..578c6ea6 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "i2p:docker:compose:start": "./scripts/start_with_i2p", "i2p:hostname": "./scripts/print_i2p_hostname", "i2p:docker:compose:stop": "./scripts/stop", - "compose:validate": "node ./scripts/verify-compose-overlays.mjs", "docker:integration:run": "docker compose -f ./test/integration/docker-compose.yml run --rm tests", "docker:test:integration": "npm run docker:integration:run -- npm run test:integration", "docker:cover:integration": "npm run docker:integration:run -- npm run cover:integration", diff --git a/scripts/verify-compose-overlays.mjs b/scripts/verify-compose-overlays.mjs deleted file mode 100644 index 82569249..00000000 --- a/scripts/verify-compose-overlays.mjs +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env node -/** - * Validates docker-compose.yml merged with Tor / I2P overlays (config -q). - * Requires Docker Desktop / Engine. Does not start containers. - */ -import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import { dirname, join } from 'node:path' - -const root = join(dirname(fileURLToPath(import.meta.url)), '..') -process.chdir(root) - -function resolveDockerExe() { - if (process.env.DOCKER_EXE && existsSync(process.env.DOCKER_EXE)) { - return process.env.DOCKER_EXE - } - if (process.platform === 'win32' && process.env.ProgramFiles) { - const candidate = join( - process.env.ProgramFiles, - 'Docker', - 'Docker', - 'resources', - 'bin', - 'docker.exe', - ) - if (existsSync(candidate)) { - return candidate - } - } - return 'docker' -} - -const dockerExe = resolveDockerExe() - -const dockerCheck = spawnSync(dockerExe, ['compose', 'version'], { encoding: 'utf8' }) -if (dockerCheck.error || dockerCheck.status !== 0) { - process.stderr.write( - 'docker is not installed or not on PATH. Install Docker Desktop / Engine, then run:\n' + - ' npm run compose:validate\n' + - 'On Windows, ensure Docker Desktop is running, or set DOCKER_EXE to the full path to docker.exe.\n', - ) - process.exit(1) -} - -if (!process.env.SECRET || process.env.SECRET.length < 16) { - process.env.SECRET = - 'ci_placeholder_not_for_production_use_repeat_to_64chars_aaaaaaaa' -} - -const runs = [ - ['docker-compose.yml', 'docker-compose.i2p.yml'], - ['docker-compose.yml', 'docker-compose.tor.yml'], - ['docker-compose.yml', 'docker-compose.tor.yml', 'docker-compose.i2p.yml'], -] - -for (const files of runs) { - const args = ['compose'] - for (const f of files) { - args.push('-f', f) - } - args.push('config', '-q') - const label = files.join(' + ') - process.stdout.write(`== ${label} ==\n`) - const r = spawnSync(dockerExe, args, { stdio: 'inherit' }) - if (r.status !== 0) { - process.stderr.write(`compose validation failed for: ${label}\n`) - process.exit(r.status ?? 1) - } - process.stdout.write('OK\n') -} - -process.stdout.write('All compose overlay merges validate successfully.\n') diff --git a/scripts/verify_compose_overlays b/scripts/verify_compose_overlays deleted file mode 100644 index 60d07a4c..00000000 --- a/scripts/verify_compose_overlays +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# Delegates to Node so the same validation runs on Windows (npm) and Linux/macOS. -exec node "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/verify-compose-overlays.mjs" From 80664cf62e3a1f71977fc418d85bf0da5681f86c Mon Sep 17 00:00:00 2001 From: archief2910 Date: Sun, 19 Apr 2026 00:29:16 +0530 Subject: [PATCH 8/8] chore: required review changes by copilot (#35) --- CONFIGURATION.md | 2 ++ docker-compose.i2p.yml | 5 +++++ i2p/i2pd.conf | 10 ++++++++++ scripts/print_i2p_hostname | 28 +++++++++++++++------------- scripts/start_with_i2p | 20 +++++++++++--------- scripts/stop | 14 ++++++++------ 6 files changed, 51 insertions(+), 28 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 6e017870..5e4faa7b 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -71,6 +71,8 @@ Configuration files live in the `i2p/` directory: Tunnel keys are persisted at `.nostr/i2p/data/` so the `.b32.i2p` address survives container restarts. +The i2pd web console (tunnel status, `.b32.i2p` destinations) is published to the host on **`127.0.0.1:7070`** only. Remove the `ports:` mapping in `docker-compose.i2p.yml` to disable host-side access. + - Start with I2P: `./scripts/start_with_i2p` - Print hostname hints: `./scripts/print_i2p_hostname` diff --git a/docker-compose.i2p.yml b/docker-compose.i2p.yml index 2125298a..ecca502d 100644 --- a/docker-compose.i2p.yml +++ b/docker-compose.i2p.yml @@ -8,6 +8,11 @@ services: - ${PWD}/.nostr/i2p/data:/home/i2pd/data - ${PWD}/i2p/i2pd.conf:/home/i2pd/data/i2pd.conf:ro - ${PWD}/i2p/tunnels.conf:/home/i2pd/data/tunnels.conf:ro + ports: + # i2pd web console — bound to 127.0.0.1 on the host so operators can + # look up the .b32.i2p destination without exposing router state + # to the LAN. Remove this mapping to disable host-side access. + - 127.0.0.1:7070:7070 restart: on-failure networks: default: diff --git a/i2p/i2pd.conf b/i2p/i2pd.conf index 26d44dd1..c41c83a9 100644 --- a/i2p/i2pd.conf +++ b/i2p/i2pd.conf @@ -1,6 +1,16 @@ # Minimal i2pd configuration for nostream. # Data and keys are persisted via the Docker volume mount at /home/i2pd/data. +[http] +# Bind the web console on all container interfaces so Docker port forwarding +# (127.0.0.1:7070 on the host) can reach it. Host binding is restricted in +# docker-compose.i2p.yml. +address = 0.0.0.0 +port = 7070 +# Accept requests whose Host header is 127.0.0.1 (port-forwarded) or the +# container's IP. Without this, i2pd returns a "host mismatch" error. +strictheaders = false + [limits] transittunnels = 256 diff --git a/scripts/print_i2p_hostname b/scripts/print_i2p_hostname index 4b594efd..4a9b02ed 100644 --- a/scripts/print_i2p_hostname +++ b/scripts/print_i2p_hostname @@ -1,21 +1,23 @@ #!/bin/bash +set -euo pipefail -PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." -KEYS_FILE="$PROJECT_ROOT/.nostr/i2p/data/nostream.dat" +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." +KEYS_FILE="${PROJECT_ROOT}/.nostr/i2p/data/nostream.dat" -if [ ! -f "$KEYS_FILE" ]; then +if [ ! -f "${KEYS_FILE}" ]; then echo "I2P destination keys not found. Is the i2pd container running?" - echo "Expected: $KEYS_FILE" + echo "Expected: ${KEYS_FILE}" exit 1 fi -# i2pd writes a binary .dat file for tunnel keys. The base32 hostname is -# derived from the public key portion, but extracting it from the binary -# format is non-trivial without a helper. Instead, read it from the i2pd -# web console or container logs. -echo "I2P destination keys exist at: $KEYS_FILE" +# The .b32.i2p address is derived from a SHA-256 hash of the Destination +# inside nostream.dat, so we cannot compute it portably from the host. +# Query the running i2pd container instead. +echo "I2P destination keys exist at: ${KEYS_FILE}" echo "" -echo "To find your .b32.i2p address, use one of these methods:" -echo " 1. docker exec i2pd cat /home/i2pd/data/nostream.dat | head -c 391 | base32 | tr '[:upper:]' '[:lower:]' | tr '+/' '-~'" -echo " 2. Check the i2pd web console at http://127.0.0.1:7070/?page=i2p_tunnels" -echo " 3. docker logs i2pd 2>&1 | grep -i 'nostream'" +echo "To find your nostream .b32.i2p address, use one of these methods:" +echo " 1. Open the i2pd web console: http://127.0.0.1:7070/?page=i2p_tunnels" +echo " (published by docker-compose.i2p.yml, bound to 127.0.0.1 only)" +echo " 2. Query the console from inside the container:" +echo " docker exec i2pd wget -qO- 'http://127.0.0.1:7070/?page=i2p_tunnels' \\" +echo " | grep -oE '[a-z2-7]{52}\\.b32\\.i2p' | sort -u" diff --git a/scripts/start_with_i2p b/scripts/start_with_i2p index 0d3f5283..cc458a3d 100644 --- a/scripts/start_with_i2p +++ b/scripts/start_with_i2p @@ -1,12 +1,14 @@ #!/bin/bash -PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +set -euo pipefail + +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" -I2P_DATA_DIR="$PROJECT_ROOT/.nostr/i2p/data" +I2P_DATA_DIR="${PROJECT_ROOT}/.nostr/i2p/data" NOSTR_CONFIG_DIR="${PROJECT_ROOT}/.nostr" SETTINGS_FILE="${NOSTR_CONFIG_DIR}/settings.yaml" DEFAULT_SETTINGS_FILE="${PROJECT_ROOT}/resources/default-settings.yaml" -CURRENT_DIR=$(pwd) +CURRENT_DIR="$(pwd)" if [[ ${CURRENT_DIR} =~ /scripts$ ]]; then echo "Please run this script from the Nostream root folder, not the scripts directory." @@ -15,8 +17,8 @@ if [[ ${CURRENT_DIR} =~ /scripts$ ]]; then exit 1 fi -if [ "$EUID" -eq 0 ] - then echo "Error: Nostream should not be run as root." +if [ "$EUID" -eq 0 ]; then + echo "Error: Nostream should not be run as root." exit 1 fi @@ -30,9 +32,9 @@ if [[ ! -f "${SETTINGS_FILE}" ]]; then cp "${DEFAULT_SETTINGS_FILE}" "${SETTINGS_FILE}" fi -mkdir -p $I2P_DATA_DIR +mkdir -p "${I2P_DATA_DIR}" docker compose \ - -f $DOCKER_COMPOSE_FILE \ - -f $DOCKER_COMPOSE_I2P_FILE \ - up --build --remove-orphans $@ + -f "${DOCKER_COMPOSE_FILE}" \ + -f "${DOCKER_COMPOSE_I2P_FILE}" \ + up --build --remove-orphans "$@" diff --git a/scripts/stop b/scripts/stop index d63a59d3..788cf020 100755 --- a/scripts/stop +++ b/scripts/stop @@ -1,13 +1,15 @@ #!/bin/bash -PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +set -euo pipefail + +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" DOCKER_COMPOSE_TOR_FILE="${PROJECT_ROOT}/docker-compose.tor.yml" DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" DOCKER_COMPOSE_LOCAL_FILE="${PROJECT_ROOT}/docker-compose.local.yml" docker compose \ - -f $DOCKER_COMPOSE_FILE \ - -f $DOCKER_COMPOSE_TOR_FILE \ - -f $DOCKER_COMPOSE_I2P_FILE \ - -f $DOCKER_COMPOSE_LOCAL_FILE \ - down $@ + -f "${DOCKER_COMPOSE_FILE}" \ + -f "${DOCKER_COMPOSE_TOR_FILE}" \ + -f "${DOCKER_COMPOSE_I2P_FILE}" \ + -f "${DOCKER_COMPOSE_LOCAL_FILE}" \ + down "$@"