Skip to content

Initial implementation: validation pipeline, store, and dashboard - #1

Open
alltheseas wants to merge 6 commits into
mainfrom
initial-implementation
Open

Initial implementation: validation pipeline, store, and dashboard#1
alltheseas wants to merge 6 commits into
mainfrom
initial-implementation

Conversation

@alltheseas

@alltheseas alltheseas commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • NIP-01 relay skeleton — WebSocket server handling EVENT/REQ/CLOSE, NIP-11 info document
  • Schemata validation + crypto verification — SHA-256 id hash, schnorr signature, schemata-codegen validators (179 known kinds, 133 with tag/content constraints), three-tier classification (validated/base-only/unknown)
  • SQLite validation store — persists every result with REST API (/api/results, /api/summary, /api/pubkeys)
  • Real-time web dashboard — self-contained HTML served at relay root, SSE live streaming, filter by pubkey/tier/status, light+dark themes
  • README — quick start, architecture diagram, relation to schemata/schemata-codegen/sherlock/nostr-test-vectors

Validation pipeline

EVENT → base fields → id hash (SHA-256) → signature (schnorr) → schemata validators → store → SSE → dashboard

Crypto failures are rejected (OK false). Schema errors are accepted (OK true) with error details — clients keep working while devs see what needs fixing.

Commits

Each commit is standalone and builds on the previous:

  1. Scaffold NIP-01 WebSocket relay skeleton — minimal relay, passes nak publish
  2. Add schemata validation and cryptographic verification — vendors codegen output, adds id/sig verification
  3. Add SQLite validation store and REST API — better-sqlite3 persistence, three query endpoints
  4. Add real-time web dashboard with SSE live updates — self-contained HTML, live event stream
  5. Add README — docs, quick start, architecture, nostrability ecosystem context

Test plan

  • npm install && npm run build && npm start — relay starts on ws://localhost:7777
  • Send a kind 1 event via nak — accepted, tier=base-only
  • Send a kind 4 event without p tag — accepted with 3 schemata errors in OK message
  • Send an event with bad signature — rejected (OK false)
  • Open http://localhost:7777 — dashboard shows results with live updates
  • curl /api/summary returns correct pass/fail counts
  • curl /api/pubkeys lists seen pubkeys with counts

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Nostr test-relay with real-time event validation and categorized outcomes (validated, base-only, unknown).
    • Web dashboard showing live stream, summary stats, tier/kind breakdowns, pubkey filter, per-event details, and optional client-tag display.
    • REST APIs and SSE for querying results and streaming live validation events; default local endpoints provided.
  • Documentation

    • Added comprehensive README with quick-start, architecture, defaults, and license.

alltheseas and others added 5 commits April 1, 2026 00:46
Minimal test relay that accepts EVENT (with base field validation),
REQ (immediate EOSE, no stored events), and CLOSE. Serves NIP-11
relay info document. Verified working with nak and raw WebSocket client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate the schemata-codegen generated validators (179 kinds, 133
with tag/content constraints) into the event handling pipeline:

- Verify event id matches SHA-256 of NIP-01 serialized form
- Verify schnorr signature via @noble/curves
- Run validateEvent() for content constraints and validateKindTags()
  for tag structure constraints
- Classify each kind into three tiers: validated (has schema
  constraints), base-only (known but no constraints), unknown
- Accept schema-invalid events (true) with error details in OK message
  so clients continue working; reject only crypto failures

Vendored files from schemata-codegen: validators.ts (2907 lines,
139 kind-specific tag validators) and kind-registry.ts (177 kind
metadata entries with names and categories).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Persist every validation result to SQLite (better-sqlite3, WAL mode)
with indexes on pubkey, kind, and received_at for dashboard queries.

Schema: event_id (PK), pubkey, kind, kind_name, created_at,
received_at, tier, valid, errors (JSON), client_tag.

REST API endpoints:
- GET /api/results?pubkey=&kind=&limit=&offset= — query results
- GET /api/summary?pubkey= — aggregate pass/fail by tier and kind
- GET /api/pubkeys — list pubkeys with event counts

NIP-89 client tag extracted from event tags and stored for
attribution in the dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-contained HTML dashboard served at the relay's HTTP root.
Connects to /api/events via Server-Sent Events for live streaming
of validation results as events arrive.

Features:
- Summary stats: total events, passed, failed, kinds seen
- Tier breakdown badges (validated/base-only/unknown with counts)
- Filter by pubkey, validation tier, and pass/fail status
- Per-event detail: kind name, event id, pubkey, timestamp,
  client tag, and inline schema error display
- Light/dark theme via prefers-color-scheme
- Responsive layout, reduced-motion support

Bumps version to 0.3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the validation pipeline, three-tier classification,
dashboard features, REST API, configuration, and how test-relay
relates to schemata, schemata-codegen, nostr-test-vectors, and
sherlock within the nostrability ecosystem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds a standalone Nostr test-relay: HTTP + WebSocket servers, a four-stage event validation pipeline, SQLite persistence for validation results, a live SSE-backed dashboard, and supporting artifacts (kind registry, TypeScript config, package manifest, README).

Changes

Cohort / File(s) Summary
Project metadata & config
README.md, package.json, tsconfig.json
New README and package manifest; TypeScript compiler settings and build/dev scripts.
Server & API
src/index.ts
New HTTP + WebSocket server implementing NIP-11 relay info, REST endpoints (/api/results, /api/summary, /api/pubkeys), SSE /api/events, WebSocket REQ/EVENT/CLOSE handling, and a four-stage validation pipeline (base checks, ID SHA-256, schnorr sig verification, schemata validation) with persisted outcomes and OK/NOTICE replies.
Persistence layer
src/store.ts
SQLite-backed persistence: DB init, results table/indexes, saveResult, getResults, getSummary, getPubkeys, and types for ValidationResult/KindSummary/ValidationTier.
Kind metadata
src/kind-registry.ts
Auto-generated kind registry and types: KindMeta, KIND_REGISTRY, KIND_NAMES, and KNOWN_KIND_NUMBERS.
Dashboard UI
src/dashboard.ts
Exported HTML string for a self-contained dashboard: initial REST fetches, SSE live updates, client-side state, filtering, stats, and event list rendering.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant WS as WebSocket Server
    participant Validator as Event Validator
    participant DB as SQLite DB
    participant SSE as SSE Broadcaster
    participant Dashboard as Dashboard (Browser)

    Client->>WS: Send EVENT (WS message)
    WS->>Validator: Parse message, extract event
    Validator->>Validator: Base NIP-01 checks
    Validator->>Validator: Compute SHA-256 ID and compare
    Validator->>Validator: Verify schnorr signature
    Validator->>Validator: Schema validate & determine tier
    Validator->>DB: Persist ValidationResult
    DB->>SSE: Notify new result
    SSE->>Dashboard: Push event (SSE)
    WS->>Client: Reply with OK / NOTICE (validation outcome)
    Dashboard->>Dashboard: Update UI / filters / stats
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through SHA and schnorr tonight,
Checked tags and kinds by dashboard light,
Events pranced in, then left with a note,
Stored in SQLite, sent by SSE's throat,
A tiny rabbit cheers this testing flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the three main components introduced: validation pipeline, store, and dashboard.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch initial-implementation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alltheseas

Copy link
Copy Markdown
Contributor Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown

@alltheseas Sure, I'll review the changes right away!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
src/index.ts (2)

77-82: SSE write errors are silently ignored.

If a client disconnects unexpectedly or the write buffer is full, res.write() can fail. While the close event handler removes clients, there's no error handling for write failures which could cause unhandled exceptions in edge cases.

💡 Add defensive error handling
 function broadcastSSE(result: ValidationResult): void {
   const data = `data: ${JSON.stringify(result)}\n\n`;
   for (const res of sseClients) {
-    res.write(data);
+    try {
+      res.write(data);
+    } catch {
+      sseClients.delete(res);
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 77 - 82, The broadcastSSE function currently calls
res.write(...) for each client in sseClients without handling write failures;
wrap each write in a try/catch (or check the return value) and on error remove
the failing client from sseClients and properly close/destroy the response
stream (and optionally log the error). Also add an 'error' event listener to
each SSE response when clients are added so write-time errors are handled
centrally; update the code that manages sseClients (where clients are pushed)
and the broadcastSSE function to reference those listeners and cleanup logic.

494-506: Startup logging exposes binding address — consider operational note.

The server binds to all interfaces by default when no host is specified in listen(). This is fine for local development but should be documented for production deployment scenarios.

For production deployments, consider:

  • Binding to specific interface: httpServer.listen(PORT, '127.0.0.1', ...)
  • Adding environment variable for bind address
  • Documenting reverse proxy setup in README
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 494 - 506, Change the httpServer.listen call to
accept a configurable bind address instead of implicitly binding to all
interfaces: read a BIND_ADDRESS (or similar) env var with a safe default (e.g.,
'0.0.0.0' or '127.0.0.1' depending on desired default), pass that value as the
host argument to httpServer.listen(PORT, bindAddress, ...), and update the
console output to show the effective bind address used; this uses the existing
httpServer.listen and PORT symbols so you only need to replace the current
listen invocation and logs to reference the chosen bindAddress and document the
new env var in README or deployment notes.
src/dashboard.ts (2)

228-228: External Google Fonts dependency has privacy implications.

Loading fonts from Google Fonts sends user IP addresses to Google. For a developer testing tool this may be acceptable, but consider noting this in documentation or providing a self-hosted alternative for privacy-conscious users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard.ts` at line 228, The HTML in src/dashboard.ts currently loads
Google Fonts via an external <link> tag which leaks user IPs; remove or make
this optional and provide a self-hosted/fallback strategy instead: replace the
hardcoded external <link> usage with a configurable option (e.g., a flag or env
var) that either serves local font files bundled with the app or falls back to
system fonts, and update documentation to mention the privacy trade-off and how
to enable the self-hosted fonts; search for the Google Fonts <link> tag in
src/dashboard.ts to locate the change point and implement the config-driven
loading and doc note.

303-306: SSE reconnect uses fixed delay without exponential backoff.

A fixed 3-second reconnect could cause connection storms if the server is under load or temporarily unavailable. Consider exponential backoff with jitter and a maximum retry limit.

💡 Add exponential backoff
let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 30000;

function connectSSE() {
  const es = new EventSource('/api/events');
  es.onopen = () => { reconnectAttempts = 0; };
  es.onmessage = (e) => { /* ... existing code ... */ };
  es.onerror = () => {
    es.close();
    const delay = Math.min(1000 * Math.pow(2, reconnectAttempts) + Math.random() * 1000, MAX_RECONNECT_DELAY);
    reconnectAttempts++;
    setTimeout(connectSSE, delay);
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard.ts` around lines 303 - 306, The current es.onerror handler in
connectSSE uses a fixed 3s retry; replace this with an exponential backoff with
jitter and a cap: introduce a reconnectAttempts counter (reset to 0 in
es.onopen), define a MAX_RECONNECT_DELAY (e.g., 30000ms) and compute delay =
min(base * 2^reconnectAttempts + randomJitter, MAX_RECONNECT_DELAY), increment
reconnectAttempts on each error, then setTimeout(connectSSE, delay) after
es.close(); optionally enforce a maxAttempts limit to stop retrying. Ensure you
update the es.onopen and es.onerror handlers in the connectSSE function
accordingly (use the symbols reconnectAttempts, MAX_RECONNECT_DELAY, connectSSE,
es.onerror, es.onopen).
src/store.ts (1)

33-36: No guard against double initialization.

If initDB() is called multiple times, it will create a new database connection without closing the previous one, potentially causing resource leaks. While unlikely in current usage, adding a guard improves robustness.

💡 Add initialization guard
 let db: Database.Database;
 
 export function initDB(): Database.Database {
+  if (db) return db;
   db = new Database(DB_PATH);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store.ts` around lines 33 - 36, The initDB function currently creates a
new Database instance every call (using the db variable and Database.Database),
risking resource leaks; add a guard in initDB that checks the module-level db
variable and if already initialized either returns the existing db or first
closes it before creating a new one, ensuring you call the Database.close() (or
appropriate close method) on the old instance if you choose to recreate; update
initDB to return the existing Database.Database when present and ensure any
caller that expects reinitialization can explicitly call a new teardown/close
function you add.
package.json (1)

10-10: Dev script has a potential race condition and cross-platform issue.

The tsc --watch & node --watch dist/index.js pattern:

  1. Runs tsc --watch in background, then immediately starts node --watch
  2. On first run, dist/index.js may not exist yet (TypeScript hasn't finished compiling)
  3. The & operator is shell-specific and won't work on Windows cmd

Consider using a tool like concurrently or npm-run-all for reliable parallel execution with proper startup sequencing.

💡 Suggested alternative using concurrently
   "scripts": {
     "build": "tsc",
     "start": "node dist/index.js",
-    "dev": "tsc --watch & node --watch dist/index.js"
+    "dev": "npm run build && concurrently \"tsc --watch\" \"node --watch dist/index.js\""
   },

And add to devDependencies:

"concurrently": "^8.0.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 10, The dev script "tsc --watch & node --watch
dist/index.js" causes a race on initial compile and is shell-specific; replace
it with a cross-platform parallel runner (e.g., add "concurrently" to
devDependencies) and change the "dev" script to run the TypeScript compiler and
the Node process via that tool so node only starts/restarts after files in dist
exist (or use nodemon via the runner to watch dist). Update package.json's dev
script and add the concurrently dependency, referencing the existing "tsc
--watch" and "node --watch dist/index.js" commands when constructing the new
concurrently-based script.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 59-64: The fenced code block listing the API endpoints (the lines
starting with "GET /api/results...", "GET /api/summary...", "GET /api/pubkeys",
and "GET /api/events") is missing a language specifier; update the opening fence
from ``` to a specified language such as ```text or ```http so static analysis
passes and the block is properly highlighted.
- Around line 96-117: The fenced code block containing the architecture diagram
uses plain triple backticks without a language specifier; update the opening
fence from ``` to ```text to mark it as a text block (the block that starts with
the diagram lines "Client (any Nostr app)" and the EVENT line) so markdown
renderers treat it as plain text and preserve formatting.

In `@src/dashboard.ts`:
- Around line 383-386: The code building the error HTML assigns r.errors entries
to errors but escapes only e.message; escape e.path as well by passing it
through the existing escapeHtml helper before concatenation so the line that
constructs errors uses escapeHtml(e.path) instead of e.path (refer to r.errors,
e.path, escapeHtml and the errors variable / "<div class=\"event-errors\">").
This ensures both path and message are HTML-escaped and prevents XSS when
rendering the event-errors block.

In `@src/index.ts`:
- Around line 132-146: In handleApiResults, the numeric query params (kind,
limit, offset) must be validated for NaN before calling getResults; after
parsing each with Number(), check isNaN(...) and if any are invalid return a 400
jsonResponse with an error message (so getResults only ever receives valid
numbers or undefined for kind), otherwise use the parsed values (with existing
defaults for limit/offset) when calling getResults; update handleApiResults to
perform these isNaN checks and short-circuit with jsonResponse on error.

In `@src/kind-registry.ts`:
- Around line 284-292: Kind 777 (“nipless”, name "Spell — portable REQ filter
(nipless)") was generated with hasConditionals: false but category:
"conditional", so update the upstream schemata so the generated output matches
the other conditional kinds: change the schemata definition for kind 777 to mark
it as conditional (set hasConditionals=true or equivalent flag in the source
schema) or adjust the category mapping logic so that a conditional kind yields
category "conditional" only when hasConditionals is true; then re-run
`@nostrability/schemata-codegen` to regenerate the file (look for the schema entry
for kind 777 or the mapping function that derives category/hasConditionals) and
run tests to ensure consistency with other "nipless" conditional kinds.

In `@src/store.ts`:
- Around line 181-194: The rowToResult function uses JSON.parse on row.errors
which can throw on malformed JSON; wrap the parsing of row.errors in a defensive
try/catch inside rowToResult (or a small helper) so that if JSON.parse fails you
return a safe default (e.g., an empty array or null) and do not let the
exception bubble up; include a descriptive debug/warn log mentioning
event_id/pubkey and the parse error to aid troubleshooting while still returning
a valid ValidationResult with errors set to the safe default.

---

Nitpick comments:
In `@package.json`:
- Line 10: The dev script "tsc --watch & node --watch dist/index.js" causes a
race on initial compile and is shell-specific; replace it with a cross-platform
parallel runner (e.g., add "concurrently" to devDependencies) and change the
"dev" script to run the TypeScript compiler and the Node process via that tool
so node only starts/restarts after files in dist exist (or use nodemon via the
runner to watch dist). Update package.json's dev script and add the concurrently
dependency, referencing the existing "tsc --watch" and "node --watch
dist/index.js" commands when constructing the new concurrently-based script.

In `@src/dashboard.ts`:
- Line 228: The HTML in src/dashboard.ts currently loads Google Fonts via an
external <link> tag which leaks user IPs; remove or make this optional and
provide a self-hosted/fallback strategy instead: replace the hardcoded external
<link> usage with a configurable option (e.g., a flag or env var) that either
serves local font files bundled with the app or falls back to system fonts, and
update documentation to mention the privacy trade-off and how to enable the
self-hosted fonts; search for the Google Fonts <link> tag in src/dashboard.ts to
locate the change point and implement the config-driven loading and doc note.
- Around line 303-306: The current es.onerror handler in connectSSE uses a fixed
3s retry; replace this with an exponential backoff with jitter and a cap:
introduce a reconnectAttempts counter (reset to 0 in es.onopen), define a
MAX_RECONNECT_DELAY (e.g., 30000ms) and compute delay = min(base *
2^reconnectAttempts + randomJitter, MAX_RECONNECT_DELAY), increment
reconnectAttempts on each error, then setTimeout(connectSSE, delay) after
es.close(); optionally enforce a maxAttempts limit to stop retrying. Ensure you
update the es.onopen and es.onerror handlers in the connectSSE function
accordingly (use the symbols reconnectAttempts, MAX_RECONNECT_DELAY, connectSSE,
es.onerror, es.onopen).

In `@src/index.ts`:
- Around line 77-82: The broadcastSSE function currently calls res.write(...)
for each client in sseClients without handling write failures; wrap each write
in a try/catch (or check the return value) and on error remove the failing
client from sseClients and properly close/destroy the response stream (and
optionally log the error). Also add an 'error' event listener to each SSE
response when clients are added so write-time errors are handled centrally;
update the code that manages sseClients (where clients are pushed) and the
broadcastSSE function to reference those listeners and cleanup logic.
- Around line 494-506: Change the httpServer.listen call to accept a
configurable bind address instead of implicitly binding to all interfaces: read
a BIND_ADDRESS (or similar) env var with a safe default (e.g., '0.0.0.0' or
'127.0.0.1' depending on desired default), pass that value as the host argument
to httpServer.listen(PORT, bindAddress, ...), and update the console output to
show the effective bind address used; this uses the existing httpServer.listen
and PORT symbols so you only need to replace the current listen invocation and
logs to reference the chosen bindAddress and document the new env var in README
or deployment notes.

In `@src/store.ts`:
- Around line 33-36: The initDB function currently creates a new Database
instance every call (using the db variable and Database.Database), risking
resource leaks; add a guard in initDB that checks the module-level db variable
and if already initialized either returns the existing db or first closes it
before creating a new one, ensuring you call the Database.close() (or
appropriate close method) on the old instance if you choose to recreate; update
initDB to return the existing Database.Database when present and ensure any
caller that expects reinitialization can explicitly call a new teardown/close
function you add.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd870203-1be8-417e-b5dd-d9fbcd28c84a

📥 Commits

Reviewing files that changed from the base of the PR and between fdfb091 and 391b33f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • README.md
  • package.json
  • src/dashboard.ts
  • src/index.ts
  • src/kind-registry.ts
  • src/schemata-validators.ts
  • src/store.ts
  • tsconfig.json

Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread src/dashboard.ts
Comment thread src/index.ts
Comment thread src/kind-registry.ts
Comment on lines +284 to +292
777: {
kind: 777,
nip: "nipless",
name: "Spell — portable REQ filter (nipless)",
description: "Spell — portable REQ filter (Grimoire/nak)",
requiredTags: ["cmd"],
hasConditionals: false,
category: "conditional",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if other kinds have similar inconsistency
rg -n 'hasConditionals: false' src/kind-registry.ts | head -20
rg -n 'category: "conditional"' src/kind-registry.ts | head -20

Repository: nostrability/test-relay

Length of output: 1357


🏁 Script executed:

#!/bin/bash
# Extract context around each conditional category to see the full kind entry
# Get line numbers for conditional category
conditional_lines=$(rg -n 'category: "conditional"' src/kind-registry.ts | cut -d: -f1)

# For each line, show context to identify kind number and hasConditionals value
for line_num in $conditional_lines; do
  # Show the kind entry (usually 10 lines)
  start=$((line_num - 8))
  sed -n "${start},${line_num}p" src/kind-registry.ts | head -10
  echo "---"
done | head -100

Repository: nostrability/test-relay

Length of output: 2578


🏁 Script executed:

#!/bin/bash
# Check for any comments or documentation about kind 777 or Spell
rg -B5 -A5 'kind: 777' src/kind-registry.ts

# Check for codegen markers or comments in the file
head -50 src/kind-registry.ts | grep -i 'auto\|generated\|codegen'

# Check if there are other "nipless" kinds
rg -n 'nipless' src/kind-registry.ts

Repository: nostrability/test-relay

Length of output: 1636


🏁 Script executed:

#!/bin/bash
# Get all nipless kinds and check their hasConditionals and category values
rg -B6 'nipless' src/kind-registry.ts | grep -E 'kind: [0-9]+|hasConditionals:|category:' | paste - - - | head -20

Repository: nostrability/test-relay

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Extract all nipless kind entries with their properties
python3 << 'EOF'
import re

with open('src/kind-registry.ts', 'r') as f:
    content = f.read()

# Find all kind entries that contain "nipless"
pattern = r'(\d+):\s*\{[^}]*nip:\s*"nipless"[^}]*hasConditionals:\s*(true|false)[^}]*category:\s*"([^"]+)"'
matches = re.findall(pattern, content, re.DOTALL)

print("Kind | hasConditionals | category")
print("-----|-----------------|----------")
for kind, has_cond, category in matches:
    print(f"{kind:5} | {has_cond:15} | {category}")
EOF

Repository: nostrability/test-relay

Length of output: 453


Fix upstream codegen for kind 777: inconsistent hasConditionals and category fields.

Kind 777 has hasConditionals: false but category: "conditional", conflicting with all other conditional kinds (which have hasConditionals: true). All other "nipless" kinds consistently pair their categories with their hasConditionals value. Since this is auto-generated by @nostrability/schemata-codegen, investigate and fix the upstream schemata definition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kind-registry.ts` around lines 284 - 292, Kind 777 (“nipless”, name
"Spell — portable REQ filter (nipless)") was generated with hasConditionals:
false but category: "conditional", so update the upstream schemata so the
generated output matches the other conditional kinds: change the schemata
definition for kind 777 to mark it as conditional (set hasConditionals=true or
equivalent flag in the source schema) or adjust the category mapping logic so
that a conditional kind yields category "conditional" only when hasConditionals
is true; then re-run `@nostrability/schemata-codegen` to regenerate the file (look
for the schema entry for kind 777 or the mapping function that derives
category/hasConditionals) and run tests to ensure consistency with other
"nipless" conditional kinds.

Comment thread src/store.ts
- Escape e.path in dashboard error rendering (XSS prevention)
- Validate numeric query params in /api/results (return 400 on NaN)
- Handle SSE write errors by removing stale clients
- Guard initDB against double initialization
- Defensive try/catch on JSON.parse in rowToResult
- Add language specifiers to README fenced code blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/dashboard.ts (1)

400-402: Consider adding single-quote escaping for defense in depth.

The escapeHtml function handles &, <, >, and ", but omits single quotes ('). While current usage only involves double-quoted attributes, adding .replace(/'/g, '&#39;') provides additional protection if the function is used in other contexts.

♻️ More complete escaping
 function escapeHtml(s) {
-  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
+  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard.ts` around lines 400 - 402, The escapeHtml function currently
replaces &, <, > and " but omits single quotes; update the escapeHtml
implementation (function escapeHtml) to also replace single quotes by adding a
.replace(/'/g,'&#39;') to the chain so single-quote characters are HTML-escaped
(defense-in-depth for usages in single-quoted attributes or other contexts).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 36-41: README counts are out of sync with the registry: validate
whether the doc should reference KNOWN_KIND_NUMBERS (179 entries) or the full
KIND_REGISTRY (180 entries), then update the table counts to match that chosen
source; specifically adjust the "base-only" count from 46 to 47 and the total
from 179 to 180 if you choose KIND_REGISTRY (or change the total to 179 and keep
base-only 47 if you choose KNOWN_KIND_NUMBERS), and ensure the README text and
the table rows referencing "validated", "base-only", and "total" reflect the
final numbers; check symbols KIND_REGISTRY and KNOWN_KIND_NUMBERS to determine
which to cite in the doc.

---

Nitpick comments:
In `@src/dashboard.ts`:
- Around line 400-402: The escapeHtml function currently replaces &, <, > and "
but omits single quotes; update the escapeHtml implementation (function
escapeHtml) to also replace single quotes by adding a .replace(/'/g,'&#39;') to
the chain so single-quote characters are HTML-escaped (defense-in-depth for
usages in single-quoted attributes or other contexts).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57614c21-fb7d-4c77-9a70-6198057620c6

📥 Commits

Reviewing files that changed from the base of the PR and between 391b33f and 5e19015.

📒 Files selected for processing (4)
  • README.md
  • src/dashboard.ts
  • src/index.ts
  • src/store.ts

Comment thread README.md
Comment on lines +36 to +41
| Tier | Meaning | Count |
|------|---------|-------|
| **validated** | Schema exists with tag/content constraints — fully checked | 133 kinds |
| **base-only** | Known kind but no tag/content constraints in schemata — only NIP-01 base fields checked | 46 kinds |
| **unknown** | Kind not in schemata at all — only NIP-01 base fields checked | everything else |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify kind counts in the codebase match README documentation

echo "=== Counting kinds in kind-registry.ts ==="

# Count KNOWN_KIND_NUMBERS array length
echo "Total known kinds:"
rg -oP '\d+' src/kind-registry.ts | head -1000 | wc -l

# More precise: count entries in KNOWN_KIND_NUMBERS array
ast-grep --pattern 'export const KNOWN_KIND_NUMBERS: readonly number[] = [$$$]' | head -50

# Count entries where category !== "bare" (validated kinds)
echo ""
echo "Checking KIND_REGISTRY entries..."
rg 'category:' src/kind-registry.ts | head -20

# Count bare vs non-bare
echo ""
echo "Bare category count:"
rg -c 'category: "bare"' src/kind-registry.ts || echo "0"

echo ""
echo "Non-bare (validated) kinds:"
rg 'category:' src/kind-registry.ts | rg -v '"bare"' | wc -l

Repository: nostrability/test-relay

Length of output: 2560


🏁 Script executed:

#!/bin/bash
# Get precise count of KNOWN_KIND_NUMBERS array entries
echo "=== Counting KNOWN_KIND_NUMBERS array entries ==="

# Extract just the array content and count numbers
sed -n '/export const KNOWN_KIND_NUMBERS/,/^\]/p' src/kind-registry.ts | grep -oP '\d+' | wc -l

echo ""
echo "=== Verify total kinds in KIND_REGISTRY ==="
# Count total KIND_REGISTRY entries
rg -c 'kind:' src/kind-registry.ts | head -1

echo ""
echo "=== Double-check bare count ==="
rg -c 'category: "bare"' src/kind-registry.ts

echo ""
echo "=== Count all category types ==="
rg -o 'category: "[^"]*"' src/kind-registry.ts | sort | uniq -c

Repository: nostrability/test-relay

Length of output: 367


Update kind counts in documentation to match current registry data.

The documented counts in the README (133 validated, 46 base-only, 179 total) are out of sync with the actual kind-registry.ts:

  • Validated kinds: 133 ✓ (correct)
  • Base-only kinds: 47 (documented as 46)
  • Total kinds in KIND_REGISTRY: 180 (documented as 179)

Confirm whether the documentation should reference KNOWN_KIND_NUMBERS (179 entries) or the full KIND_REGISTRY (180 entries), then update the counts and totals accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 36 - 41, README counts are out of sync with the
registry: validate whether the doc should reference KNOWN_KIND_NUMBERS (179
entries) or the full KIND_REGISTRY (180 entries), then update the table counts
to match that chosen source; specifically adjust the "base-only" count from 46
to 47 and the total from 179 to 180 if you choose KIND_REGISTRY (or change the
total to 179 and keep base-only 47 if you choose KNOWN_KIND_NUMBERS), and ensure
the README text and the table rows referencing "validated", "base-only", and
"total" reflect the final numbers; check symbols KIND_REGISTRY and
KNOWN_KIND_NUMBERS to determine which to cite in the doc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant