Skip to content

Integration draft proposal: @nostrwatch/schemata-codegen package for nostr-watch #8

@alltheseas

Description

@alltheseas

Context

nostr-watch is the primary NIP-66 relay monitoring system. It currently uses @nostrability/schemata + AJV for validation, but only in the browser GUI (Web Worker) and an auditor test library. The production server-side apps that publish and consume NIP-66 events — relaymon, route66 — have zero schema validation.

This issue proposes adding a @nostrwatch/schemata-codegen package to the nostr-watch monorepo, generated by this tool, to provide typed interfaces, a kind registry, lightweight validators, and pre-processed AJV schemas.

What schemata-codegen adds vs. what exists today

Capability schemata-js-ajv today schemata-codegen adds Benefit
TypeScript types per kind None — events are any/NostrEvent 177 interfaces with literal kind discriminants Catch malformed events at compile time; enables IDE autocomplete on event fields
Tag tuple types None — tags are string[][] 156 typed tuples (ETag, RTag, DTag, etc.) Prevents tag construction bugs (wrong position, missing field); self-documenting code
Kind name registry None KIND_NAMES[30166]"Relay Discovery (NIP-66)" Human-readable labels in UI, logs, and error messages without maintaining a manual mapping
Lightweight validators Full AJV (heavy, needs peer dep) Zero-dependency tag validators (~132 kinds) Runs anywhere (edge, WASM, workers) without bundling AJV; faster for tag-only checks
Pre-processed AJV schemas Strips $id at runtime per call Already stripped at build time Eliminates repeated work on every validation call; cleaner AJV integration
Error messages AJV's cryptic schema paths Human-friendly: "kind must equal 30166" Actionable feedback for users/developers instead of #/allOf/1/properties/kind/const

Current schemata coverage of nostr-watch kinds and tags

Kinds (9 used by nostr-watch)

Kind Schema? Registry? Validator? Notes
0 No tag constraints to validate
2 Deprecated (replaced by kind 10002)
3 No tag constraints to validate
1066 nostr-watch extension, not standardized
10002 Full coverage
10166 PR #106 adds tag constraints → would generate a validator
20166 nostr-watch extension, not standardized
30002 Full coverage
30166 PR #106 enriches tag constraints → richer validator

3 fully covered, 3 partial (schema but no validator), 3 missing (2 non-standard)

Tags (18 used by nostr-watch)

Tag Schema? TS Type? NIP-66 semantic match?
d ✓ DTag
r ✓ RTag
n ✓ NTag ⚠️ Schemata = Bitcoin networks; NIP-66 = clearnet/tor/i2p
N No TS type generated for uppercase N
R No TS type generated for uppercase R
c ✓ CTag ⚠️ Schemata = commitment; NIP-66 = check type
k ✓ KTag
p ✓ PTag
s ✓ STag ⚠️ Schemata = status; NIP-66 = software name
g ✓ GTag
l ✓ LTag
L ✓ UpperLTag
frequency NIP-66 specific, not in schemata
timeout NIP-66 specific, not in schemata
rtt-open NIP-66 specific, not in schemata
rtt-read NIP-66 specific, not in schemata
rtt-write NIP-66 specific, not in schemata
client ✓ ClientTag

Semantic collisions: Tags n, c, and s have different meanings across NIPs. The generic tag types in tags.d.ts reflect their original NIP definitions, not NIP-66. This is expected — the kind-level validators (not tag-level types) enforce NIP-66-specific values per kind.

Missing tag schemas: frequency, timeout, rtt-open, rtt-read, rtt-write are NIP-66-specific tags with no standalone schemas in schemata. However, PR #106 adds these as kind-level tag constraints on kinds 10166 and 30166, so the codegen validators will enforce them.

Dependency on schemata PR #106

schemata PR #106 enriches the NIP-66 kind schemas with per-tag validation:

  • kind 10166 gains constraints for: frequency, timeout, c, g tags
  • kind 30166 gains constraints for: rtt-*, n, N, R, T, t, k, g, l tags

Before #106: codegen generates nearly empty validators for NIP-66 kinds (only d tag check on 30166).
After #106: codegen generates validators that check tag structure, value patterns, and enums — actually catching the bugs found in nostr-watch's Kind30166.ts.

This integration should wait for #106 to merge and a new schemata release to publish.

Where validation is used today in nostr-watch

Component Role Schema validation?
GUI (browser) Dashboard Yes — schemata-js-ajv in Web Worker
Auditor (library) Protocol compliance testing Yes — @nostrability/schemata directly
rstate (server) REST + MCP API AJV for MCP tool output only, not Nostr schemas
relaymon (server) Publishes Kind 10166/30166/1066 None 🧪 add schemata-cogen lightweight validators, types 🧪
trawler (server) Crawls relay lists (Kind 2/3/10002) None 🧪 add schemata-cogen lightweight validators, types 🧪
route66 (library) Consumes/aggregates NIP-66 events None 🧪 add schemata-cogen lightweight validators, types🧪

Production publish and consume paths are entirely unvalidated.

Bugs found in nostr-watch event construction

Kind30166.ts — RTT validation checks the wrong variable:

// BUG: checks 'open' not 'read'
if( read && typeof open === 'number' && read > 0 ){
  tags.push(['rtt-read', String(Math.round(read))]);
}
// BUG: checks 'open' not 'write'
if( write && typeof open === 'number' && write > 0 ){
  tags.push(['rtt-write', String(Math.round(write))]);
}

Other type safety gaps:

  • Kind10166Data.networks accepts arbitrary strings (should be "clearnet" | "tor" | "i2p")
  • Kind30166 CheckData.info.supported_nips accepts scalar numbers (not just arrays)
  • Kind30166 silently truncates attributes to 9 and kinds to 21 without warning
  • SSL date parsing can produce NaN silently
  • GeoData is fully untyped ({[key: string]: any})

Proposed integration

New package: libraries/schemata-codegen/

A new workspace package @nostrwatch/schemata-codegen containing generated outputs:

libraries/schemata-codegen/
  package.json          # zero runtime dependencies, tsc build
  tsconfig.json
  generate.sh           # regeneration script
  src/
    index.ts            # barrel re-export
    kinds.d.ts          # 177 kind event interfaces
    tags.d.ts           # 156 tag tuple types
    validators.ts       # ~132 zero-dep kind validators + 3 tag validators
    kind-registry.ts    # KIND_REGISTRY, KIND_NAMES, KNOWN_KIND_NUMBERS
    error-messages.ts   # human-friendly error messages
  ajv-schemas/          # 177 pre-processed JSON schemas for AJV

Full set (all 177 kinds, 156 tags) — tree-shakeable, zero runtime cost for unused.

Automation: CI drift detection

Prerequisite: Publish @nostrability/schemata-codegen to npm.

  1. Add @nostrability/schemata-codegen and @nostrability/schemata as devDependencies
  2. generate.sh runs npx schemata-codegen --schemas node_modules/@nostrability/schemata/dist ...
  3. CI drift check: pnpm generate && git diff --exit-code — fails if generated files are stale
  4. Renovate/Dependabot bumps versions when either package publishes

Performance cost

  • Runtime: Near-zero. Types erased at compile time. Validators do simple string/array checks (microseconds). Event publishing is network-bound (milliseconds).
  • Build: ~1-2 seconds for tsc. Zero new runtime dependencies.
  • Bundle: Tree-shakeable — unused exports stripped.

Relationship to existing packages

  • Does not touch libraries/schemata-js-ajv — coexists as complementary package
  • Does not touch libraries/schemata/ (deprecated local schema copy)
  • Future: schemata-js-ajv could import pre-processed AJV schemas instead of runtime $id stripping

Sequencing

  1. schemata #105 — issue filed for NIP-66 gaps
  2. schemata #106 — PR adding NIP-66 tag constraints (must merge first)
  3. 🔲 Publish new schemata release with enriched NIP-66 schemas
  4. 🔲 Publish @nostrability/schemata-codegen to npm (this repo)
  5. 🔲 Create @nostrwatch/schemata-codegen package in nostr-watch (PR)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions