Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions